Skip to content

feat: detached audio playback - #148

Open
brunoro wants to merge 1 commit into
mainfrom
feat/audio-detached
Open

feat: detached audio playback#148
brunoro wants to merge 1 commit into
mainfrom
feat/audio-detached

Conversation

@brunoro

@brunoro brunoro commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Tools can move playback into an SDK-owned MediaSessionService, so audio survives the screen that started it. newPlayer(playback = Detached) returns a handle backed by a MediaController instead of an in-process ExoPlayer.
Both modes share one public surface for queue, transport, position, metadata and errors, so nothing from media3 reaches the public API.

A detailed explanation of the architecture can be found in docs/design_decisions/detached_audio.md.

Some noteworthy details:

  • release() on a detached handle disconnects without stopping playback. Call stop() first to actually end it.
  • A reconnecting player may find a live session, so it must awaitReady() and reuse a non-empty queue rather than replacing it.
  • The service stops itself 60s after playback pauses with no handle open. The handle is the liveness signal, not the controller count. media3's notification controller and platform media button controllers might connect too, and this design ignores those connections for the timeout.
  • A live session keeps its LightAudioUsage. Reconnecting with a different one throws.
  • Playback failures surface as LightAudioError in both modes; player-creation failures throw LightAudioPlayerException.

Opting in

[tool]
capabilities = ["detached-audio"]

[tool].capabilities is new. The Gradle plugin expands detached-audio into the two foreground-service permissions, the LightAudioService declaration, and a com.thelightphone.sdk.CAPABILITY_DETACHED_AUDIO metadata marker.

The metada marker is what the SDK checks at runtime.

androidx.work contributes FOREGROUND_SERVICE transitively to every tool, so a platform permission can never be a statement about what the tool asked for. A metadata key under our own namespace can only come from our generator.

Both permissions were removed from ALLOWED_PERMISSIONS; hand-writing one now fails the build with an error naming the capability to declare instead.

LightAudioService also moved out of the SDK library manifest, so tools that never touch audio no longer ship a mediaPlayback claim.

Testing

The audio demo Player tab has a DETACHED toggle that switches between the playback modes. With DETACHED: ON, you should be able to lock the phone and navigate in the UI without stopping playback. The player state should be updated when you come back to it.

Tools can move playback into an SDK-owned MediaSessionService so it
survives the screen that started it. newPlayer(playback = Detached)
returns a handle backed by a MediaController instead of an in-process
ExoPlayer; both modes share one public surface for queue, transport,
position, metadata and errors.

Opt in with `capabilities = ["detached-audio"]` in lighttool.toml. The
Gradle plugin turns that one entry into the foreground-service
permissions, the service declaration, and a meta-data marker under our
own namespace. The marker is what the SDK checks at runtime, because a
platform permission can be contributed by any transitive dependency and
says nothing about what the tool asked for.

Detached ownership differs from attached in two ways the docs lead with:
release() disconnects the handle without stopping playback, and a
reconnecting player may find a live session, so it awaits readiness and
reuses a non-empty queue rather than replacing it. The service stops
itself 60 seconds after playback pauses with no handle open.

Playback failures surface as SDK-owned LightAudioError values in both
modes, so no media3 type reaches the public API.
@brunoro
brunoro requested a review from dupontgu August 5, 2026 23:37
@brunoro

brunoro commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

cc @jonathancaudill @tthayer

@jonathancaudill

Copy link
Copy Markdown

@brunoro this looks great!!

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