Skip to content

fix: audio-mix and stop/start races in both publishers - #32

Merged
joeblau merged 1 commit into
mainfrom
fix/publisher-audio-and-start-race
Jul 4, 2026
Merged

fix: audio-mix and stop/start races in both publishers#32
joeblau merged 1 commit into
mainfrom
fix/publisher-audio-and-start-race

Conversation

@joeblau

@joeblau joeblau commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Two pre-existing robustness gaps shared by RTMPPublisher and SessionPublisher, surfaced by the issue #20 resilience review (they were out of scope there — pre-existing in both, not #20 regressions). Fixed in both via the shared layer; RTMP's normal go-live path is unchanged (both guards fire only on a concurrent stop).

1. App-audio mix silent if the mic is dead from go-live

HaishinKit renders the multitrack mix only when the MAIN track (mic, track 0) appends. The mic-stall failover promoted app audio (track 1) to main only after the mic had appended at least once (lastMicAppendAt > 0) — so a mic dead from the start never promoted, and the whole mix (app audio included) stayed silent forever.

MicStallEvaluator.shouldPromoteApp now takes startedAt and measures mic silence from go-live when the mic has never appeared (micReference = lastMicAppendAt > 0 ? lastMicAppendAt : startedAt). The previously-working stall path is byte-for-byte unchanged.

2. stop() during start()'s pre-isRunning setup leaked tasks

Both publishers ran setup awaits (factory/mixer setup + startRunning) before isRunning = true and before spawning the path-supervisor / watchdog / frame-repeat tasks. A stop() interleaving there early-returned via guard isRunning, then start() resumed and spawned tasks it could never cancel — a 2s watchdog timer waking forever + the mixer left running.

Added guard !userInitiatedStop { await mixer.stopRunning(); return } after mixer.startRunning() in both publishers, plus a second re-check in RTMP after its two await connection.status/await stream.status calls (a narrower window Session doesn't have).

Verification

  • StreamCore: 99 tests pass (+3 new mic-stall cases: dead-from-go-live promotes, within-grace doesn't, no-app-audio doesn't)
  • Full app builds against the iOS 27 SDK
  • Adversarially reviewed (mic-fix correctness · start-race completeness · RTMP normal-path preservation); the review's one residual finding (a second RTMP start window) is fixed here.
  • ⚠️ Audio mixing + start/stop races are device-only-verifiable; worth a smoke test (start with mic denied + app audio on; stop mid-connect).

🤖 Generated with Claude Code

Two pre-existing robustness gaps shared by RTMPPublisher and SessionPublisher,
surfaced by the issue #20 resilience review. Fixed in both via the shared layer.

1. App-audio mix went permanently silent if the mic route was dead from
   go-live. HaishinKit renders the multitrack mix only when the MAIN track
   (mic, track 0) appends; the mic-stall failover only promoted app audio to
   the main track once the mic had appended at least once (`lastMicAppendAt >
   0`), so a mic dead from the start never promoted and the whole mix — app
   audio included — stayed silent forever. MicStallEvaluator now measures mic
   silence from `startedAt` (go-live) when the mic has never appeared, so a
   dead-from-start route still promotes app audio once the grace window passes.
   The previously-working stall path is byte-for-byte unchanged (micReference ==
   lastMicAppendAt when > 0).

2. stop() during start()'s pre-`isRunning` setup leaked long-lived tasks. Both
   publishers ran their setup awaits (factory/mixer setup + startRunning) before
   setting isRunning and spawning the path-supervisor / watchdog / frame-repeat
   tasks; a stop() interleaving there early-returned via `guard isRunning`,
   then start() resumed and spawned tasks it could never cancel (leaving a 2s
   watchdog timer waking forever and the mixer running). Added a
   `guard !userInitiatedStop` bail-out after mixer.startRunning in both, plus a
   second re-check in RTMP after the two status awaits (a narrower window Session
   doesn't have) so the supervisors/watchdog are never spawned post-teardown.

RTMP's normal go-live path is unchanged (both guards fire only on a concurrent
stop). Reviewed adversarially. StreamCore: 99 tests pass (+3 mic-stall cases);
full app builds against the iOS 27 SDK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@joeblau
joeblau merged commit 9f23bb6 into main Jul 4, 2026
1 check passed
@joeblau
joeblau deleted the fix/publisher-audio-and-start-race branch July 4, 2026 23:22
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.

1 participant