| 97 | } |
| 98 | |
| 99 | private spawnProc(): void { |
| 100 | if (this.proc || this.stopped) return; |
| 101 | this.generation++; |
| 102 | // Fresh stream epoch: clients must rejoin at the new init segment. |
| 103 | this.initSegment = null; |
| 104 | this.codec = null; |
| 105 | this.buffer = Buffer.alloc(0); |
| 106 | for (const c of this.clients) c.awaitingFragment = true; |
| 107 | |
| 108 | const args = [ |
| 109 | "-hide_banner", "-loglevel", "error", |
| 110 | "-rtsp_transport", "tcp", |
| 111 | "-i", this.url, |
| 112 | "-c:v", "copy", "-an", |
| 113 | "-f", "mp4", |
| 114 | "-movflags", "frag_keyframe+empty_moov+default_base_moof", |
| 115 | "pipe:1", |
| 116 | ]; |
| 117 | const proc = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] }); |
| 118 | this.proc = proc; |
| 119 | this.lastError = undefined; |
| 120 | console.log(`[mse] ffmpeg -c copy <- ${this.url}`); |
| 121 | |
| 122 | proc.stdout!.on("data", (chunk: Buffer) => this.onData(chunk)); |
| 123 | let stderrTail = ""; |
| 124 | proc.stderr!.on("data", (c: Buffer) => { |
| 125 | stderrTail = (stderrTail + c.toString()).slice(-400); |
| 126 | }); |
| 127 | proc.on("exit", (code) => { |
| 128 | // A killed/superseded process must not clobber its replacement or |
| 129 | // schedule a duplicate respawn. |
| 130 | if (this.proc !== proc) return; |
| 131 | this.proc = null; |
| 132 | if (this.stopped) return; |
| 133 | this.lastError = `ffmpeg exited (${code}): ${stderrTail.trim().split("\n").pop() ?? ""}`; |
| 134 | console.error(`[mse] ${this.lastError}`); |
| 135 | this.restartTimer = setTimeout(() => this.spawnProc(), 3000); |
| 136 | }); |
| 137 | } |
| 138 | |
| 139 | private kill(): void { |
| 140 | this.proc?.kill("SIGKILL"); |