Skip to content

feat: always-visible mic level meter in the header - #40

Merged
joeblau merged 1 commit into
mainfrom
feat/mic-level-meter-in-header
Aug 2, 2026
Merged

feat: always-visible mic level meter in the header#40
joeblau merged 1 commit into
mainfrom
feat/mic-level-meter-in-header

Conversation

@joeblau

@joeblau joeblau commented Aug 2, 2026

Copy link
Copy Markdown
Owner

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:

What you see What it means
Bars moving Audio is reaching the encoder
Amber glyph, dark bars No meter samples at all — the mic isn't feeding the stream
Red mic.slash Muted (gain 0)

How

While broadcasting the level costs nothing: it's 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.

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. Since 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 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

  • The orange mic indicator is on whenever Stream is foregrounded. Inherent to metering an idle mic. It's released as soon as the app leaves the foreground, so the mic is never held behind other apps.
  • Mic permission is now requested at launch (previously deferred to Settings). An always-on meter that can only read "no signal" until the user happens to open Settings is a dead end.

Also: button latency

Haptics.tap() allocated a fresh UIImpactFeedbackGenerator on every press with no prepare(), 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):

  • Debounced, off-main-actor settings persistence (SettingsPersistenceCoordinator / SettingsWriter, SettingsStore.saveConnection + saveNonSecret)
  • WHIP H.264-only codec gating (StreamProtocol.supportedVideoCodecs, effectiveVideoCodec) with its StreamCore tests

Verification

  • Builds against the iOS 27 SDK for device (-sdk iphoneos27.0)
  • The 157-test StreamCore suite passes on an iOS 26 simulator — the same suite CI runs
  • Not verified: the meter's on-device appearance. iOS 26+ toolbar glass grouping and the 3.5×14pt segment sizing were chosen by eye, not seen on hardware.

🤖 Generated with Claude Code

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>
@joeblau
joeblau merged commit be2c8ba into main Aug 2, 2026
1 check passed
@joeblau
joeblau deleted the feat/mic-level-meter-in-header branch August 2, 2026 15:51
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