feat(838): mute audio across iOS + Android — default muted, every config path#839
Merged
Conversation
…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>
…-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>
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.
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)
isMutedfalse → true (decl +loadFlags; the load now defaults ON while still honouring-is.flag.muted falsevia thed.boolcoercion pattern).mutedthroughServerAppConfig: struct field, parse infetchServerAppConfig, live overlay (player.isMuted) inapplyServerAppConfigso harness/config-on-connect per-play push reaches it.is.flag.mutedlaunch flag, UserDefaults persist, force-muted preview tiles, and theisAudioMutedcapability migration already existed.Android (full build, copied from the
allow4Kpattern)UiState.muted = true;FLAG_MUTED;setMuted()mappingplayer.volumeto0f/1f.LaunchConfig.muted+is.flag.mutedintent capture; load-order override (LaunchConfig.muted ?: prefs(FLAG_MUTED, true)).PickerItemin 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|false→app_config.muted(+appconfig_test.gocases).ProbeConfig.Muted→-is.flag.muted;""omits (app default-mutes),falseis meaningful (+probe_test.gocases). Also fixed two staleprobe_test.goexpectations that omitted thereset_advancedarg and were already failing on the branch.Arm.Muted+MutedS();CHAR_ARM_<i>_MUTED/CHAR_SWEEP_MUTEDwired through both the fleet and sweep-probe readers + plan-summary metadata.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
** BUILD SUCCEEDED **(iOS simulator).:app:compileDebugKotlinclean.harness-cli+go-proxybuild;appconfig/charmatrix/ characterizationrunnertests pass.-is.flag.muted false/ mutes by default — deferred to on-device check.🤖 Generated with Claude Code