( ffmpegPath: string, args: string[], timeoutMs: number, session?: NativeStaticLayoutExportSession, )
| 1205 | } |
| 1206 | |
| 1207 | async function runFfmpegWithMetrics( |
| 1208 | ffmpegPath: string, |
| 1209 | args: string[], |
| 1210 | timeoutMs: number, |
| 1211 | session?: NativeStaticLayoutExportSession, |
| 1212 | ): Promise<{ |
| 1213 | success: boolean; |
| 1214 | elapsedMs: number; |
| 1215 | stderr: string; |
| 1216 | code: number | null; |
| 1217 | signal: NodeJS.Signals | null; |
| 1218 | }> { |
| 1219 | const startedAt = getNowMs(); |
| 1220 | return await new Promise((resolve) => { |
| 1221 | const child = spawn(ffmpegPath, args, { |
| 1222 | stdio: ["ignore", "ignore", "pipe"], |
| 1223 | }); |
| 1224 | if (session) { |
| 1225 | session.currentProcess = child; |
| 1226 | if (session.terminating) { |
| 1227 | child.kill("SIGKILL"); |
| 1228 | } |
| 1229 | } |
| 1230 | let stderr = ""; |
| 1231 | let settled = false; |
| 1232 | const timeout = setTimeout(() => { |
| 1233 | if (settled) return; |
| 1234 | try { |
| 1235 | child.kill("SIGKILL"); |
| 1236 | } catch { |
| 1237 | // ignore |
| 1238 | } |
| 1239 | }, timeoutMs); |
| 1240 | |
| 1241 | child.stderr.on("data", (chunk: Buffer) => { |
| 1242 | stderr += chunk.toString(); |
| 1243 | }); |
| 1244 | child.once("error", (error) => { |
| 1245 | if (settled) return; |
| 1246 | settled = true; |
| 1247 | if (session?.currentProcess === child) { |
| 1248 | session.currentProcess = null; |
| 1249 | } |
| 1250 | clearTimeout(timeout); |
| 1251 | resolve({ |
| 1252 | success: false, |
| 1253 | elapsedMs: getNowMs() - startedAt, |
| 1254 | stderr: error instanceof Error ? error.message : String(error), |
| 1255 | code: null, |
| 1256 | signal: null, |
| 1257 | }); |
| 1258 | }); |
| 1259 | child.once("close", (code, signal) => { |
| 1260 | if (settled) return; |
| 1261 | settled = true; |
| 1262 | if (session?.currentProcess === child) { |
| 1263 | session.currentProcess = null; |
| 1264 | } |
no test coverage detected