Skip to content

fix(capture): serialize muxer emits under stdin backpressure#816

Closed
cursor[bot] wants to merge 1 commit into
masterfrom
cursor/critical-bug-investigation-4da9
Closed

fix(capture): serialize muxer emits under stdin backpressure#816
cursor[bot] wants to merge 1 commit into
masterfrom
cursor/critical-bug-investigation-4da9

Conversation

@cursor

@cursor cursor Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Bug and impact

Screenshot capture mode can produce corrupted video when ffmpeg's stdin applies backpressure.

PR #812 made createFrameMuxer backpressure-aware: emit() now returns a promise when stdin.write() signals drain is needed. Screencast mode correctly defers CDP frame acks by returning that promise from onFrame, but screenshot polling never awaits muxer.write().

Concrete trigger:

  1. Capture with @browserless/capture/screenshot at a frame rate ffmpeg cannot keep up with (slow encoder, large resolution).
  2. muxer.write() returns a pending drain promise after emitting frame N.
  3. The polling loop immediately captures frame N+1 and calls muxer.write() again, or flush() runs in finally while the emit is still in flight.
  4. Two emit() calls interleave their stdin.write() calls → cluster headers and JPEG payloads splice together → corrupted Matroska stream → broken/unplayable output or ffmpeg failure.

Root cause

Backpressure serialization was added for screencast CDP ack pacing but not enforced inside the muxer. Screenshot (and flush() racing an in-flight write) could start a new emit before the previous one finished draining.

Fix

Serialize subsequent emits only while one is awaiting backpressure (pendingAsync queue). When stdin accepts writes synchronously, write() still returns undefined, preserving the screencast sync-ack fast path from #814.

Validation

  • Added unit test: concurrent muxer.write() calls under backpressure stay ordered (2 writes before drain, not 4).
  • pnpm --filter @browserless/capture test — 43/43 pass
  • pnpm --filter @browserless/screencast test — 10/10 pass
Open in Web View Automation 

Screenshot polling does not await muxer.write() promises. After #812
introduced backpressure-aware emits, a second write or flush() could start
while the previous emit was still waiting on drain, interleaving cluster
headers and payloads and corrupting the Matroska stream fed to ffmpeg.

Queue subsequent emits only while one is awaiting backpressure so the
screencast sync-ack fast path is preserved when writes complete
synchronously.
@coveralls

Copy link
Copy Markdown

Coverage Status

Coverage is 84.961%cursor/critical-bug-investigation-4da9 into master. No base build found for master.

@Kikobeats Kikobeats closed this Jun 29, 2026
@Kikobeats Kikobeats deleted the cursor/critical-bug-investigation-4da9 branch June 30, 2026 08:44
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.

3 participants