131 lines
3.4 KiB
TypeScript
Executable File
131 lines
3.4 KiB
TypeScript
Executable File
#!/usr/bin/env -S ts-node-esm --compilerOptions='{"module": "es2022"}'
|
|
|
|
$.verbose = false;
|
|
|
|
import { $ } from "zx";
|
|
// Or
|
|
import "zx/globals";
|
|
import { spawn } from "child_process";
|
|
import { PassThrough } from "stream";
|
|
|
|
function sleep(time: number) {
|
|
return new Promise<void>((resolve) => {
|
|
setTimeout(() => resolve(), time);
|
|
});
|
|
}
|
|
|
|
void (async function () {
|
|
const pick_device =
|
|
await $`ls -1 /dev/video* | xargs -I {} bash -c "v4l2-ctl --device {} --info | grep --extended-regexp \\"(Model|Card type)\\" | awk -F: '{print \\$2}' | awk '{print \"{}\" \",\" \\$0}' | tr , '\n'" | zenity --list --column=device --column=name`;
|
|
|
|
const dev = `/dev/video${pick_device.stdout.split(" ")[0]}`;
|
|
|
|
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);
|
|
});
|
|
})();
|