#!/usr/bin/env -S ts-node-esm --compilerOptions='{"module": "es2022"}' $.verbose = true; import { $ } from "zx"; // Or import "zx/globals"; import { spawn } from "child_process"; import { PassThrough } from "stream"; function sleep(time: number) { return new Promise((resolve) => { setTimeout(() => resolve(), time); }); } void (async function () { const device_ids = (await $`ls -1 /dev/video*`).stdout .split("\n") .slice(0, -1); const device_names = await Promise.all( device_ids.map(async (device_id) => { return ( await $`v4l2-ctl --device ${device_id} --info | grep --extended-regexp "(Model|Card type)" | head -n 1 | cut -d: -f2` ).stdout.replace("\n", ""); }) ); let zenity_args = ""; for (let i in device_ids) { zenity_args += device_ids[i] + "\n" + device_names[i] + "\n"; } const dev = ( await $`echo ${zenity_args} | zenity --list --column=device --column=name` ).stdout.replace("\n", ""); const source = spawn("ffmpeg", [ "-f", "video4linux2", "-input_format", "nv12", "-thread_queue_size", "512", "-video_size", "3840x2160", "-i", dev, "-f", "nut", "-c", "copy", "pipe:1", ]); const preview = spawn("ffplay", [ "-f", "nut", "-use_wallclock_as_timestamps", "1", "-", ]); const convert_and_add_audio = spawn("ffmpeg", [ "-y", "-vaapi_device", "/dev/dri/renderD128", "-f", "nut", "-i", "-", "-f", "pulse", "-thread_queue_size", "512", "-i", "alsa_input.usb-Elgato_Cam_Link_4K_0123456789000-03.analog-stereo", "-vf", "format=nv12,hwupload", "-c:v", "h264_vaapi", "-b:v", "12M", "-f", "mp4", `output-${Date.now()}.mp4`, ]); console.log("started ffmpeg..."); source.stderr.on("data", (data) => { // console.error("source", data.toString()); }); convert_and_add_audio.stderr.on("data", (data) => { // console.error("convert", data.toString()); }); preview.stderr.on("data", (data) => { // console.error("preview", data.toString()); }); // preview.stderr.on("data", (data) => console.error(data.toString())); const source_stream_for_convert = new PassThrough(); const source_stream_for_preview = new PassThrough(); // const source_stream_for_dump = new PassThrough(); // source.stdout.pipe(source_stream_for_convert); // // source.stdout.pipe(source_stream_for_preview); // source.stdout.pipe(source_stream_for_dump); // source_stream_for_convert.pipe(convert_and_add_audio.stdin); // source_stream_for_preview.pipe(preview.stdin); let preview_ok = true; const stats = {}; let count = 0; let preview_stopped = false; let preview_can_resume = false; let preview_should_stop_asap = false; source.stdout.on("data", (data) => { // this implements framedropping. we detect when a frame starts and emit it only when the stdin is drained count++; // console.log("got data!", a++); const beginning = data.slice(0, 4).join(""); if (beginning == "7875228173") { //means the beginning of the frame (counted the most frequent start of data and that was the most frequent) if (preview_can_resume) { preview_stopped = false; } if (preview_should_stop_asap) { preview_should_stop_asap = false; preview_stopped = true; } } if (!preview_stopped) { let preview_result = preview.stdin.write(data); if (!preview_result && !preview_should_stop_asap) { preview_should_stop_asap = true; preview_can_resume = false; preview.stdin.once("drain", () => { preview_can_resume = true; }); } } convert_and_add_audio.stdin.write(data); }); })();