@@ -662,8 +662,31 @@ fn run<W: Write>(
662662 // means the import never works, which would otherwise record nothing — see
663663 // MAX_CONSECUTIVE_IMPORT_FAILURES.
664664 let mut consecutive_drops: u32 = 0 ;
665+ // When the capture clock last advanced. See the heartbeat at the top of the loop.
666+ let mut last_advance = Instant :: now ( ) ;
665667
666668 loop {
669+ // THE HEARTBEAT, CHECKED ON EVERY PASS, NOT ONLY ON A TIMEOUT. `advance`
670+ // holds the last picture forward and drains the audio rings, and it used
671+ // to run only on a new frame or when `recv_timeout` expired. Any other
672+ // message restarts that timeout, and cursor messages arrive at the
673+ // compositor's rate while the mouse moves: a static screen with a moving
674+ // mouse never timed out, so nothing drained and the 2 s rings dropped
675+ // their oldest audio (getopenscreen/openscreen#936).
676+ if heartbeat_due ( last_advance, Instant :: now ( ) , config. tick ) {
677+ last_advance = Instant :: now ( ) ;
678+ if let Some ( capture) = capture. as_mut ( ) {
679+ if let Err ( message) = capture. advance ( ) {
680+ let _ = emitter. emit ( & Event :: Error {
681+ code : "encode-failed" . to_owned ( ) ,
682+ message,
683+ } ) ;
684+ exit_code = 1 ;
685+ break ;
686+ }
687+ }
688+ }
689+
667690 // Return PipeWire buffers whose dmabuf imports completed last iteration
668691 // (or that were superseded on the capture thread). This MUST run on this
669692 // loop, not the PipeWire thread — `Session::requeue` takes the thread-loop
@@ -918,6 +941,7 @@ fn run<W: Write>(
918941 }
919942 }
920943 if let Some ( capture) = capture. as_mut ( ) {
944+ last_advance = Instant :: now ( ) ;
921945 if let Err ( message) = capture. advance ( ) {
922946 let _ = emitter. emit ( & Event :: Error {
923947 code : "encode-failed" . to_owned ( ) ,
@@ -1239,19 +1263,8 @@ fn run<W: Write>(
12391263 emit_sample ( emitter, & cursor, content_rect ( & capture, size) , & mut pending_asset, None ) ;
12401264 last_emit = Instant :: now ( ) ;
12411265 }
1242- // The heartbeat that keeps the output at a constant frame rate
1243- // while the screen is static: no frame arrived, but the clock
1244- // moved, so the last picture is held forward.
1245- if let Some ( capture) = capture. as_mut ( ) {
1246- if let Err ( message) = capture. advance ( ) {
1247- let _ = emitter. emit ( & Event :: Error {
1248- code : "encode-failed" . to_owned ( ) ,
1249- message,
1250- } ) ;
1251- exit_code = 1 ;
1252- break ;
1253- }
1254- }
1266+ // No advance here: a timeout means a tick has passed, so the
1267+ // heartbeat at the top of the next pass holds the picture forward.
12551268 }
12561269
12571270 Err ( RecvTimeoutError :: Disconnected ) => break ,
@@ -1349,6 +1362,11 @@ fn finish_capture<W: Write>(
13491362/// only rectangle the file shows. Before then, and for a cursor-only session
13501363/// that opens no encoder at all, the whole negotiated stream, which is what the
13511364/// consumer of a cursor-only recording is compositing over.
1365+ /// Whether a tick has passed since the capture clock last advanced.
1366+ fn heartbeat_due ( last_advance : Instant , now : Instant , tick : Duration ) -> bool {
1367+ now. saturating_duration_since ( last_advance) >= tick
1368+ }
1369+
13521370fn content_rect ( capture : & Option < Capture > , size : Option < ( i32 , i32 ) > ) -> Option < shim:: CropRect > {
13531371 match capture {
13541372 Some ( capture) if capture. started ( ) => Some ( capture. content_rect ( ) ) ,
@@ -1563,6 +1581,28 @@ mod cursor_sample_tests {
15631581 assert ! ( buffer. is_empty( ) , "emitted {}" , String :: from_utf8_lossy( & buffer) ) ;
15641582 }
15651583
1584+ /// #936: messages arriving faster than the tick must not hold the heartbeat
1585+ /// off. The loop checks it on every pass, so what matters is that it comes
1586+ /// due once a tick has passed, however many passes happened in between.
1587+ #[ test]
1588+ fn the_heartbeat_comes_due_every_tick_whatever_else_arrives ( ) {
1589+ let tick = Duration :: from_millis ( 16 ) ;
1590+ let start = Instant :: now ( ) ;
1591+ let mut last_advance = start;
1592+ let mut advances = 0 ;
1593+ // A cursor message every 7 ms (144 Hz) for one second of loop passes.
1594+ for pass in 0 ..=( 1000 / 7 ) {
1595+ let now = start + Duration :: from_millis ( pass * 7 ) ;
1596+ if heartbeat_due ( last_advance, now, tick) {
1597+ last_advance = now;
1598+ advances += 1 ;
1599+ }
1600+ }
1601+ // Every 21 ms at worst (three 7 ms passes): about 47 a second, never zero.
1602+ assert ! ( advances >= 1000 / 21 , "{advances} advances in a second of cursor traffic" ) ;
1603+ assert ! ( !heartbeat_due( start, start + Duration :: from_millis( 15 ) , tick) ) ;
1604+ }
1605+
15661606 /// A cursor-only session opens no encoder, so it falls back to the whole
15671607 /// stream; once pixels are flowing the encoder's rectangle wins.
15681608 #[ test]
0 commit comments