@@ -39,6 +39,9 @@ struct Cli {
3939 #[ arg( long, global = true , default_value = "30" ) ]
4040 fps : u32 ,
4141
42+ #[ arg( long, global = true , default_value_t = STARTUP_TO_FIRST_FRAME_WARNING_MS ) ]
43+ startup_threshold_ms : f64 ,
44+
4245 #[ arg( long, global = true ) ]
4346 verbose : bool ,
4447
@@ -66,6 +69,7 @@ enum Commands {
6669const FPS_TOLERANCE : f64 = 2.0 ;
6770const DECODE_LATENCY_WARNING_MS : f64 = 50.0 ;
6871const SCRUB_SEEK_WARNING_MS : f64 = 40.0 ;
72+ const STARTUP_TO_FIRST_FRAME_WARNING_MS : f64 = 250.0 ;
6973const AUDIO_VIDEO_SYNC_TOLERANCE_MS : f64 = 100.0 ;
7074const CAMERA_SYNC_TOLERANCE_MS : f64 = 100.0 ;
7175
@@ -101,6 +105,8 @@ struct PlaybackTestResult {
101105 fps_ok : bool ,
102106 jitter_ms : f64 ,
103107 decode_latency_ok : bool ,
108+ startup_latency_ok : bool ,
109+ startup_threshold_ms : f64 ,
104110 errors : Vec < String > ,
105111}
106112
@@ -243,6 +249,12 @@ impl RecordingTestReport {
243249 if !result. decode_latency_ok {
244250 println ! ( " WARN: Decode latency exceeds {DECODE_LATENCY_WARNING_MS}ms!" ) ;
245251 }
252+ if !result. startup_latency_ok {
253+ println ! (
254+ " WARN: Startup-to-first-frame exceeds {:.1}ms!" ,
255+ result. startup_threshold_ms
256+ ) ;
257+ }
246258 for err in & result. errors {
247259 println ! ( " ERROR: {err}" ) ;
248260 }
@@ -390,12 +402,14 @@ async fn test_playback(
390402 meta : & StudioRecordingMeta ,
391403 segment_index : usize ,
392404 fps : u32 ,
405+ startup_threshold_ms : f64 ,
393406 verbose : bool ,
394407) -> PlaybackTestResult {
395408 let playback_start = Instant :: now ( ) ;
396409 let mut result = PlaybackTestResult {
397410 segment_index,
398411 expected_fps : fps as f64 ,
412+ startup_threshold_ms,
399413 ..Default :: default ( )
400414 } ;
401415
@@ -496,9 +510,11 @@ async fn test_playback(
496510 result. fps_ok = ( result. effective_fps - result. expected_fps ) . abs ( ) <= FPS_TOLERANCE
497511 || result. effective_fps >= result. expected_fps ;
498512 result. decode_latency_ok = result. p95_decode_time_ms <= DECODE_LATENCY_WARNING_MS ;
513+ result. startup_latency_ok = result. startup_to_first_frame_ms <= startup_threshold_ms;
499514
500515 result. passed = result. fps_ok
501516 && result. decode_latency_ok
517+ && result. startup_latency_ok
502518 && result. failed_frames == 0
503519 && result. decoded_frames > 0 ;
504520
@@ -880,6 +896,7 @@ fn discover_recordings(input_dir: &Path) -> Vec<PathBuf> {
880896async fn run_tests_on_recording (
881897 recording_path : & Path ,
882898 fps : u32 ,
899+ startup_threshold_ms : f64 ,
883900 run_decoder : bool ,
884901 run_playback : bool ,
885902 run_scrub : bool ,
@@ -969,8 +986,15 @@ async fn run_tests_on_recording(
969986 if verbose {
970987 println ! ( " Testing playback for segment {segment_idx}..." ) ;
971988 }
972- let playback_result =
973- test_playback ( & meta, studio_meta. as_ref ( ) , segment_idx, fps, verbose) . await ;
989+ let playback_result = test_playback (
990+ & meta,
991+ studio_meta. as_ref ( ) ,
992+ segment_idx,
993+ fps,
994+ startup_threshold_ms,
995+ verbose,
996+ )
997+ . await ;
974998 report. playback_results . push ( playback_result) ;
975999 }
9761000
@@ -1082,6 +1106,13 @@ fn get_failure_tags(report: &RecordingTestReport) -> Vec<String> {
10821106 if report. playback_results . iter ( ) . any ( |r| !r. decode_latency_ok ) {
10831107 tags. push ( "LATENCY" . to_string ( ) ) ;
10841108 }
1109+ if report
1110+ . playback_results
1111+ . iter ( )
1112+ . any ( |r| !r. startup_latency_ok )
1113+ {
1114+ tags. push ( "STARTUP" . to_string ( ) ) ;
1115+ }
10851116 if report. scrub_results . iter ( ) . any ( |r| !r. seek_latency_ok ) {
10861117 tags. push ( "SCRUB_LATENCY" . to_string ( ) ) ;
10871118 }
@@ -1191,7 +1222,7 @@ fn report_to_markdown(report: &RecordingTestReport) -> String {
11911222 ) ) ;
11921223 md. push_str ( & format ! (
11931224 "| ↳ Startup | {} | first_decode={:.1}ms startup_to_first={:.1}ms |\n " ,
1194- if result. startup_to_first_frame_ms > 0.0 {
1225+ if result. startup_latency_ok {
11951226 "✅"
11961227 } else {
11971228 "❌"
@@ -1505,9 +1536,10 @@ fn shell_quote(value: &str) -> String {
15051536
15061537fn build_command_string ( cli : & Cli ) -> String {
15071538 let mut command = format ! (
1508- "cargo run -p cap-recording --example playback-test-runner -- {} --fps {}" ,
1539+ "cargo run -p cap-recording --example playback-test-runner -- {} --fps {} --startup-threshold-ms {:.1} " ,
15091540 command_name( cli. command. as_ref( ) ) ,
1510- cli. fps
1541+ cli. fps,
1542+ cli. startup_threshold_ms
15111543 ) ;
15121544
15131545 if let Some ( path) = & cli. recording_path {
@@ -1598,9 +1630,10 @@ async fn main() -> anyhow::Result<()> {
15981630 println ! ( "\n Cap Playback Test Runner" ) ;
15991631 println ! ( "{}" , "=" . repeat( 40 ) ) ;
16001632 println ! (
1601- "Testing {} recording(s) at {} FPS" ,
1633+ "Testing {} recording(s) at {} FPS (startup threshold: {:.1}ms) " ,
16021634 recordings. len( ) ,
1603- cli. fps
1635+ cli. fps,
1636+ cli. startup_threshold_ms
16041637 ) ;
16051638 println ! ( ) ;
16061639
@@ -1612,6 +1645,7 @@ async fn main() -> anyhow::Result<()> {
16121645 match run_tests_on_recording (
16131646 recording_path,
16141647 cli. fps ,
1648+ cli. startup_threshold_ms ,
16151649 run_decoder,
16161650 run_playback,
16171651 run_scrub,
0 commit comments