1- import { execFileSync } from "node:child_process" ;
1+ import { execFileSync , spawnSync } from "node:child_process" ;
22import {
33 copyFileSync ,
44 mkdirSync ,
@@ -9,16 +9,20 @@ import {
99 writeFileSync ,
1010} from "node:fs" ;
1111import { tmpdir } from "node:os" ;
12- import { dirname , join } from "node:path" ;
12+ import { basename , dirname , join } from "node:path" ;
1313import { MEDIA_DURATION_FIXTURES } from "@hyperframes/parsers/media-duration-fixtures" ;
1414import { fileURLToPath } from "node:url" ;
1515import { afterEach , beforeAll , describe , expect , it } from "vitest" ;
1616import { ensureDOMParser } from "../utils/dom.js" ;
17- import { createProbeGate , describeProject } from "./describeProject.js" ;
17+ import { createProbeGate , describeProject , type MeasureMedia } from "./describeProject.js" ;
1818import { formatTimeline } from "./formatTimeline.js" ;
1919
20- const REAL_AUDIO = new URL ( "../../../../skills/media-use/audio/assets/sfx/pop.mp3" , import . meta. url )
21- . pathname ;
20+ const REAL_AUDIO = fileURLToPath (
21+ new URL ( "../../../../skills/media-use/audio/assets/sfx/pop.mp3" , import . meta. url ) ,
22+ ) ;
23+ const hasFfprobe = spawnSync ( "ffprobe" , [ "-version" ] ) . status === 0 ;
24+ /** Recorded ffprobe answer for pop.mp3 (0.72 s), so the resolver path runs on runners without ffprobe. */
25+ const POP_SECONDS = async ( ) => 0.72 ;
2226
2327const INDEX = `<html><body>
2428<div data-composition-id="main" data-width="1920" data-height="1080" data-duration="10">
@@ -38,12 +42,17 @@ const TINY_PNG = Buffer.from(
3842 "base64" ,
3943) ;
4044
41- const rowsOf = async ( html : string , withSting = false , setup ?: ( root : string ) => void ) => {
45+ const rowsOf = async (
46+ html : string ,
47+ withSting = false ,
48+ setup ?: ( root : string ) => void ,
49+ measure ?: MeasureMedia ,
50+ ) => {
4251 const index = project ( ) ;
4352 writeFileSync ( index , html ) ;
4453 if ( withSting ) copyFileSync ( REAL_AUDIO , join ( dir , "sting.mp3" ) ) ;
4554 setup ?.( dir ) ;
46- const timeline = await describeProject ( index ) ;
55+ const timeline = await describeProject ( index , measure ) ;
4756 return { rows : timeline . tracks . flatMap ( ( t ) => t . rows ) , text : formatTimeline ( timeline ) } ;
4857} ;
4958
@@ -243,26 +252,46 @@ describe("describeProject", () => {
243252 } = await rowsOf (
244253 `<div data-composition-id="m"><audio id="s" src="sting.mp3" data-start="0" data-playback-start="0.2" data-playback-rate="2"></audio></div>` ,
245254 true ,
255+ undefined ,
256+ POP_SECONDS ,
246257 ) ;
247258 // (0.72 - 0.2) / 2, hand-computed
248259 expect ( row ! . duration ) . toBeCloseTo ( 0.26 , 1 ) ;
249260 } ) ;
250261
251- it ( "probes a media leaf's real duration when none is authored" , async ( ) => {
262+ it ( "takes a media leaf's duration from the probe when none is authored" , async ( ) => {
263+ const probed : string [ ] = [ ] ;
252264 const {
253265 rows : [ row ] ,
254266 text,
255267 } = await rowsOf (
256268 `<div data-composition-id="m"><audio id="sting" src="sting.mp3" data-start="0"></audio></div>` ,
257269 true ,
270+ undefined ,
271+ async ( file , tag ) => {
272+ probed . push ( `${ tag } :${ basename ( file ) } ` ) ;
273+ return POP_SECONDS ( ) ;
274+ } ,
258275 ) ;
276+ expect ( probed ) . toEqual ( [ "audio:sting.mp3" ] ) ;
259277 expect ( row ) . toMatchObject ( { durationAuthored : false , durationSource : "media" } ) ;
260278 expect ( row ! . pendingReason ) . toBeNull ( ) ;
261279 expect ( row ! . duration ) . toBeCloseTo ( 0.72 , 1 ) ;
262280 expect ( row ! . end ) . toBeCloseTo ( 0.72 , 1 ) ;
263281 expect ( text ) . toContain ( "duration=media" ) ;
264282 } ) ;
265283
284+ it . skipIf ( ! hasFfprobe ) ( "measures a real audio file with ffprobe by default" , async ( ) => {
285+ const {
286+ rows : [ row ] ,
287+ } = await rowsOf (
288+ `<div data-composition-id="m"><audio id="sting" src="sting.mp3" data-start="0"></audio></div>` ,
289+ true ,
290+ ) ;
291+ expect ( row ) . toMatchObject ( { durationSource : "media" } ) ;
292+ expect ( row ! . duration ) . toBeCloseTo ( 0.72 , 1 ) ;
293+ } ) ;
294+
266295 it ( "reports pending with a reason instead of guessing when the source file is missing" , async ( ) => {
267296 const {
268297 rows : [ row ] ,
0 commit comments