@@ -21,6 +21,8 @@ import type {
2121 InferProps ,
2222} from './props-if-has-props' ;
2323import { createFrame } from './take-screenshot' ;
24+ import { createThrottledProgressCallback } from './throttle-progress' ;
25+ import { validateVideoFrame , type OnFrameCallback } from './validate-video-frame' ;
2426import { waitForReady } from './wait-for-ready' ;
2527
2628export type InputPropsIfHasProps <
@@ -78,6 +80,7 @@ type OptionalRenderMediaOnWebOptions<Schema extends AnyZodObject> = {
7880 frameRange : FrameRange | null ;
7981 transparent : boolean ;
8082 onArtifact : OnArtifact | null ;
83+ onFrame : OnFrameCallback | null ;
8184} ;
8285
8386export type RenderMediaOnWebOptions <
@@ -96,13 +99,10 @@ type InternalRenderMediaOnWebOptions<
9699
97100// TODO: More containers
98101// TODO: Audio
99- // TODO: onFrame
100102// TODO: Metadata
101103// TODO: Validating inputs
102104// TODO: Web file system API
103105// TODO: Apply defaultCodec
104- // TODO: Throttle onProgress
105- // TODO: getStaticFiles()
106106
107107const internalRenderMediaOnWeb = async <
108108 Schema extends AnyZodObject ,
@@ -124,6 +124,7 @@ const internalRenderMediaOnWeb = async <
124124 frameRange,
125125 transparent,
126126 onArtifact,
127+ onFrame,
127128} : InternalRenderMediaOnWebOptions < Schema , Props > ) => {
128129 const cleanupFns : ( ( ) => void ) [ ] = [ ] ;
129130 const format = containerToMediabunnyContainer ( container ) ;
@@ -249,6 +250,8 @@ const internalRenderMediaOnWeb = async <
249250 encodedFrames : 0 ,
250251 } ;
251252
253+ const throttledOnProgress = createThrottledProgressCallback ( onProgress ) ;
254+
252255 for ( let i = realFrameRange [ 0 ] ; i <= realFrameRange [ 1 ] ; i ++ ) {
253256 timeUpdater . current ?. update ( i ) ;
254257 await waitForReady ( {
@@ -274,25 +277,42 @@ const internalRenderMediaOnWeb = async <
274277 throw new Error ( 'renderMediaOnWeb() was cancelled' ) ;
275278 }
276279
280+ const timestamp = Math . round (
281+ ( ( i - realFrameRange [ 0 ] ) / resolved . fps ) * 1_000_000 ,
282+ ) ;
277283 const videoFrame = new VideoFrame ( imageData , {
278- timestamp : Math . round (
279- ( ( i - realFrameRange [ 0 ] ) / resolved . fps ) * 1_000_000 ,
280- ) ,
284+ timestamp,
281285 } ) ;
282286 progress . renderedFrames ++ ;
283- onProgress ?.( { ...progress } ) ;
287+ throttledOnProgress ?.( { ...progress } ) ;
288+
289+ // Process frame through onFrame callback if provided
290+ let frameToEncode = videoFrame ;
291+ if ( onFrame ) {
292+ const returnedFrame = await onFrame ( videoFrame ) ;
293+ frameToEncode = validateVideoFrame ( {
294+ originalFrame : videoFrame ,
295+ returnedFrame,
296+ expectedWidth : resolved . width ,
297+ expectedHeight : resolved . height ,
298+ expectedTimestamp : timestamp ,
299+ } ) ;
300+ }
284301
285- await videoSampleSource . add ( new VideoSample ( videoFrame ) ) ;
302+ await videoSampleSource . add ( new VideoSample ( frameToEncode ) ) ;
286303 progress . encodedFrames ++ ;
287- onProgress ?.( { ...progress } ) ;
304+ throttledOnProgress ?.( { ...progress } ) ;
288305
289- videoFrame . close ( ) ;
306+ frameToEncode . close ( ) ;
290307
291308 if ( signal ?. aborted ) {
292309 throw new Error ( 'renderMediaOnWeb() was cancelled' ) ;
293310 }
294311 }
295312
313+ // Call progress one final time to ensure final state is reported
314+ onProgress ?.( { ...progress } ) ;
315+
296316 videoSampleSource . close ( ) ;
297317 await output . finalize ( ) ;
298318
@@ -328,5 +348,6 @@ export const renderMediaOnWeb = <
328348 frameRange : options . frameRange ?? null ,
329349 transparent : options . transparent ?? false ,
330350 onArtifact : options . onArtifact ?? null ,
351+ onFrame : options . onFrame ?? null ,
331352 } ) ;
332353} ;
0 commit comments