Closed bug report

Native recorder: a 4K take longer than about a minute never finalises (stop times out, take dropped, 0-byte mp4); 12 s takes are fine

John Lauer · 2mo ago ·closed by John Lauer

ConfRoomROG (4K primary monitor, NVIDIA, NVENC "H.264 Encoder MFT", gpuZeroCopy true), today, three takes plus one control:

  • 16:43:58 control: desktop_record_start {monitor:"primary", fps:30, audio:true} from the CLI, 12 s, desktop_record_stop in 117 ms: 25,189,024 bytes, 341 frames. Fine.
  • 16:39:08 and 16:49:52: two takes of about 190 s started by the KiCad bridge over the loopback direct API around its demo tour; desktop_record_stop did not answer within 60 s; afterwards desktop_record_status lists no take and the file is 0 bytes.
  • 16:54:40 isolation, no bridge involved in the recorder: desktop_record_start from the CLI, 82 s later desktop_record_status shows the take healthy (844 frames, 3,923,450 audio frames, 0 dropped, running), then desktop_record_stop from the CLI answers "Request timed out"; 20 s later the take is gone from status and desktop-m0-20260911-165440.mp4 is 0 bytes.

So a take past roughly a minute at 3840x2160 never finalises: the stop hangs past the request timeout, the recorder is dropped, and the file keeps only its header. The 12 s take finalises. Earlier tours (0.9.34x era) recorded fine on arav-rog at a smaller resolution.

Asks:

  • Make stop bounded: finalise the moov within the request budget (or answer stillRunning:true with a finalising state and let desktop_record_status report the file when it lands), never drop the take.
  • Report a captureError or finaliseError on the take instead of a 0-byte file.
  • If this is an NVENC-at-4K bitrate or buffer limit, say so in desktop_record_start's reply (bitrate, maxTakeSec) so a caller can pick a lower bitrate or gpu:false up front.

Reproduce: any box with a 4K primary: start, wait 90 s, stop.

3 Replies

John Lauer · 1mo ago

Taking this. Reproduced on ConfRoomROG this morning and measured before touching code:

  • 30 s take, 4K, NVENC (NVIDIA H.264 Encoder MFT, hardwareEncoder true, gpu path, 24 Mbps): stop hung past the 30 s budget; the file was 0 bytes and the take was gone from status. An 18 s take (502 frames, 27 fps captured, 0 pool drops) hung the same way and its file (49.9 MB) landed ~40 s after the stop. Your three takes from yesterday did land eventually: 49.5 MB / 48.8 MB / 54.1 MB, written 1 to 2 hours after their stops, each holding only the ~800 frames captured.
  • So the encoder queue is not the backlog (the DXGI pool is capped at 12 and never emptied). The MPEG-4 sink holds every encoded sample until SinkWriter.Finalize (the file stays 0 bytes for the whole take) and its Finalize grows far worse than linearly with the sample count at 4K: 341 frames 117 ms, 502 frames ~40 s, ~800 frames hours. Meanwhile desktop_record_stop joined the encoder thread synchronously after removing the take from the registry, which is why the stop timed out and the take looked dropped.

2.1.104 (building now):

  1. Fragmented MP4 output (MFTranscodeContainerType_FMPEG4, 2 s fragments): the file grows on disk while recording, a crash leaves a playable file, Finalize only closes the tail fragment.
  2. desktop_record_stop is bounded: it waits up to 15 s for Finalize, then answers {state:"finalising", stillRunning:true, frames, bytes} and keeps the take registered; desktop_record_status shows state recording -> finalising -> finalised (or failed) with bytes, finaliseMs, finaliseError, and a second desktop_record_stop collects it. A take is never dropped.
  3. Backpressure + honesty: the sink writer's own counters ride desktop_record_status (encodedFrames, encodeBacklog, sinkOutstanding, encodeFps); when the encoder falls more than 2 s behind, ticks are skipped and counted in droppedFrames instead of queued; the log states the writer counters before Finalize and how long Finalize took.

bitrate is already in the start reply; maxTakeSec is not a real limit once Finalize is bounded, so it is not added. Proof will be a 90 s+ 4K take on ConfRoomROG with the file growing during the take and a stop that answers within the budget.

John Lauer · 1mo ago

2.1.104 (insiders) update — partial, keeping this open.

Proof run on ConfRoomROG (4K, NVENC, gpu path), 90 s take:

  • The file GROWS on disk while recording now (34 MB at +10 s -> 248 MB at +80 s), never 0 bytes. Fragmented MP4 works.
  • desktop_record_stop answered in 18 s with {state:"finalising", stillRunning:true, frames:2674, bytes:248223116} and kept the take registered — no "Request timed out", no dropped take. The take is recoverable.
  • desktop_record_status carries the live counters: encodeBacklog stayed 0, sinkOutstanding 2, droppedFrames 0 throughout.

But it is NOT fully fixed: capture froze at ~80 s (frame 2674). frames and encodedFrames both stopped at 2670/2674, encodeFps decayed 27 -> 22 -> frozen, and the encoder thread never reached Finalize — the log shows stop … encoder still finalising after 15 s (2674 frames, 248223116 bytes on disk, stats (2670,2670,2670,2,250874219)). So NVENC's WriteSample wedged around frame ~2670 at 3840x2160, with the encoder fully caught up (received==encoded==processed) — this is a stall in the MFT/capture pipeline, not encoder backpressure.

So 2.1.104 turns the old failure (0-byte file, stop hangs past the budget, take dropped) into a safe, diagnosable one (file on disk, bounded stop, honest counters), but a long 4K take still doesn't finalise. Caveat on this run: ConfRoomROG is in active use (RustDesk), so a display/session change around the 80 s mark could have reconfigured the WGC frame pool. Next: reproduce on an idle 4K box to separate a genuine pipeline wedge from an environmental frame-pool reset, then fix the capture side (recreate the frame pool on a device/size change; make WriteSample non-blocking or time-boxed so the thread can always reach Finalize). Not closing until a 3+ minute idle 4K take finalises.

John Lauer · 1mo ago

Fixed in 2.1.107 (insiders); the recorder freeze is resolved.

Root cause (measured, not guessed). A long screen+audio take froze at ~105 s on every box. The audio:false control recorded 130 s clean while audio:true froze at the SAME wall-clock point on both a 4K box (crr) and a 1440p box (arav-rog) — so it was the A/V interleave, not resolution, not the encoder, not a frame-count/buffer limit (droppedFrames stayed 0 through every hang). Video was stamped on a nominal-fps clock (frame_index * 1/fps) while WGC delivers ~27 of 30 fps and the audio track is padded to wall clock; after ~100 s the video timeline was ~10 s ahead of audio and the fragmented-MP4 muxer hard-blocked video WriteSample waiting for audio to reach that timestamp, wedging the encoder loop.

Fix. Stamp each video sample with the same wall clock the audio uses (started.elapsed()), duration = gap since the last written frame.

Proof on 2.1.107, both idle test boxes:

  • arav-rog (2560x1440): 150 s take, frames 373 -> 5661, steady 27.9 fps, 0 dropped, stop finalised in 3 s (finaliseMs:5), 264 MB.
  • ConfRoomROG (3840x2160): 150 s take, frames 372 -> 5656, steady 27.8 fps, 0 dropped, stop finalised in 3 s (finaliseMs:17), 531 MB.

Also delivered here: 2.1.104's fragmented-MP4 output (file grows on disk while recording) + bounded desktop_record_stop (answers state:"finalising" within the budget, never drops the take), and the new desktop_record_encoders verb (lists h264/h265 encoders + hardware availability). HEVC recording is split to #190 (h264 is the default and fully works; codec:h265 returns a clean hevc_unsupported until #190 lands). Fleet on 2.1.107: arav-rog, ConfRoomROG, AdomLapper (winvm will self-update when it reconnects to the relay).

John Lauer closed this · 1mo ago

Log in to reply.