From 5ab5309c2befb9f53853b1b52a99c57bd055d215 Mon Sep 17 00:00:00 2001 From: Klesh Wong Date: Mon, 25 Jul 2022 11:48:34 +0800 Subject: [PATCH] feat: virtcam --- gui/awesome/rc.lua | 4 +- gui/virtcam/httpserver.lua | 82 +++++++++++++++++++++++++ {bin => gui/virtcam}/virtcam | 10 +++- gui/virtcam/virtcam.lua | 113 +++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 gui/virtcam/httpserver.lua rename {bin => gui/virtcam}/virtcam (82%) create mode 100644 gui/virtcam/virtcam.lua diff --git a/gui/awesome/rc.lua b/gui/awesome/rc.lua index 6b0a657..f282cda 100644 --- a/gui/awesome/rc.lua +++ b/gui/awesome/rc.lua @@ -774,7 +774,8 @@ awful.rules.rules = { "Wpa_gui", "veromix", "st-256color", - "xtightvncviewer" }, + "xtightvncviewer" + }, -- Note that the name property shown in xprop might be set slightly after creation of the client -- and the name shown there might not match defined rules here. @@ -883,4 +884,5 @@ awful.spawn.with_shell("bluetoothctl power on") awful.spawn.with_shell("echo DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS > ~/.cron.env") awful.spawn.with_shell("echo DISPLAY=$DISPLAY >> ~/.cron.env") awful.spawn.with_shell("grep -F 0 /sys/class/power_supply/ACAD/online && sudo light -S 30") +awful.spawn.with_shell("cd ~/dotfiles/gui/virtcam && lua virtcam.lua") -- }}} diff --git a/gui/virtcam/httpserver.lua b/gui/virtcam/httpserver.lua new file mode 100644 index 0000000..95c2697 --- /dev/null +++ b/gui/virtcam/httpserver.lua @@ -0,0 +1,82 @@ +#!/usr/bin/env lua +--[[ +requirement: sudo luarocks install http json-lua +A simple HTTP server +If a request is not a HEAD method, then reply with "Hello world!" +Usage: lua examples/server_hello.lua [] +]] + +local http_server = require "http.server" +local http_headers = require "http.headers" +local json = require "JSON" + +function NewHttpServer(host, port, handlers) + local function reply(myserver, stream) -- luacheck: ignore 212 + -- Read in headers + local req_headers = assert(stream:get_headers()) + local req_method = req_headers:get ":method" + local req_path = req_headers:get ":path" + + -- Log request to stdout + assert(io.stdout:write(string.format('[%s] "%s %s HTTP/%g" "%s" "%s"\n', + os.date("%d/%b/%Y:%H:%M:%S %z"), + req_method or "", + req_headers:get(":path") or "", + stream.connection.version, + req_headers:get("referer") or "-", + req_headers:get("user-agent") or "-" + ))) + + -- Handle request + local status = 404 + local res_body = { message = "Resource Not Found" } + local handle_key = string.format("%s %s", req_method, req_path) + local handler = handlers[handle_key] + if handler then + local req_body = nil + if req_method == "POST" or req_method == "PUT" then + req_body = json:decode(stream:get_body_as_string(0)) + end + status, res_body = handler(req_body, req_headers) + status = status or 200 + end + + -- Build response headers + local res_headers = http_headers.new() + res_headers:append(":status", tostring(status)) + res_headers:append("content-type", "application/json") + res_headers:append("Access-Control-Allow-Origin", "*") + -- Send headers to client; end the stream immediately if this was a HEAD request + assert(stream:write_headers(res_headers, req_method == "HEAD")) + if req_method ~= "HEAD" then + -- Send body, ending the stream + assert(stream:write_chunk(json:encode(res_body), true)) + end + end + + local myserver = assert(http_server.listen { + host = host or "localhost"; + port = port; + onstream = reply; + onerror = function(myserver, context, op, err, errno) -- luacheck: ignore 212 + local msg = op .. " on " .. tostring(context) .. " failed" + if err then + msg = msg .. ": " .. tostring(err) + end + assert(io.stderr:write(msg, "\n")) + end; + }) + assert(myserver:listen()) + do + local bound_port = select(3, myserver:localname()) + assert(io.stderr:write(string.format("Now listening on port %d\n", bound_port))) + end + assert(myserver:loop()) + return myserver +end + +-- NewHttpServer("127.0.0.1", 10200, { +-- ["POST /test"] = function(body, _) +-- return 200, { message = body } +-- end, +-- }) diff --git a/bin/virtcam b/gui/virtcam/virtcam similarity index 82% rename from bin/virtcam rename to gui/virtcam/virtcam index 43c76de..051d088 100755 --- a/bin/virtcam +++ b/gui/virtcam/virtcam @@ -31,14 +31,20 @@ while true; do PID=$(actualwebcam) echo piping actual webcam, press ENTER to start recording read C - echo recording, press [q] to replay - ffmpeg -y -i /dev/video22 ~/Video/seq.mp4 >/dev/null 2>&1 + echo recording for 3 seconds + ffmpeg -y -i /dev/video22 ~/Video/seq.mp4 >/dev/null 2>&1 /dev/null 2>&1 echo reversed clip created ffmpeg -y -i ~/Video/seq.mp4 -i ~/Video/reversed.mp4 -filter_complex "[0:v] [1:v] concat=n=2:v=1 [v]" -map "[v]" ~/Video/loop.mp4 >/dev/null 2>&1 echo looping clip created + pactl set-source-mute @DEFAULT_SOURCE@ 1 kill -s INT $PID echo webcam stopped, enter looping mode, press [q] to resume webcam ffmpeg -re -stream_loop -1 -i ~/Video/loop.mp4 -f v4l2 -vcodec rawvideo -pix_fmt yuyv422 -framerate 30 /dev/video21 >/dev/null 2>&1 + pactl set-source-mute @DEFAULT_SOURCE@ 0 done diff --git a/gui/virtcam/virtcam.lua b/gui/virtcam/virtcam.lua new file mode 100644 index 0000000..7a88b3e --- /dev/null +++ b/gui/virtcam/virtcam.lua @@ -0,0 +1,113 @@ +local httpserver = require "httpserver" + +local function run(cmd) + print(cmd) + local proc = io.popen(cmd .. " >/tmp/virtcam.log 2>&1") + assert(proc) + assert(proc:close()) +end + +run("sudo modprobe v4l2loopback -r -f") +run("sudo modprobe v4l2loopback video_nr=21 card_label=virtcam exclusive_caps=1") + +Ffmpeg = { cmd = "", proc = nil, name = "", started = false } + +function Ffmpeg:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o +end + +function Ffmpeg:start() + assert(self.proc == nil) + self.proc = io.popen(self.cmd .. " >/tmp/virtcam.log 2>&1", "w") + assert(self.proc) + print(self.name .. " started") + self.started = true +end + +function Ffmpeg:stop() + assert(self.proc) + self.proc:write("q") + self.proc:flush() + assert(self.proc:close()) + self.proc = nil + print(self.name .. " stopped") + self.started = false +end + +Actualcam = Ffmpeg:new { + name = "actualcam", + cmd = "ffmpeg -i /dev/video0 -vcodec copy -f v4l2 /dev/video21", +} + + +Recorder = Ffmpeg:new { + name = "recorder", + cmd = "ffmpeg -y -i /dev/video21 ~/Video/seq.mp4", +} + +Looper = Ffmpeg:new { + name = "looper", + cmd = "ffmpeg -re -stream_loop -1 -i ~/Video/loop.mp4 -f v4l2 -vcodec rawvideo -pix_fmt yuyv422 -framerate 30 /dev/video21 ", +} + + +local all = { Actualcam, Recorder, Looper } + +local function run_only(the_one) + for _, fp in ipairs(all) do + if fp ~= the_one and fp.started then + fp:stop() + end + end + if the_one and not the_one.started then + print("try to start") + the_one:start() + end +end + +local stage = "stopped" + +NewHttpServer("127.0.0.1", 10200, { + ["POST /prepare"] = function(_, _) + if stage == "stopped" then + run("pactl set-source-mute @DEFAULT_SOURCE@ 1") + run_only(Actualcam) + stage = "actual" + end + end, + ["POST /actual"] = function(_, _) + if stage ~= "actual" then + run("pactl set-source-mute @DEFAULT_SOURCE@ 0") + run_only(Actualcam) + stage = "actual" + end + end, + ["POST /fake"] = function(_, _) + if stage ~= "fake" then + run("pactl set-source-mute @DEFAULT_SOURCE@ 1") + run_only(Actualcam) + Recorder:start() + run("sleep 3") + Recorder:stop() + run("ffmpeg -y -i ~/Video/seq.mp4 -vf reverse ~/Video/reversed.mp4") + run("ffmpeg -y -i ~/Video/seq.mp4 -i ~/Video/reversed.mp4 -filter_complex '[0:v] [1:v] concat=n=2:v=1 [v]' -map '[v]' ~/Video/loop.mp4") + run_only(Looper) + stage = "fake" + end + end, + ["POST /stop"] = function(_, _) + if stage ~= "stopped" then + run_only(nil) + stage = "stopped" + end + end, + ["POST /toggle-mute"] = function(_, _) + run("pactl set-source-mute @DEFAULT_SOURCE@ toggle") + end, + ["POST /mute"] = function(_, _) + run("pactl set-source-mute @DEFAULT_SOURCE@ 1") + end, +})