Skip to content

OBS clips are cut to the highlight when finished, without the lead-in and tail OBS records (#84) - #1068

Closed
ccoventry wants to merge 8 commits into
devfrom
feat/obs-exact-trim-84
Closed

ccoventry wants to merge 8 commits into
devfrom
feat/obs-exact-trim-84

Conversation

@ccoventry

Copy link
Copy Markdown
Owner

Release note: Fixed: OBS clips are now cut to the highlight when they are finished, without the second of lead-in and the tail OBS recorded around it.

Closes #84

What changes

  • Capture: each OBS take now records where its clip starts and ends in the recording. ObsSession notes when StartRecord reported the recording started, and each START_RECORD / STOP_RECORD marker is stamped as it arrives (on the reader thread, so the capture loop's 16 ms sleep doesn't add to it). A marker's place in the file is its arrival time since the start, less a measured 16 ms (plus 30 ms for a marker from qconsole.log). native/src/obs/trim.rs.
  • Take file: saved as obs_trim (start_seconds, end_seconds, fps) in the take's dodstudio_take.json. Saved format: an optional new field, no format bump. Old takes (and a take whose START_RECORD was missed) have no trim and finish untrimmed exactly as before; older builds still read the file and ignore the field. The capture-settings write after the batch keeps the trim the batch recorded.
  • The file is now written whole (written beside it and renamed over it), and its writers are serialised. In a live run the first finish job read the file mid-write and copied its take untrimmed; a test now reads it 200+ times while another thread rewrites it.
  • Finish / render: every codec cuts an OBS take with -ss <start - half a frame> -t <length> as input options (FFmpeg's accurate seek; the half frame keeps the frame nearest the start). Progress for a cut OBS take comes from the time written against its length.
  • Keep as captured (source_copy, the OBS default) on a take with a trim is a short re-encode, no longer a copy. The dropdown now says "Keep as captured, cut to the clip (seconds)".

Why a re-encode for Keep as captured

A stream copy can only cut on a keyframe. OBS's Simple output puts one every 250 frames (4.2 s at 60 fps, measured with ffprobe), and obs-websocket can't change that in Simple mode. With a ~0.9 s head, a copy would keep the whole head. Splicing a re-encoded first GOP onto copied packets needs matching parameter sets in one MP4, which two encoders don't give, so I ruled it out as fragile. So the cut re-encodes at OBS's own quality: h264_nvenc CQP 16 (what Studio's profile records at), or libx264 CRF 14 when FFmpeg can't open NVENC. That is probed once per FFmpeg path with a one-frame encode, which has kill_on_drop and a 15 s timeout. Audio is AAC 320 kb/s. Measured on a 1440p clip: PSNR 43 dB against the original. A whole 3-clip batch (64 s of footage) finished in 17-24 s.

Why not OBS's own frame count

I built it first on GetRecordStatus's outputDuration (frames × frame time, sampled in bursts to place OBS's frame clock to ~1 ms). It failed live. That count is what the muxer has written, and the muxer holds video until it has audio to interleave. The hold is OBS's audio buffering, which OBS raises and never lowers. In one OBS session the count ran 0.37 s behind the file in one batch. Then OBS logged "adding 661 milliseconds of audio buffering", and in the next batch it had not counted a single frame 1 s into the recording. The time since OUTPUT_STARTED doesn't move with that (measured at 298 ms and 960 ms of buffering).

Test done

All on POST, Brain account through game_lock, OBS 33.0.0 at 60 fps (the user updated it from 32.2.2 that evening, so the audit's numbers were on 32.2.2), Studio built from this branch with its own settings copy. The 3 clips were m00cat's WSOD25 3k/4k/3k from the audit (wsod25_grp5_h1_m00cat ×2, wsod25_grp2_h2_m00cat), finished with Keep as captured.

  • HLAE direct-to-video 300 fps run of the same 3 clips as the reference (4 min 55 s, 103 GB).
  • 7 OBS batches while developing. The final build's batch: all 3 jobs cut ("cutting the OBS take to its clip" in the log), done in 24 s.
  • Accuracy, each finished clip against the HLAE take, matching every moving frame near each end to the closest HLAE frame (300 fps, so ~3 ms resolution):
Head left (3 clips) End offset (3 clips) Length vs HLAE
Untrimmed OBS (before) 870-897 ms 60-67 ms of tail +0.94-0.96 s
This PR (final build) +3, 0, 0 ms 0, -3, +3 ms within 1 frame

Across all 25 measurements (5 batches, both ends), the delay that would have been exact ranged from 6 to 30 ms (mean 16). With 16 ms, every cut lands within 14 ms of HLAE's frame, under one frame at 60 fps. Stills of each clip's first frame, OBS cut against HLAE: local\review-images\obs-84\first_frame_*_obs_vs_hlae.png. They show the same view; only the random shell casings differ.

  • cargo test --workspace --locked, clippy 1.98.1 --workspace --all-targets -D warnings, fmt check, npm run test:unit (923), e2e app-boot + finish-clips: all pass after merging dev.

Decisions

  • Re-encode for Keep as captured rather than leave it untrimmed. The audit's plan had kept Source copy untrimmed, but it is the default, so most OBS clips would never have been cut.
  • Time since OUTPUT_STARTED, not the frame count, for the reasons above. The 16 ms constant is measured on one PC, one game build (POST) and one encoder (NVENC). On another PC an error would show as a few ms of head or a frame short at the end, never the 0.9 s head.
  • Not done: PRE and 120 fps were not measured (120 drops frames here, OBS mode's 120 fps default is past what the encoder holds at 1440p #1060). Render tab's per-job "Skip (Keep Original)" label is unchanged, though on a trimmed OBS take it now does the same cut.

🤖 Generated with Claude Code

ccoventry and others added 7 commits October 26, 2026 22:29
…when finishing (#84)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…count (#84)

The muxer's frame count lags the file by OBS's audio buffering, which grew
from 0.37 s to over 1 s between two batches of one OBS session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ting with the batch's last write still reads the OBS trim (#84)

The first job of a finished batch read the file mid-write and copied its take untrimmed.
Start delay 40 ms, from four batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kwards (#84)

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

Copy link
Copy Markdown
Owner Author

Closed at the user's call: OBS's ~0.9 s lead-in and ~60 ms tail are fine to leave in (users trim in their editor anyway), and Keep as captured stays an exact copy. Branch kept in case this comes back; if reopened, #1066's OBS Quality needs the trim encode at CQP 12 in the take's own pixel format so Max stays 4:4:4.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Render Studio] Frame-exact trim for OBS takes needs capture-side pre-roll/post-roll metadata

1 participant