| 76 | let lastJpeg: Buffer | null = null; |
| 77 | let ffStopped = false; |
| 78 | function startFfmpeg() { |
| 79 | const proc = spawn("ffmpeg", [ |
| 80 | "-hide_banner", "-loglevel", "error", |
| 81 | "-rtsp_transport", "tcp", |
| 82 | "-i", RTSP, |
| 83 | "-f", "image2pipe", "-c:v", "mjpeg", "-q:v", "4", "-r", "8", |
| 84 | "pipe:1", |
| 85 | ], { stdio: ["ignore", "pipe", "inherit"] }); |
| 86 | let buffer = Buffer.alloc(0); |
| 87 | proc.stdout!.on("data", (chunk: Buffer) => { |
| 88 | buffer = Buffer.concat([buffer, chunk]); |
| 89 | for (;;) { |
| 90 | const start = buffer.indexOf(Buffer.from([0xff, 0xd8])); |
| 91 | if (start < 0) { buffer = Buffer.alloc(0); return; } |
| 92 | const end = buffer.indexOf(Buffer.from([0xff, 0xd9]), start + 2); |
| 93 | if (end < 0) { if (start > 0) buffer = buffer.subarray(start); return; } |
| 94 | lastJpeg = Buffer.from(buffer.subarray(start, end + 2)); |
| 95 | buffer = buffer.subarray(end + 2); |
| 96 | } |
| 97 | }); |
| 98 | proc.on("exit", () => { |
| 99 | // RTSP hiccups happen mid-run — respawn unless we're shutting down. |
| 100 | if (!ffStopped) { |
| 101 | console.error(" (ffmpeg died — respawning)"); |
| 102 | setTimeout(() => startFfmpeg(), 1000); |
| 103 | } |
| 104 | }); |
| 105 | return proc; |
| 106 | } |
| 107 | |
| 108 | async function grabLuma(): Promise<Uint8Array> { |
| 109 | // Frames arrive ~0.6-1.1 s after exposure — anything arriving now may have |