Skip to content

fix(recorder): attach the mic from the first configure - the cameraReady gate flashed every cold open - #150

Merged
morepriyam merged 1 commit into
mainfrom
fix/recorder-open-flash
Aug 8, 2026
Merged

fix(recorder): attach the mic from the first configure - the cameraReady gate flashed every cold open#150
morepriyam merged 1 commit into
mainfrom
fix/recorder-open-flash

Conversation

@morepriyam

Copy link
Copy Markdown
Collaborator

Problem

Every cold open of the recorder flashed a beat after the first preview frame — the session came up, rendered, then visibly renegotiated. Root cause (full analysis in #149): micWanted was gated on cameraReady, so the session always configured video-only first, then the onStarted -> enableAudio flip rebuilt the whole video output on the live session — a second full configure ~25ms after previewStarted:

+115ms  sessionConfigSelected        (video-only session)
+303ms  previewStarted               (first frame visible)
+304ms  started -> cameraReady=true
+319ms  videoOutput rebuilt          (enableAudio: true)
+329ms  sessionConfigSelected AGAIN  <- the visible flash

The same gate also made the H.264 pin (#143) race the post-start reconfigure (first setOutputSettings attempt always rejected, riding on the retry), and camera flips paid two extra output rebuilds since flipping resets cameraReady.

Change

One line plus documentation — drop the cameraReady term:

const micWanted = !muted && !callActive;

The gate's stated purpose ("call detection lands before the mic is requested") was already satisfied without it: useCallState reads CallDetector.isCallActive() synchronously in its state initializer, so the render that first configures the session already knows the call state. The '!pri' -11800 recovery path (reportMicPriorityError) remains the backstop for the rare cold open that races an in-progress call the snapshot missed. Mute and in-call gating are unchanged, as is the mid-recording freeze (frozenMicRef).

Verification (on-device, iPhone)

Instrumented the full session lifecycle (onSessionConfigSelected / onConfigured / onPreviewStarted / onStarted + output-identity and codec-pin tracing), reproduced the double-configure, then re-ran with the fix across multiple cold opens:

  • exactly one sessionConfigSelected/configured pair per open; nothing reconfigures after the first frame — flash gone
  • videoOutput is created audio-ful from the first render (one instance, no rebuild)
  • H.264 pin resolves on its first attempt (~2ms; previously always rejected once with "VideoOutput is not yet connected")
  • recorded clips probed from disk: h264 / aac, 1920x1080 @ 29.97 — the pipeline contract holds
  • preview-close audio handoff (fix(recorder): sequence the preview-close audio handoff; stop false mic latch #136) still sequences correctly; post-preview clips carry a live audio track

Instrumentation was removed after verification; the shipped diff is the one-line gate change with an explanatory comment.

tsc --noEmit clean, eslint clean, full jest suite green (122 tests).

Fixes #149

…ady gate flashed every cold open

micWanted was gated on cameraReady so a cold open configured the session
video-only, then rebuilt the whole video output audio-ful the moment
onStarted fired - a second full session reconfigure ~25ms after the first
preview frame, visible as a flash on every open (and two extra output
rebuilds on every camera flip, since flipping resets cameraReady).

Confirmed via a temporary session-lifecycle trace on-device (iPhone, iOS):
the flash timeline was previewStarted -> started -> videoOutput rebuilt
(enableAudio:true) -> second sessionConfigSelected. With the gate removed,
a cold open shows exactly one configure, the H.264 pin resolves on its
first attempt (~2ms - no longer racing the reconfigure), and recorded
clips probe as h264/aac 1920x1080@30.

The gate's stated purpose - call detection landing before the mic is
requested - was already satisfied without it: useCallState reads
CallDetector.isCallActive() synchronously in its state initializer, so the
render that first configures the session already knows the call state. The
'!pri' -11800 recovery path (reportMicPriorityError) remains the backstop
for a cold open racing an in-progress call the snapshot missed.
Copilot AI lite review requested due to automatic review settings August 8, 2026 21:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes the cameraReady gate from the recorder’s mic enablement decision so the capture session can be configured with audio from the very first configure, avoiding a post-preview reconfigure that causes a visible “flash” on cold open.

Changes:

  • Update micWanted to depend only on muted and callActive, no longer on cameraReady.
  • Add an in-code rationale explaining why gating on cameraReady was unnecessary and how useCallState’s synchronous initializer supports the change.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@morepriyam
morepriyam merged commit 82e3b95 into main Aug 8, 2026
1 check passed
morepriyam added a commit to jlocala1/pulse that referenced this pull request Aug 9, 2026
…264 per connection epoch

Two regressions from one root: mieweb#150 removed the accidental session restart
a camera flip used to trigger (the flip's cameraReady reset flipped
micWanted, rebuilding the video output and restarting the session), and
two behaviors were riding it.

1. On iOS, flipCamera()'s setCameraReady(false) never re-armed: onStarted
   maps to AVCaptureSession.didStartRunningNotification, and an iOS device
   swap runs inside beginConfiguration/commitConfiguration on a RUNNING
   session - the notification never fires (start() even early-returns on
   isRunning). Record gestures (enabled: cameraReady && ...) died
   permanently; flipping back couldn't recover. The reset exists solely
   for CameraX (mieweb#133 torch-mid-rebind crash) and CameraX re-fires
   onStarted per bind, so it is now Android-only - mirroring the existing
   platform scoping of the zoom/torch prop gates (ba7647b).

2. The H.264 pin is applied per-connection natively
   (output.setOutputSettings(settings, for: connection)), and a flip forms
   a new connection - the pin died with the old one and the effect, keyed
   on output identity, never re-applied: post-flip clips silently recorded
   HEVC. The pin now re-keys on a connection epoch bumped from
   <Camera onConfigured> (fires after connections are formed on every
   reconfigure: cold open, enableAudio rebuild, flip), so it re-lands on
   the new connection with the existing bounded retry riding any race.

Repro'd on TestFlight 2.0.0 (31) = main @ 82e3b95, the first build
containing mieweb#150.

Fixes mieweb#153
morepriyam added a commit that referenced this pull request Aug 12, 2026
fix(recorder): attach the mic from the first configure - the cameraReady gate flashed every cold open
morepriyam added a commit that referenced this pull request Aug 12, 2026
…264 per connection epoch

Two regressions from one root: #150 removed the accidental session restart
a camera flip used to trigger (the flip's cameraReady reset flipped
micWanted, rebuilding the video output and restarting the session), and
two behaviors were riding it.

1. On iOS, flipCamera()'s setCameraReady(false) never re-armed: onStarted
   maps to AVCaptureSession.didStartRunningNotification, and an iOS device
   swap runs inside beginConfiguration/commitConfiguration on a RUNNING
   session - the notification never fires (start() even early-returns on
   isRunning). Record gestures (enabled: cameraReady && ...) died
   permanently; flipping back couldn't recover. The reset exists solely
   for CameraX (#133 torch-mid-rebind crash) and CameraX re-fires
   onStarted per bind, so it is now Android-only - mirroring the existing
   platform scoping of the zoom/torch prop gates (fb06eea).

2. The H.264 pin is applied per-connection natively
   (output.setOutputSettings(settings, for: connection)), and a flip forms
   a new connection - the pin died with the old one and the effect, keyed
   on output identity, never re-applied: post-flip clips silently recorded
   HEVC. The pin now re-keys on a connection epoch bumped from
   <Camera onConfigured> (fires after connections are formed on every
   reconfigure: cold open, enableAudio rebuild, flip), so it re-lands on
   the new connection with the existing bounded retry riding any race.

Repro'd on TestFlight 2.0.0 (31) = main @ f21fb0b, the first build
containing #150.

Fixes #153
@morepriyam morepriyam moved this to Done in Scrum Team Jerry Aug 14, 2026
@morepriyam morepriyam self-assigned this Aug 14, 2026
@github-project-automation github-project-automation Bot moved this from Ready to Done in Scrum Team Jerry Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

recorder: visible flash after cold open - enableAudio flip rebuilds the video output on the live session

2 participants