feat: always-visible mic level meter in the header - #40
Merged
Conversation
Replaces the redundant "Stream" title with a live microphone meter — a mic glyph plus a five-segment ladder — so the answer to "is my mic working?" is on screen at all times rather than buried in Settings → Audio. Three states are distinguishable at a glance: moving bars (audio is reaching the encoder), an amber glyph with dark bars (no meter samples at all), and a red slash (muted). While broadcasting the level costs nothing — it is read off the existing MicrophoneLevelChannel that ScreenCaptureController already publishes at 10 Hz, post-gain. When idle the monitor opens its own AVAudioEngine tap so the mic can be verified before going live; that tap is released the moment the app leaves the foreground, so the mic is never held behind other apps. Mic permission is now requested at launch, since an always-on meter that can only read "no signal" until the user happens to open Settings is a dead end. MicrophoneLevelMonitor moves out of SettingsView into AudioInputProvider (which owns AudioSessionCoordinator, its dependency) and is now owned by ContentView as a single shared instance — two instances would open competing record sessions on the local-capture path. Because the toolbar meter and the Audio settings pane can be on screen together, start/stop is reference-counted through a Client token so closing the sheet mid-broadcast cannot silence the meter behind it. Also fixes button latency across the app: Haptics.tap() allocated a fresh UIImpactFeedbackGenerator per press with no prepare(), cold-starting the Taptic Engine inside each button's main-actor action — felt as the whole control responding late. Now one long-lived generator, re-armed after each tap and warmed at scene activation. This commit also carries two threads of prior uncommitted work that could not be cleanly separated from the above (they overlap in ContentView.swift): debounced off-main-actor settings persistence, and WHIP H.264-only codec gating with its StreamCore tests. Verified: builds against the iOS 27 SDK for device; the 157-test StreamCore suite passes on an iOS 26 simulator. The meter's on-device appearance has not been visually verified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Replaces the redundant "Stream" title with a live microphone meter — a mic glyph plus a five-segment ladder — so the answer to "is my mic working?" is on screen at all times instead of buried in Settings → Audio.
Three states are distinguishable at a glance:
mic.slashHow
While broadcasting the level costs nothing: it's read off the existing
MicrophoneLevelChannelthatScreenCaptureControlleralready publishes at 10 Hz, post-gain. When idle the monitor opens its ownAVAudioEnginetap so the mic can be verified before going live.MicrophoneLevelMonitormoves out ofSettingsViewintoAudioInputProvider(which ownsAudioSessionCoordinator, its dependency) and is now owned byContentViewas a single shared instance — two instances would open competing record sessions on the local-capture path. Since the toolbar meter and the Audio settings pane can be on screen together, start/stop is reference-counted through aClienttoken, so closing the sheet mid-broadcast can't silence the meter behind it.Going live stays safe: capture and the meter share the serial
AudioSessionCoordinator, so the idle tap's teardown remains ordered ahead of ScreenCaptureKit's session activation.Behavior changes worth reviewing
Also: button latency
Haptics.tap()allocated a freshUIImpactFeedbackGeneratoron every press with noprepare(), cold-starting the Taptic Engine inside each button's main-actor action — felt as the whole control responding late, and every button in the app routes through it. Now one long-lived generator, re-armed after each tap and warmed at scene activation.Scope note
This PR also carries two threads of prior uncommitted working-tree work that couldn't be cleanly separated from the above (they overlap inside
ContentView.swift):SettingsPersistenceCoordinator/SettingsWriter,SettingsStore.saveConnection+saveNonSecret)StreamProtocol.supportedVideoCodecs,effectiveVideoCodec) with its StreamCore testsVerification
-sdk iphoneos27.0)🤖 Generated with Claude Code