Skip to content

chore(release): cherry-pick RC-window bugfixes for 1.7.0 rc.3#124

Merged
EtienneLescot merged 8 commits into
release/v1.7.0from
cherry-pick/rc3-fixes
Jul 19, 2026
Merged

chore(release): cherry-pick RC-window bugfixes for 1.7.0 rc.3#124
EtienneLescot merged 8 commits into
release/v1.7.0from
cherry-pick/rc3-fixes

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Cherry-picks the 8 bugfix commits merged to main during the 1.7.0 RC-testing window, per the release-branch-freeze contract in .harness/docs/git-workflow.md § Release branches — these are cherry-picks of already-merged main fixes, not new work.

Fixes bundled:

Verification

Cherry-picked cleanly onto release/v1.7.0 (no conflicts). On the resulting tree:

Once merged, this branch tip is ready for an rc.3 cut (Actions → "Cut a release candidate", or the manual tag fallback) for a final round of testing before promotion to stable.

EtienneLescot and others added 8 commits July 19, 2026 11:35
… span

Auto zoom regions (created by the magic-wand cursor-dwell suggestion pass)
were built with focusMode left undefined unless the separate, global
"Auto Focus All" toggle was on. That meant the pan/zoom used the static
dwell-centroid focus captured at suggestion time and never tracked the
cursor afterward, even though the cursor-follow interpolation logic
(zoomRegionUtils/cursorFollowUtils) already existed and just wasn't wired
up by default for these regions.

Auto-suggested regions now always get focusMode "auto" so they pan to
track the cursor telemetry across their whole span, independent of the
"Auto Focus All" toggle (which still only controls the default for
manually-drawn zoom regions).

Fixes #72

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The video-writer thread held the shared frame-state mutex across
IMFSinkWriter::WriteSample, which the main thread's stop-wait also
locks to check control.stopRequested. With the software H.264 encoder
fallback (preferSoftwareEncoder: true), WriteSample is slow enough
that the writer thread could keep re-acquiring the lock every frame
faster than the main thread could win it after a stop request,
starving the stop-wait indefinitely — explaining why no [stop-timing]
line ever printed.

Split MFEncoder::writeFrame/writeBgraFrame into a capture step (GPU
readback + IMFSample construction, still under the shared mutex since
it touches latestFrameTexture) and a submit step (WriteSample, only
under the encoder's own low-contention writerMutex_), and call submit
outside the shared mutex in main.cpp's writeVideoFrames.

Fixes #115.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes #60: recording a single window (as opposed to a full display) on
Windows produced a solid black video for the entire clip.

Root cause: GraphicsCaptureItem::Size() for a window capture item reports the
window's real client-area pixel dimensions verbatim, which are frequently odd
(arbitrary resize, DPI rounding) — unlike monitor/display capture items,
which are always even. H.264 encoding (and the RGB32->NV12 conversion
feeding it) requires even width/height. The frame pool and captureWidth()/
captureHeight() (which configures the encoder's input media type) both used
the raw, possibly-odd item size, so an odd-dimensioned window capture fed the
Media Foundation H.264 encoder a size it can't encode correctly, producing
black output.

Fix: round window capture dimensions up to the nearest even value once, in
WgcSession::createCaptureItem(HWND), and use that same rounded size (not the
raw item size) for both the Direct3D11CaptureFramePool buffer and
captureWidth()/captureHeight(), so every consumer of the session agrees on
one even-dimensioned size. Monitor/display capture is untouched (always even
in practice).

Verified: built wgc-capture.exe via CMake/Ninja (VS 18 Insiders x64), opened
a real Notepad window resized to an odd 789x595 client area, and drove the
helper directly against it (sourceType:"window", real HWND).
- Original (pre-fix) binary: 5.4KB output, ffmpeg blackdetect flags the
  entire clip as black (black_duration ~= full clip length).
- Fixed binary: 36.5KB output (consistent with real, compressible frame
  content vs. a solid-color clip), ffmpeg blackdetect reports zero black
  segments across the same clip.
- Sanity-checked normal display/monitor capture is unaffected (unchanged
  code path).
…ks playback

The timeline playhead read its position from `currentTime` React state, set
from VideoEditor (a ~3300-line component) on every rAF tick of video playback
and on every `seeking` event while dragging. Each tick forced a full re-render
of VideoEditor + VideoPlayback + TimelineEditor before the playhead's DOM
moved, so the visual position lagged behind actual video playback and felt
sluggish while scrubbing during playback.

- PlaybackCursor now reads the video element's currentTime directly via a
  `getPlaybackTimeMs` getter, in its own requestAnimationFrame loop and its
  own local `useState`. Only this small component re-renders per frame,
  independent of how long the ancestor tree's render takes.
- Coalesce the `onTimeUpdate` React state commit (still used by everything
  else that reads `currentTime`) to at most once per animation frame via a
  new `createRafCoalescer` helper, so a burst of `seeking` events from a fast
  timeline drag doesn't force one state commit per event.

Fixes #111
Per CodeRabbit review on #120: PlaybackCursor wrote to
getPlaybackTimeMsRef.current directly in the render body instead of in an
effect. Writing to a ref during render is a React anti-pattern — it can leave
the ref holding a stale value from a discarded render pass under concurrent
rendering. Moved the assignment into a useEffect keyed on getPlaybackTimeMs,
matching the recommended pattern for "latest callback" refs.

Verified: tsc --noEmit clean, biome clean, rafCoalescer.test.ts and
zoomRegionUtils.test.ts pass (7/7).
Follow-up to #119 (Fixes #115). CodeRabbit flagged, on the now-superseded
#121, that the video-writer thread's per-frame cv.wait() has no timeout: if a
notification were ever missed, the thread would block indefinitely, hanging
videoWriterThread.join() and the whole stop sequence — exactly the failure
mode this release is eliminating. #119 already fixed the actual root cause
(the blocking WriteSample call no longer runs under this mutex, so the main
thread's stop-wait is never starved), but the wait itself was still
unbounded. Switch it to wait_for(100ms) so the loop always re-checks
stopRequested/encodeFailed even in that circumstance, instead of relying
solely on a notification reaching an already-waiting thread.

Verified: rebuilt wgc-capture.exe on top of current main (includes #119),
re-ran the #115 repro (screen-only, all extras disabled) and the
full-featured regression (system audio + cursor) - both stop in under 110ms
with a full [stop-timing] log and a valid MP4.
Native macOS recordings write system audio and the microphone as two
separate AAC tracks in the screen recording, and both are flagged
`default`. The exporter decoded audio through web-demuxer's bare "audio"
selector, which resolves to the single stream FFmpeg's
`av_find_best_stream` picks — the first (system-audio) track. When
nothing was playing, that track is silent, so the exported video had no
audible audio even though the mic was recorded fine. The browser
recorder already blends system + mic into one track; the native path
never did.

Decode every audio stream and mix them into one timeline before
encoding, mirroring the browser recorder:

- `mixPlanarSources` (pure, unit-tested) sums each decoded source,
  downmixed to the target channels and aligned at its source-time
  offset, clamped to [-1, 1].
- Per-stream decode via `readAVPacket(streamIndex)` targets each track
  by container index instead of the best-stream heuristic.
- The trim-only and offline (speed) export paths both mix multi-track
  sources; multi-track speed projects are routed to the offline path
  because the real-time <audio> capture can only play one track.
- The source-copy fast path is disabled for multi-track sources, since
  copying verbatim carries both tracks over and players fall back to the
  silent first one.

Single-track recordings keep the original fast path unchanged. Adds a
real-browser regression test (fixture: silent track + 440 Hz tone) that
asserts the exported audio carries the tone through both mixing paths.

Closes #108

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Preserve the signed startFrame so AAC preroll (negative timestamp) no
  longer collides with the timestamp-zero frame; mixPlanarSources already
  discards pre-zero frames.
- Keep the sole decodable stream instead of returning null, so a partial
  decode failure can't fall back to the demuxer's best stream and export
  silence.
- Log per-track sample rates when a cross-rate mix is skipped.
- Document the hard-clip trade-off in mixPlanarSources.
- Tests: pin the source-copy audioStreamCount>1 blocker, cover the signed
  startFrame case, and tighten the browser RMS thresholds (0.01 -> 0.05).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 219460c5-91de-4de0-816c-b07658336bb5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cherry-pick/rc3-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@EtienneLescot
EtienneLescot merged commit fcea2b1 into release/v1.7.0 Jul 19, 2026
2 checks passed
@EtienneLescot
EtienneLescot deleted the cherry-pick/rc3-fixes branch July 19, 2026 09:42
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.

2 participants