diff --git a/streamer/src/ffmpeg.ts b/streamer/src/ffmpeg.ts new file mode 100644 index 0000000..558a957 --- /dev/null +++ b/streamer/src/ffmpeg.ts @@ -0,0 +1,58 @@ +import { ChildProcessWithoutNullStreams, spawn } from "node:child_process"; +import { EventEmitter } from "node:events"; +import { Writable } from "node:stream"; + +export interface FfmpegOpts { + rtmpUrl: string; + width: number; + height: number; + framerate: number; + videoBitrate: string; + audioBitrate: string; +} + +export class Ffmpeg extends EventEmitter { + private proc: ChildProcessWithoutNullStreams | null = null; + + constructor(private opts: FfmpegOpts) { super(); } + + start(): { videoIn: Writable; audioIn: Writable } { + const args = [ + "-loglevel", "warning", + // video in (mjpeg pipe on fd 3) + "-f", "image2pipe", "-c:v", "mjpeg", + "-r", String(this.opts.framerate), "-i", "pipe:3", + // audio in (s16le pcm pipe on fd 4) + "-f", "s16le", "-ar", "48000", "-ac", "2", "-i", "pipe:4", + // video encode + "-c:v", "libx264", "-preset", "veryfast", + "-pix_fmt", "yuv420p", + "-b:v", this.opts.videoBitrate, + "-maxrate", this.opts.videoBitrate, "-bufsize", "9000k", + "-g", String(this.opts.framerate * 2), "-keyint_min", String(this.opts.framerate * 2), + "-r", String(this.opts.framerate), + // audio encode + "-c:a", "aac", "-b:a", this.opts.audioBitrate, "-ar", "48000", + // output + "-f", "flv", this.opts.rtmpUrl, + ]; + + this.proc = spawn("ffmpeg", args, { + stdio: ["ignore", "pipe", "pipe", "pipe", "pipe"], + }) as unknown as ChildProcessWithoutNullStreams; + + const stdio = (this.proc as unknown as { stdio: Writable[] }).stdio; + const videoIn = stdio[3]!; + const audioIn = stdio[4]!; + + this.proc.stderr.on("data", (d) => this.emit("log", d.toString())); + this.proc.on("exit", (code) => this.emit("exit", code)); + + return { videoIn, audioIn }; + } + + stop(): void { + this.proc?.kill("SIGTERM"); + this.proc = null; + } +}