@@ -24,6 +24,10 @@ use std::sync::{
2424use std:: time:: { Duration , Instant , SystemTime , UNIX_EPOCH } ;
2525use yuv_sys;
2626
27+ mod timestamp_burn;
28+
29+ use timestamp_burn:: TimestampOverlay ;
30+
2731#[ derive( Parser , Debug ) ]
2832#[ command( author, version, about, long_about = None ) ]
2933struct Args {
@@ -83,6 +87,10 @@ struct Args {
8387 #[ arg( long, default_value_t = false ) ]
8488 attach_timestamp : bool ,
8589
90+ /// Burn the attached timestamp into each video frame; does nothing unless --attach-timestamp is also enabled
91+ #[ arg( long, default_value_t = false ) ]
92+ burn_timestamp : bool ,
93+
8694 /// Shared encryption key for E2EE (enables AES-GCM end-to-end encryption when set)
8795 #[ arg( long) ]
8896 e2ee_key : Option < String > ,
@@ -118,8 +126,8 @@ struct PublisherTimingSummary {
118126 camera_capture_ms : RollingMs ,
119127 decode_ms : RollingMs ,
120128 buffer_convert_ms : RollingMs ,
129+ frame_draw_ms : RollingMs ,
121130 buffer_prepare_ms : RollingMs ,
122- webrtc_capture_ms : RollingMs ,
123131 capture_to_webrtc_capture_ms : RollingMs ,
124132}
125133
@@ -128,8 +136,8 @@ impl PublisherTimingSummary {
128136 self . camera_capture_ms . reset ( ) ;
129137 self . decode_ms . reset ( ) ;
130138 self . buffer_convert_ms . reset ( ) ;
139+ self . frame_draw_ms . reset ( ) ;
131140 self . buffer_prepare_ms . reset ( ) ;
132- self . webrtc_capture_ms . reset ( ) ;
133141 self . capture_to_webrtc_capture_ms . reset ( ) ;
134142 }
135143}
@@ -146,11 +154,10 @@ fn format_timing_line(timings: &PublisherTimingSummary) -> String {
146154 "convert_to_i420 {:.2}" ,
147155 timings. buffer_convert_ms. average( ) . unwrap_or_default( )
148156 ) ) ;
157+ if let Some ( frame_draw_ms) = timings. frame_draw_ms . average ( ) {
158+ parts. push ( format ! ( "frame_draw {:.2}" , frame_draw_ms) ) ;
159+ }
149160 parts. push ( format ! ( "buffer {:.2}" , timings. buffer_prepare_ms. average( ) . unwrap_or_default( ) ) ) ;
150- parts. push ( format ! (
151- "webrtc_capture {:.2}" ,
152- timings. webrtc_capture_ms. average( ) . unwrap_or_default( )
153- ) ) ;
154161 parts. push ( format ! (
155162 "capture_to_webrtc {:.2}" ,
156163 timings. capture_to_webrtc_capture_ms. average( ) . unwrap_or_default( )
@@ -381,14 +388,14 @@ async fn run(args: Args, ctrl_c_received: Arc<AtomicBool>) -> Result<()> {
381388 let mut timings = PublisherTimingSummary :: default ( ) ;
382389 let mut logged_mjpeg_fallback = false ;
383390 let mut frame_counter: u32 = 0 ;
391+ let mut timestamp_overlay = ( args. attach_timestamp && args. burn_timestamp )
392+ . then ( || TimestampOverlay :: new ( width, height) ) ;
384393 loop {
385394 if ctrl_c_received. load ( Ordering :: Acquire ) {
386395 break ;
387396 }
388397 // Wait until the scheduled next frame time
389- let wait_start = Instant :: now ( ) ;
390398 ticker. tick ( ) . await ;
391- let iter_start = Instant :: now ( ) ;
392399
393400 // Capture the frame as early as possible so the attached timestamp is
394401 // close to the camera acquisition point.
@@ -398,7 +405,8 @@ async fn run(args: Args, ctrl_c_received: Arc<AtomicBool>) -> Result<()> {
398405 let camera_frame_acquired_at = Instant :: now ( ) ;
399406 let ( stride_y, stride_u, stride_v) = frame. buffer . strides ( ) ;
400407 let ( data_y, data_u, data_v) = frame. buffer . data_mut ( ) ;
401- let ( decode_finished_at, buffer_ready_at, used_decode_path) = if is_yuyv {
408+ let stride_y_usize = stride_y as usize ;
409+ let ( decode_finished_at, mut buffer_ready_at, used_decode_path) = if is_yuyv {
402410 // Fast path for YUYV: convert directly to I420 via libyuv
403411 let src = frame_buf. buffer ( ) ;
404412 let src_bytes = src. as_ref ( ) ;
@@ -513,6 +521,15 @@ async fn run(args: Args, ctrl_c_received: Arc<AtomicBool>) -> Result<()> {
513521 }
514522 } ;
515523
524+ let mut frame_draw_ms = None ;
525+ if let Some ( overlay) = timestamp_overlay. as_mut ( ) {
526+ let overlay_started_at = Instant :: now ( ) ;
527+ overlay. draw ( data_y, stride_y_usize, capture_wall_time_us) ;
528+ let overlay_finished_at = Instant :: now ( ) ;
529+ frame_draw_ms = Some ( ( overlay_finished_at - overlay_started_at) . as_secs_f64 ( ) * 1000.0 ) ;
530+ buffer_ready_at = overlay_finished_at;
531+ }
532+
516533 // Update RTP timestamp (monotonic, microseconds since start)
517534 frame. timestamp_us = start_ts. elapsed ( ) . as_micros ( ) as i64 ;
518535 // Optionally attach wall-clock time as user timestamp and frame_id
@@ -541,12 +558,12 @@ async fn run(args: Args, ctrl_c_received: Arc<AtomicBool>) -> Result<()> {
541558 timings
542559 . buffer_convert_ms
543560 . record ( ( buffer_ready_at - decode_finished_at) . as_secs_f64 ( ) * 1000.0 ) ;
561+ if let Some ( frame_draw_ms) = frame_draw_ms {
562+ timings. frame_draw_ms . record ( frame_draw_ms) ;
563+ }
544564 timings
545565 . buffer_prepare_ms
546566 . record ( ( buffer_ready_at - camera_frame_acquired_at) . as_secs_f64 ( ) * 1000.0 ) ;
547- timings
548- . webrtc_capture_ms
549- . record ( ( webrtc_capture_finished_at - buffer_ready_at) . as_secs_f64 ( ) * 1000.0 ) ;
550567 timings. capture_to_webrtc_capture_ms . record (
551568 ( webrtc_capture_finished_at - camera_capture_started_at) . as_secs_f64 ( ) * 1000.0 ,
552569 ) ;
0 commit comments