Skip to content

feat(838): mute audio across iOS + Android — default muted, every config path#839

Merged
jonathaneoliver merged 4 commits into
devfrom
feat/838-mute-audio
Jun 23, 2026
Merged

feat(838): mute audio across iOS + Android — default muted, every config path#839
jonathaneoliver merged 4 commits into
devfrom
feat/838-mute-audio

Conversation

@jonathaneoliver

Copy link
Copy Markdown
Owner

Closes #838.

Adds a Mute audio option to Settings → Advanced on iOS and Android, defaulting to muted, settable from every config path. Roku out of scope.

iOS (was ~80% pre-built — finished it)

  • Flipped default isMuted false → true (decl + loadFlags; the load now defaults ON while still honouring -is.flag.muted false via the d.bool coercion pattern).
  • Threaded muted through ServerAppConfig: struct field, parse in fetchServerAppConfig, live overlay (player.isMuted) in applyServerAppConfig so harness/config-on-connect per-play push reaches it.
  • The Settings toggle, is.flag.muted launch flag, UserDefaults persist, force-muted preview tiles, and the isAudioMuted capability migration already existed.

Android (full build, copied from the allow4K pattern)

  • UiState.muted = true; FLAG_MUTED; setMuted() mapping player.volume to 0f/1f.
  • LaunchConfig.muted + is.flag.muted intent capture; load-order override (LaunchConfig.muted ?: prefs(FLAG_MUTED, true)).
  • "Mute audio" PickerItem in the Advanced section.
  • ServerAppConfig.muted (parse from /api/sessions + overlay).
  • applyMuteState() wired at init / state-reset / player-recreate.

Harness + characterization

  • harness app-config <target> --muted true|falseapp_config.muted (+ appconfig_test.go cases).
  • ProbeConfig.Muted-is.flag.muted; "" omits (app default-mutes), false is meaningful (+ probe_test.go cases). Also fixed two stale probe_test.go expectations that omitted the reset_advanced arg and were already failing on the branch.
  • Arm.Muted + MutedS(); CHAR_ARM_<i>_MUTED / CHAR_SWEEP_MUTED wired through both the fleet and sweep-probe readers + plan-summary metadata.
  • Config-on-connect (app.muted=true) is generic — verified, no change needed.

Default-muted means no forced characterization baseline is required; an arm opts into audible playback with muted: false.

Verification

  • iOS: ** BUILD SUCCEEDED ** (iOS simulator).
  • Android: :app:compileDebugKotlin clean.
  • Go: harness-cli + go-proxy build; appconfig / charmatrix / characterization runner tests pass.
  • Not yet done: verifying from data on a real run that audio actually flows on -is.flag.muted false / mutes by default — deferred to on-device check.

🤖 Generated with Claude Code

…ig path

Adds a "Mute audio" option to Settings → Advanced on both client apps,
defaulting to muted, settable from every config path (Settings UI,
cold-launch flag, config-on-connect, harness per-play app-config, and the
characterization matrix knobs). Roku out of scope.

iOS (~80% pre-built — finished):
- Flip default isMuted false → true (decl + loadFlags; the load now
  defaults ON while still honouring `-is.flag.muted false` via d.bool).
- Thread `muted` through ServerAppConfig: struct field, parse in
  fetchServerAppConfig, live overlay (player.isMuted) in applyServerAppConfig.

Android (full build, copied from the allow4K pattern):
- UiState.muted=true; FLAG_MUTED; setMuted() mapping player.volume to 0f/1f;
  LaunchConfig.muted + is.flag.muted intent capture; load-order override;
  "Mute audio" PickerItem in Advanced; ServerAppConfig.muted (parse + overlay).
- applyMuteState() wired at init / state-reset / player-recreate.

Harness + characterization:
- `harness app-config --muted true|false` → app_config.muted (+ test).
- ProbeConfig.Muted → -is.flag.muted; "" omits (app default-mutes), false
  is meaningful (+ tests; also fixed two stale probe_test expectations that
  omitted the reset_advanced arg and were already failing on the branch).
- Arm.Muted + MutedS(); CHAR_ARM_<i>_MUTED / CHAR_SWEEP_MUTED wired through
  both fleet + sweep probe readers + plan-summary metadata.
- config-on-connect (`app.muted=true`) is generic — verified, no change.

Default-muted means no forced characterization baseline is needed; an arm
opts into audible playback with muted:false.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the enhancement New feature or request label Jun 23, 2026
jonathaneoliver and others added 3 commits June 23, 2026 12:04
…-play PATCH)

The handoff assumed config-on-connect was generic for muted, but the proxy's
applyAppConfigPatch is an ALLOWLIST (segment/protocol/live_offset_s/
peak_bitrate_mbps) — `muted` was silently dropped, so neither config-on-connect
(`app.muted`) nor `harness app-config --muted` ever reached the client off
GET /api/sessions. The merge-patch path guard already admits app_config.*
sub-keys via prefix match; only the allowlist needed the field.

- applyAppConfigPatch: store `muted` as a real bool (coerceURLValue already
  turns "true"/"false" → bool for the config-on-connect path, same as strip_*).
- OpenAPI v2 AppConfig.muted (boolean, nullable); regenerate go-proxy oapigen +
  harness-cli v2gen proxy client. forwarder.gen.go picks up pre-existing
  EventRow.app_config drift (committed artifact was stale vs its own yaml).
- Tests: mergepatch (store both ways + null-drop), unsupportedPaths allows
  app_config.muted, config-on-connect parse + round-trip carry muted as bool.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… DF fleet

Lets a char-matrix arm deliver mute through config-on-connect (the bootstrap
PlayerPatch.app_config.muted), so the player reads it back off GET /api/sessions
(applyServerAppConfig) — complementing the launch-arg path. Both derive from the
same arm.Muted and agree; config-on-connect is what writes the session app_config,
so it's the clean proof path.

- sweep.Experiment.Muted (*bool); arm.ToExperiment carries arm.Muted into it.
- experimentPlayerPatch stamps app_config.muted onto the bootstrap patch.
- charmatrix axisKeys admits `is.muted` so a spec can sweep it as an A/B axis.
- matrix/mute-config-on-connect.yaml: 2-arm (muted true/false) DF fleet spec.

Verified on 2 booted iOS sims via Device Farm (CHAR_DEVICE_FARM=1
CHAR_LOCAL_PROXY=true — the overlay needs LocalProxy ON):
  control 9b9c497a → /api/sessions app_config={muted:true}  → sim applied muted=true
  variant b3e9f668 → /api/sessions app_config={muted:false} → sim applied muted=false
Both arms played 30s, both fleet subtests PASS. Confirms the proxy allowlist fix
carries muted end to end over config-on-connect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Companion specs to mute-config-on-connect.yaml for the #838 verification:
both arms carry the same is.muted (from defaults), differing only on a benign
proxy.live_offset so they're distinct fleet arms. Both ran green on a 2-sim
Device Farm fleet (server /api/sessions + per-sim overlay logs agreed):
  mute-both     → both app_config.muted=true,  both sims overlay muted=true
  mute-both-off → both app_config.muted=false, both sims overlay muted=false

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jonathaneoliver jonathaneoliver merged commit 5dc7741 into dev Jun 23, 2026
1 check passed
@jonathaneoliver jonathaneoliver deleted the feat/838-mute-audio branch June 23, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant