@@ -193,6 +193,45 @@ function buildVideoExtractionStageError(
193193 ) ;
194194}
195195
196+ export function buildHdrProbeStageError (
197+ failures : readonly Pick < ReturnType < typeof classifyVideoExtractionError > , "kind" | "retryable" > [ ] ,
198+ ) : VideoExtractionStageError {
199+ const counts = new Map < VideoExtractionFailureKind , number > ( ) ;
200+ for ( const failure of failures ) {
201+ counts . set ( failure . kind , ( counts . get ( failure . kind ) ?? 0 ) + 1 ) ;
202+ }
203+ const summary = Array . from ( counts , ( [ kind , count ] ) => ( { kind, count } ) ) . sort ( ( a , b ) =>
204+ a . kind . localeCompare ( b . kind ) ,
205+ ) ;
206+ const retryable = failures . length > 0 && failures . every ( ( failure ) => failure . retryable ) ;
207+ return new VideoExtractionStageError (
208+ retryable ? "VIDEO_EXTRACTION_FAILED" : "VIDEO_SOURCE_UNRENDERABLE" ,
209+ retryable ,
210+ summary ,
211+ ) ;
212+ }
213+
214+ type HdrProbeFailure = {
215+ error : unknown ;
216+ classified : ReturnType < typeof classifyVideoExtractionError > ;
217+ } ;
218+
219+ function isHdrProbeFailure ( failure : HdrProbeFailure | null ) : failure is HdrProbeFailure {
220+ return failure !== null ;
221+ }
222+
223+ function throwHdrProbeFailures (
224+ failures : readonly HdrProbeFailure [ ] ,
225+ mode : VideoExtractionFailureMode ,
226+ ) : void {
227+ if ( failures . length === 0 ) return ;
228+ if ( mode === "enforce" ) {
229+ throw buildHdrProbeStageError ( failures . map ( ( failure ) => failure . classified ) ) ;
230+ }
231+ const firstFailure = failures [ 0 ] ;
232+ if ( firstFailure ) throw firstFailure . error ;
233+ }
234+
196235function applyVideoExtractionFailurePolicy (
197236 result : ExtractionResult ,
198237 policy : VideoExtractionPolicy ,
@@ -242,7 +281,7 @@ export async function runExtractVideosStage(
242281 let hdrProbeTransientRetries = 0 ;
243282 if ( job . config . hdrMode !== "force-sdr" && composition . videos . length > 0 ) {
244283 log ?. info ( "Probing video color spaces..." , { videoCount : composition . videos . length } ) ;
245- await Promise . all (
284+ const probeFailures = await Promise . all (
246285 composition . videos . map ( async ( v ) => {
247286 // Use the shared resolver so a `<video src="../assets/foo">` in a
248287 // sub-composition resolves the same way the browser would (see
@@ -252,7 +291,7 @@ export async function runExtractVideosStage(
252291 const videoPath = isAbsolute ( v . src )
253292 ? v . src
254293 : resolveProjectRelativeSrc ( v . src , projectDir , compiledDir ) ;
255- if ( ! existsSync ( videoPath ) ) return ;
294+ if ( ! existsSync ( videoPath ) ) return null ;
256295 try {
257296 // Retries are separately opt-in from the failure gate. With the
258297 // default zero budget this remains the exact legacy single probe.
@@ -271,27 +310,21 @@ export async function runExtractVideosStage(
271310 nativeHdrVideoIds . add ( v . id ) ;
272311 videoTransfers . set ( v . id , detectTransfer ( meta . colorSpace ) ) ;
273312 }
313+ return null ;
274314 } catch ( error ) {
275- if ( extractionPolicy . failureMode !== "off" ) {
276- const classified = classifyVideoExtractionError ( error ) ;
277- log ?. warn ( "Video HDR metadata probe failed" , {
278- mode : extractionPolicy . failureMode ,
279- kind : classified . kind ,
280- retryable : classified . retryable ,
281- transientRetries : hdrProbeTransientRetries ,
282- } ) ;
283- if ( extractionPolicy . failureMode === "enforce" ) {
284- throw new VideoExtractionStageError (
285- classified . retryable ? "VIDEO_EXTRACTION_FAILED" : "VIDEO_SOURCE_UNRENDERABLE" ,
286- classified . retryable ,
287- [ { kind : classified . kind , count : 1 } ] ,
288- ) ;
289- }
290- }
291- throw error ;
315+ if ( extractionPolicy . failureMode === "off" ) throw error ;
316+ const classified = classifyVideoExtractionError ( error ) ;
317+ log ?. warn ( "Video HDR metadata probe failed" , {
318+ mode : extractionPolicy . failureMode ,
319+ kind : classified . kind ,
320+ retryable : classified . retryable ,
321+ transientRetries : hdrProbeTransientRetries ,
322+ } ) ;
323+ return { error, classified } ;
292324 }
293325 } ) ,
294326 ) ;
327+ throwHdrProbeFailures ( probeFailures . filter ( isHdrProbeFailure ) , extractionPolicy . failureMode ) ;
295328 }
296329
297330 // Probe images for HDR color spaces (16-bit PNGs tagged BT.2020 PQ/HLG).
0 commit comments