Skip to content

Add per-site background audio playback - #501

Open
theoden8 wants to merge 6 commits into
masterfrom
claude/background-audio-playback-mulsg3
Open

Add per-site background audio playback#501
theoden8 wants to merge 6 commits into
masterfrom
claude/background-audio-playback-mulsg3

Conversation

@theoden8

Copy link
Copy Markdown
Owner

Answers the "can you play audio in the background?" user question with a per-site Background audio toggle (spec: openspec/specs/background-audio/spec.md, BGAUDIO-001..005).

Why audio stopped

  • Site switch: iOS per-instance pause is the plugin's pauseTimers() alert-deadlock hack — it freezes the page's JS thread, so MSE/streaming players (YouTube Music, radio sites) stall within seconds even though the decoder itself survives.
  • App background: pauseForAppLifecycle() freezes JS timers process-globally on Android, and iOS suspends the whole app without a .playback audio session.

What the toggle does

  • pauseWebView() early-returns for background-audio sites (mirrors the notification-site exemption; archive-tier sites stay paused per ARCH-006).
  • AppLifecycleEngine.backgroundPlan skips the app-background JS pause when any loaded site has the toggle — Android's pauseTimers() is process-global, so pausing the active site would starve a backgrounded player. Resume mirrors; state capture is not gated. The paused branch now logs a non-sensitive App background: jsPause=<bool> capture=<bool> decision line.
  • iOS: audio added to UIBackgroundModes; BackgroundTaskPlugin switches the shared AVAudioSession to .playback while a background-audio site is loaded (back to .ambient otherwise — silent-switch behavior unchanged for ordinary sites). Category only, no forced activation, so no audio-focus stealing while idle.
  • Retention: background-audio sites share the notification eviction tier (no formal-model change — retention.tla abstracts the retained flag).
  • Setting rides WebViewModel.toJson (conditional key, archive byte-identity preserved), the settings-QR share set, and the per-site settings UI; strings added to all 68 ARB locales.

No Android foreground service (same Play-review posture as notifications): audio plays while the process lives; the OS's reluctance to kill audio-playing processes plus the retention tier mitigate the rest.

Testing background mode in CI

Real OS backgrounding can't be driven from an integration test, but the whole path hangs off didChangeAppLifecycleState, so integration_test/background_audio_lifecycle_test.dart injects lifecycle states via tester.binding.handleAppLifecycleStateChanged and observes two things without touching private app state:

  1. the decision log line (pause vs exemption), written synchronously by the handler;
  2. real JS liveness: the new HTML fixture (integration_test/fixtures/background_audio.html) beacons GET /beacon?ticks=N&audio=<playState> to the test's loopback server every 250 ms — beacons continuing through the injected background window, with monotonically increasing ticks, prove the same live page kept running. Runs on the existing Linux (WPE) and macOS CI jobs.

The "JS actually freezes for non-exempt sites" direction is only true on Android/iOS hardware (Linux/macOS plugins implement no pauseTimers()), so that matrix lives in test/app_lifecycle_engine_test.dart instead.

Verification

  • flutter analyze: 0 errors
  • flutter test: 1858 passed (incl. new engine + pause-contract cases)
  • npm run test:js: 284 passed (l10n key parity + CLD3 language identity over the 67 translated locales)
  • npx openspec validate --no-interactive --all: 50 passed

🤖 Generated with Claude Code

https://claude.ai/code/session_01EizMhpW94e7q2fYVQW8JQC


Generated by Claude Code

@theoden8
theoden8 force-pushed the claude/background-audio-playback-mulsg3 branch from 9ad3d17 to 786ea12 Compare July 20, 2026 12:39
claude added 6 commits July 21, 2026 15:34
Streaming players stall when lifecycle pauses freeze JS; iOS also suspends
the app without a .playback session. Toggle exempts the site from both
pauses and keeps CI-testable via lifecycle injection + beaconing fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizMhpW94e7q2fYVQW8JQC
macOS CI's ad-hoc-entitled test app gets EPERM reading repo files at
runtime. Drift test pins the mirror to the authoritative .html.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizMhpW94e7q2fYVQW8JQC
WPE in the CI container (no GStreamer plugin sets or audio sink)
crash-loops the web process on media-pipeline init, so no beacon ever
arrived. The audio element is now built in JS and skipped under
?noMedia=1; the lifecycle test asserts JS-timer liveness only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizMhpW94e7q2fYVQW8JQC
Lock the settings-save round-trip + effective getter so an enabled
toggle can't silently regress to jsPause=true after restart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizMhpW94e7q2fYVQW8JQC
…dio (BGAUDIO-006)

Lightweight exemption kept audio best-effort and showed no controls. A
mediaPlayback foreground service with a MediaStyle notification now keeps
playback alive when backgrounded and gives lockscreen/notification
play-pause, driven by a page-JS media-session bridge. Media playback is a
Play-accepted FGS type, unlike SPECIAL_USE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizMhpW94e7q2fYVQW8JQC
Deliberate root-only field (media notification is a root-site concern,
not a privacy posture) — PLUMBING, like backForwardGestures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizMhpW94e7q2fYVQW8JQC
@theoden8
theoden8 force-pushed the claude/background-audio-playback-mulsg3 branch from 6146803 to 776ded6 Compare July 21, 2026 14:34
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.

2 participants