@@ -2,7 +2,10 @@ import { file, type Subprocess, spawn } from "bun";
22import type { VideoMetadata } from "./job-manager" ;
33import { createTempFile , type TempFileHandle } from "./temp-files" ;
44
5- const PROCESS_TIMEOUT_MS = 30 * 60 * 1000 ;
5+ const PROCESS_TIMEOUT_MS = 45 * 60 * 1000 ;
6+ const PROCESS_TIMEOUT_PER_SECOND_MS = 20_000 ;
7+ const MAX_PROCESS_TIMEOUT_MS = 2 * 60 * 60 * 1000 ;
8+ const PROCESS_EXIT_WAIT_MS = 5_000 ;
69const THUMBNAIL_TIMEOUT_MS = 60_000 ;
710const DOWNLOAD_TIMEOUT_MS = 10 * 60 * 1000 ;
811const MAX_STDERR_BYTES = 64 * 1024 ;
@@ -15,6 +18,7 @@ export interface VideoProcessingOptions {
1518 crf ?: number ;
1619 preset ?: "ultrafast" | "fast" | "medium" | "slow" ;
1720 remuxOnly ?: boolean ;
21+ timeoutMs ?: number ;
1822}
1923
2024const DEFAULT_OPTIONS : Required < VideoProcessingOptions > = {
@@ -25,6 +29,7 @@ const DEFAULT_OPTIONS: Required<VideoProcessingOptions> = {
2529 crf : 23 ,
2630 preset : "medium" ,
2731 remuxOnly : false ,
32+ timeoutMs : PROCESS_TIMEOUT_MS ,
2833} ;
2934
3035export interface ThumbnailOptions {
@@ -64,15 +69,38 @@ function killProcess(proc: Subprocess): void {
6469 } catch { }
6570}
6671
67- async function withTimeout < T > (
72+ async function waitForProcessExit (
73+ proc : Pick < Subprocess , "exited" > ,
74+ timeoutMs = PROCESS_EXIT_WAIT_MS ,
75+ ) : Promise < void > {
76+ await Promise . race ( [
77+ proc . exited . then (
78+ ( ) => undefined ,
79+ ( ) => undefined ,
80+ ) ,
81+ new Promise < void > ( ( resolve ) => {
82+ setTimeout ( resolve , timeoutMs ) ;
83+ } ) ,
84+ ] ) ;
85+ }
86+
87+ async function terminateProcess ( proc : Subprocess ) : Promise < void > {
88+ killProcess ( proc ) ;
89+ await waitForProcessExit ( proc ) ;
90+ }
91+
92+ export async function withTimeout < T > (
6893 promise : Promise < T > ,
6994 timeoutMs : number ,
70- cleanup ?: ( ) => void ,
95+ cleanup ?: ( ) => void | Promise < void > ,
7196) : Promise < T > {
7297 let timeoutId : ReturnType < typeof setTimeout > | undefined ;
98+ let cleanupPromise : Promise < void > | undefined ;
7399 const timeoutPromise = new Promise < never > ( ( _ , reject ) => {
74100 timeoutId = setTimeout ( ( ) => {
75- cleanup ?.( ) ;
101+ cleanupPromise = ( async ( ) => {
102+ await cleanup ?.( ) ;
103+ } ) ( ) . catch ( ( ) => undefined ) ;
76104 reject ( new Error ( `Operation timed out after ${ timeoutMs } ms` ) ) ;
77105 } , timeoutMs ) ;
78106 } ) ;
@@ -82,6 +110,9 @@ async function withTimeout<T>(
82110 if ( timeoutId ) clearTimeout ( timeoutId ) ;
83111 return result ;
84112 } catch ( err ) {
113+ if ( cleanupPromise ) {
114+ await cleanupPromise ;
115+ }
85116 if ( timeoutId ) clearTimeout ( timeoutId ) ;
86117 throw err ;
87118 }
@@ -283,7 +314,7 @@ export async function repairContainer(
283314 ) ;
284315 } ) ( ) ,
285316 REPAIR_TIMEOUT_MS ,
286- ( ) => killProcess ( proc ) ,
317+ ( ) => terminateProcess ( proc ) ,
287318 ) ;
288319
289320 return repairedFile ;
@@ -294,7 +325,7 @@ export async function repairContainer(
294325 if ( abortCleanup ) {
295326 abortSignal ?. removeEventListener ( "abort" , abortCleanup ) ;
296327 }
297- killProcess ( proc ) ;
328+ await terminateProcess ( proc ) ;
298329 }
299330}
300331
@@ -329,6 +360,23 @@ function buildExtraOutputFlags(flags: ResilientInputFlags): string[] {
329360 return [ ] ;
330361}
331362
363+ function getProcessTimeoutMs (
364+ durationSeconds : number ,
365+ baseTimeoutMs : number ,
366+ ) : number {
367+ if ( ! Number . isFinite ( durationSeconds ) || durationSeconds <= 0 ) {
368+ return baseTimeoutMs ;
369+ }
370+
371+ return Math . min (
372+ MAX_PROCESS_TIMEOUT_MS ,
373+ Math . max (
374+ baseTimeoutMs ,
375+ Math . ceil ( durationSeconds * PROCESS_TIMEOUT_PER_SECOND_MS ) ,
376+ ) ,
377+ ) ;
378+ }
379+
332380export async function processVideo (
333381 inputPath : string ,
334382 metadata : VideoMetadata ,
@@ -355,6 +403,10 @@ export async function processVideo(
355403 const extraOutputArgs = resilientFlags
356404 ? buildExtraOutputFlags ( resilientFlags )
357405 : [ ] ;
406+ const processTimeoutMs = getProcessTimeoutMs (
407+ metadata . duration ,
408+ opts . timeoutMs ,
409+ ) ;
358410
359411 const ffmpegArgs : string [ ] = [
360412 "ffmpeg" ,
@@ -473,8 +525,8 @@ export async function processVideo(
473525 throw new Error ( "FFmpeg produced empty output file" ) ;
474526 }
475527 } ) ( ) ,
476- PROCESS_TIMEOUT_MS ,
477- ( ) => killProcess ( proc ) ,
528+ processTimeoutMs ,
529+ ( ) => terminateProcess ( proc ) ,
478530 ) ;
479531
480532 return outputTempFile ;
@@ -485,7 +537,7 @@ export async function processVideo(
485537 if ( abortCleanup ) {
486538 abortSignal ?. removeEventListener ( "abort" , abortCleanup ) ;
487539 }
488- killProcess ( proc ) ;
540+ await terminateProcess ( proc ) ;
489541 }
490542}
491543
0 commit comments