Skip to content

OBS mode: the game waits for OBS to record before each clip, and a late-started take isn't counted captured (#1109) - #1185

Merged
ccoventry merged 7 commits into
devfrom
fix/obs-hold-until-recording-1109
Oct 30, 2026
Merged

ccoventry merged 7 commits into
devfrom
fix/obs-hold-until-recording-1109

Conversation

@ccoventry

Copy link
Copy Markdown
Owner

Release note: Fixed: In OBS mode the game now waits for OBS to start recording before each clip plays, and a take OBS started late is no longer counted as captured.

Closes #1109.

What changes

Prevent it (the user's idea on the issue). With the hook running the batch (Seek Between Clips) in OBS mode, Studio writes a ds_capture_batch wait_for_recording line at every clip's AUDIO_SYNC (native::patch::runtime_batch). There the hook (goldsrc-hooks/src/batch.rs, new WaitingForRecording phase beside #885's hold):

  1. pauses the demo and sends BATCH_WAITING_FOR_RECORDING on the events pipe;
  2. Studio starts OBS on the AUDIO_SYNC marker as before and waits for OBS_WEBSOCKET_OUTPUT_STARTED;
  3. Studio sends ds_capture_batch recording (capture_engine::RecordingGoAhead); the hook plays on.

The lead-in then plays its full second with OBS already recording, however slow the start.

The safety net (the issue body). Every Marker now carries when its reader read it (at), and ObsClient::start_record returns when OUTPUT_STARTED was read. At a clip's START_RECORD, ObsSession checks the two. If OBS started after the clip did, the engine sends EngineEvent::ObsStartedLate, and then:

  • take verification marks the block not captured and not renderable (started_late_ms, mark_started_late);
  • Render While Capturing doesn't render it;
  • the activity log, the toast and Last Batch say by how many ms ("OBS started 605 ms late: missing the clip's opening (not captured)").

This net covers a hook DLL without the wait, a patched batch, and a wait that timed out.

Decisions

  • Hold timeout 20 s, then play on rather than end the batch. OBS starts in 59-85 ms. Studio's own start_record gives up after about 16 s (750 ms + one retry with 15 s), and when it does, the batch already ends on ObsSession::refused/dead. So 20 s only trips when Studio's answer got lost. Playing on keeps the rest of the batch, and the safety net marks that one take. Ending the batch is kept for "Studio gone", as the issue comment says.
  • The wait line is an at line running a ds_capture_batch subcommand, not a new batch-file keyword. A new keyword would make an older hook DLL refuse the whole batch file ("unknown line"). An unknown subcommand only prints the usage.
  • The go-ahead is counted, not paired by clip. One is owed for every clip that has both the hook's wait and Studio's start. The hook's word can arrive before or after the AUDIO_SYNC marker. A go-ahead that is late after a timeout is harmless: entering the next wait clears it.
  • No margin on the late check. Both times are read stamps, each a few ms after the event. With the wait on, OBS starts a whole lead-in ahead, so only real lateness trips it.
  • A late take is a new status in Last Batch ("OBS started N ms late"), not cut_short. cut_short means the batch's end cut a clip (cut_by), so reusing it would have read "Cut short by Cancel".
  • Late starts are known only to the Studio that ran the batch. The next start's catch-up (A capture batch outlives DoD Studio closing, and Studio catches up when it reopens #545/Studio marks clips captured after a batch that ran while it was closed (part of #545) #760) checks takes on disk and can't tell a late start. The take is still on disk, so a catch-up after a crash would count it. That keeps this PR off the recovery code resume545 is changing.
  • No new console name. recording and wait_for_recording are subcommands of ds_capture_batch. Commands.txt's usage line and docs/dodstudio_commands.md say so, and console_commands_data.js is unchanged.

Test done

Unit tests:

  • hook: recording_wait (go / gone / timeout) and the wait line's text; cargo test -p goldsrc-hooks --target i686-pc-windows-msvc;
  • native: runtime_batch writes the line only in OBS mode at sys_sound; RecordingGoAhead counting, including a hook DLL that never asks; ObsSession reports a late start once and not an early one;
  • studio: mark_started_late;
  • JS: resultsOf shows late; takesFoundMissing names late takes.

Runs: workspace tests (minus hooks and web-analyzer), clippy on host and i686, rustfmt, vitest (127 files), and e2e app-boot, batch-results and finish-clips.

Some failures are not from this PR. obs::provision fake-OBS tests and one pipe_tail test failed only intermittently here, with connect timeouts (os error 10060) and a pipe-name clash under load from parallel agents. They passed in the final full workspace run after merging dev, and those files are untouched.

Live, Brain account, game lock held, own Studio (Vite 5192, CDP 9232, settings in a scratch folder), OBS 32 on this PC, 120 fps, 1280x720, demo wsod25_grp5_h1_m00cat.dem (m00cat's POV):

install batch clips result
POST with the wait 2, 2, 2, 4 all captured; the game waited 68-112 ms a clip (hook log 74-112 ms)
POST without the wait (test-only env switch, same 4 clips) 4 all captured; for the A/V comparison
POST wait on, OBS start held back 1.5 s (test-only delay) 2 game waited 1573 / 1584 ms; both takes whole and captured
POST wait off, OBS start held back 1.5 s 2 0 of 2 captured: "OBS started 605 ms late" / "589 ms late" in Last Batch and the toast
POST wait on, OBS start held back 25 s 1 hook: "no word from Studio ... after 20 s: playing on"; take marked 4066 ms late
PRE with the wait 4 all captured; the game waited 79-87 ms a clip
PRE without the wait 4 all captured; for the A/V comparison

The checks the issue asked for:

  • Demo clock: the demo player's clock is the same at the pause and the resume (e.g. 783.644 → 783.644). Takes with and without the wait are the same length (20.358 s / 20.358 s), so the pause skips and replays nothing.
  • Still frame at the head: none in any take (frame-to-frame differences from frame 0). OBS's first frame comes with the go-ahead, so the file starts on the moving lead-in, about 1 s before the clip's first frame. A contact sheet of the first 200 ms is in local\review-images\1109\head_b0.png in the main checkout.
  • Sound: I aligned each clip's takes with and without the wait by picture and by sound, separately.
    • The picture-to-sound difference between them was -15 to -12 ms on POST and +8 to +19 ms on PRE.
    • Two runs that both had the wait differed by up to ±14 ms.
    • So the pause shifts neither against the other beyond run-to-run noise (about a frame and a half at 120 fps either way).
  • Cost per clip: about 0.07-0.11 s, as the issue expected.

Not tested live: Studio closing mid-wait (the "Studio gone" ending). It is the #885 space hold's studio_gone with the same 10 s, unit-tested in recording_wait.

Found on the way

#1169: in OBS mode a clip chained to the one before it has no AUDIO_SYNC, so OBS never records it, and later clips land in the wrong take folders. Read from the code; it is out of scope here.

🤖 Generated with Claude Code

ccoventry and others added 4 commits October 30, 2026 01:23
…late start isn't captured (#1109)

The hook's batch pauses the demo at every AUDIO_SYNC (a `ds_capture_batch
wait_for_recording` line Studio writes in OBS mode) and tells Studio; Studio
starts OBS on that marker as before and sends `ds_capture_batch recording`
once OUTPUT_STARTED is in. No go-ahead in 20 s: the clip plays on; Studio
gone for 10 s: the batch ends as the space hold does (#885).

Safety net: OUTPUT_STARTED's arrival is held against the clip's
START_RECORD marker's, both stamped when read. A take OBS started after the
clip did is not captured, Render While Capturing skips it, and Last Batch
says by how many ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Studio hears the wait only over the events pipe; without it the lead-in
plays on as before and the late-start check stays the net.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ccoventry ccoventry added bug Something isn't working area:capture Capture pipeline: scan, patch, batch, OBS labels Oct 30, 2026
ccoventry and others added 2 commits October 30, 2026 03:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The wait line follows the clip's AUDIO_SYNC echo, the marker OBS starts
  on, rather than the sys_sound beside it, so a clip chained to the one
  before (no sound flush, #1169) waits for its recording too once it has
  its own AUDIO_SYNC.
- ds_capture_batch stop puts the demo's pause back, so a batch stopped
  while it waited for OBS (or held, #861) never leaves the demo paused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ccoventry

Copy link
Copy Markdown
Owner Author

Review (review-night)

Verdict: ready to merge, with the dev merge and the two fixes in 7fd2f42 (pushed).

Merged dev (9764f04)

dev merged in with no conflicts, including #1167's split of capture_manager and #1171's recovery. After the merge:

  • cargo test --workspace (minus hooks and web-analyzer) and the hook tests: all pass.
  • Clippy on host and i686, and fmt: clean.

Fixes pushed (7fd2f42)

  1. The wait goes with the AUDIO_SYNC echo, not sys_sound. runtime_batch wrote the wait line after sys_sound. That is the same tick as AUDIO_SYNC for a normal clip, but OBS mode never records a clip chained to the one before it, and later clips land in the wrong take folders #1169's chained clip gets an AUDIO_SYNC with no sound flush (OBS mode: a highlight right after another is recorded too, and later clips go to the right takes (#1169) #1186), and it needs the wait too. The line now follows the marker OBS actually starts on (is_audio_sync_echo). The test uses the builder's real quoted echo and adds a chained AUDIO_SYNC with no sys_sound. The hook sees the same lines in the same frame as before.
  2. ds_capture_batch stop unpauses. A stop during WaitingForRecording (or Render and clean up clips while a capture batch is still running, so a long batch never fills the drive #861's Held) took the batch away and left the demo paused. Studio's own Cancel follows stop with disconnect, so it was hidden there; a stop typed in the console wasn't. stop now resets the pause, as it already resets the time scale.

Checked: can the wait hang or leave the demo paused?

Checked: the late-start timestamps

  • Both stamps come from one clock. They are Instants in one process: the marker's at when its reader parsed it, and OBS's when the client read OUTPUT_STARTED.
  • With the wait on, START_RECORD comes at least sound_lead (1 s of demo time) after the go-ahead, and the go-ahead is sent only after OUTPUT_STARTED was read. Pipe and websocket latency (ms) can't turn that around.
  • Without the wait (old hook, patched batch), a good start is ~60-85 ms against a 1 s lead.
  • The one pessimistic stamp is the is_recording() fallback, which stamps when the status answer arrived. It only runs after 750 ms with no event, and with the wait on it is still well before the clip.
  • A slow marker reader (the qconsole tail) makes START_RECORD read later, which can only hide lateness, never invent it.
  • So no margin is needed.

Checked: Render While Capturing and #1171

  • Render While Capturing: clip_started_late is pushed at the clip's START_RECORD, before the take settles. The take folder matches (take_folder_of of the OBS destination is the manifest's block folder), so it isn't rendered.
  • Capture: after Studio closes or crashes mid-batch, it offers to capture the clips the game never got to (#20, part of #545) #1171:
    • Late takes are known only to the Studio that ran the batch, as the PR says.
    • The catch-up's unfinished-take check reads only HLAE's sound.wav, which OBS takes don't have, so it doesn't interact.
    • started_late_ms: None is set in verify_capture_takes_settling, which the catch-up uses. It compiles and passes after the merge.

Live

An OBS batch on POST with a chained pair, run on this branch plus #1186, goes in a results comment on #1186 (the game was busy with another test when this was posted).

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:capture Capture pipeline: scan, patch, batch, OBS bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An OBS clip whose recording started after the clip's first frame still counts as captured

1 participant