@@ -2,6 +2,7 @@ import { afterAll, describe, expect, it, mock } from "bun:test";
22import { mkdtempSync , rmSync } from "node:fs" ;
33import { tmpdir } from "node:os" ;
44import { join } from "node:path" ;
5+ import type { RateSpec } from "@hyperframes/core" ;
56
67const workDir = mkdtempSync ( join ( tmpdir ( ) , "hf-stage-test-" ) ) ;
78afterAll ( ( ) => rmSync ( workDir , { recursive : true , force : true } ) ) ;
@@ -413,6 +414,99 @@ describe("runProbeStage — forceScreenshot threading", () => {
413414 } ) ;
414415
415416 // Scene in-point 5 and clip trim 1: the collector starts picture and sound at source 6.
417+ it . each ( [
418+ { tagName : "video" , existing : false } ,
419+ { tagName : "audio" , existing : false } ,
420+ { tagName : "video" , existing : true } ,
421+ { tagName : "audio" , existing : true } ,
422+ ] ) ( "retains a runtime $tagName rate (existing: $existing)" , async ( { tagName, existing } ) => {
423+ resetRetryMocks ( ) ;
424+ browserMediaResults = [
425+ {
426+ id : "clip" ,
427+ tagName,
428+ src : "clip.mp4" ,
429+ start : 0 ,
430+ end : 3 ,
431+ duration : 3 ,
432+ mediaStart : 0.5 ,
433+ playbackRate : 1.5 ,
434+ loop : false ,
435+ hasAudio : true ,
436+ volume : 1 ,
437+ muted : false ,
438+ } ,
439+ ] ;
440+ const { runProbeStage } = await import ( "./probeStage.js" ) ;
441+ const input = makeProbeInput ( { } ) ;
442+ const clip = {
443+ id : "clip" ,
444+ src : "clip.mp4" ,
445+ start : 0 ,
446+ end : 3 ,
447+ mediaStart : 0.5 ,
448+ playbackRate : 1 ,
449+ } ;
450+ if ( existing && tagName === "video" ) {
451+ input . composition . videos . push ( { ...clip , loop : false , hasAudio : true } ) ;
452+ input . composition . audios . push ( { ...clip , id : "clip-audio" , layer : 0 , type : "video" } ) ;
453+ }
454+ if ( existing && tagName === "audio" ) {
455+ input . composition . audios . push ( { ...clip , layer : 0 , type : "audio" } ) ;
456+ }
457+
458+ await runProbeStage ( input ) ;
459+
460+ const media = tagName === "video" ? input . composition . videos : input . composition . audios ;
461+ expect ( media [ 0 ] ) . toMatchObject ( { playbackRate : 1.5 , mediaStart : 0.5 } ) ;
462+ if ( existing && tagName === "video" ) {
463+ expect ( input . composition . audios [ 0 ] ) . toMatchObject ( { playbackRate : 1.5 , mediaStart : 0.5 } ) ;
464+ }
465+ } ) ;
466+
467+ it ( "preserves a collected speed ramp during scalar browser reconciliation" , async ( ) => {
468+ resetRetryMocks ( ) ;
469+ browserMediaResults = [
470+ {
471+ id : "clip" ,
472+ tagName : "video" ,
473+ src : "clip.mp4" ,
474+ start : 0 ,
475+ end : 3 ,
476+ duration : 3 ,
477+ mediaStart : 0 ,
478+ playbackRate : 1 ,
479+ loop : false ,
480+ hasAudio : false ,
481+ volume : 1 ,
482+ muted : false ,
483+ } ,
484+ ] ;
485+ const { runProbeStage } = await import ( "./probeStage.js" ) ;
486+ const input = makeProbeInput ( { } ) ;
487+ const playbackRate = {
488+ target : "rate" ,
489+ points : [
490+ { t : 0 , v : 1 } ,
491+ { t : 3 , v : 2 } ,
492+ ] ,
493+ } satisfies RateSpec ;
494+ input . composition . videos . push ( {
495+ id : "clip" ,
496+ src : "clip.mp4" ,
497+ start : 0 ,
498+ end : 3 ,
499+ mediaStart : 0 ,
500+ playbackRate,
501+ loop : false ,
502+ hasAudio : false ,
503+ } ) ;
504+
505+ await runProbeStage ( input ) ;
506+
507+ expect ( input . composition . videos [ 0 ] ?. playbackRate ) . toEqual ( playbackRate ) ;
508+ } ) ;
509+
416510 it . each ( [
417511 { pageStart : 1 , expected : 6 } ,
418512 { pageStart : 3 , expected : 8 } ,
0 commit comments