177 lines
6.1 KiB
Markdown
177 lines
6.1 KiB
Markdown
|
|
# Media3 Migration + Android Auto Support
|
||
|
|
|
||
|
|
## Goals
|
||
|
|
|
||
|
|
1. Proper system media notification with metadata, album art, and transport controls (Play/Stop + Seek to Live)
|
||
|
|
2. Android Auto compatibility — app registers as a media source with a browsable station catalog
|
||
|
|
|
||
|
|
## Approach
|
||
|
|
|
||
|
|
Migrate from legacy `MediaSessionCompat` + manual `NotificationHelper` to **Media3 `MediaLibraryService`** with a thin `Player` adapter wrapping the existing custom audio engine. The custom audio pipeline (OkHttp → IcyParser → MediaCodec → AudioTrack) remains completely untouched.
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
```
|
||
|
|
RadioController ──intent──▶ RadioPlaybackService (MediaLibraryService)
|
||
|
|
│
|
||
|
|
├── RadioPlayerAdapter (implements Player)
|
||
|
|
│ └── delegates to AudioEngine (untouched)
|
||
|
|
│
|
||
|
|
├── MediaLibrarySession
|
||
|
|
│ ├── auto-publishes metadata/state/art to OS
|
||
|
|
│ ├── auto-manages notification (Play/Stop/SeekLive)
|
||
|
|
│ └── serves browse tree for Android Auto
|
||
|
|
│
|
||
|
|
└── AudioEngine (unchanged custom pipeline)
|
||
|
|
```
|
||
|
|
|
||
|
|
## RadioPlayerAdapter
|
||
|
|
|
||
|
|
A thin facade implementing `androidx.media3.common.Player` that maps the small radio-relevant command surface to existing logic. It does NOT produce or route audio.
|
||
|
|
|
||
|
|
### Supported commands
|
||
|
|
|
||
|
|
- `COMMAND_PLAY_PAUSE`
|
||
|
|
- `COMMAND_STOP`
|
||
|
|
- `COMMAND_GET_CURRENT_MEDIA_ITEM`
|
||
|
|
- `COMMAND_GET_MEDIA_ITEMS_METADATA`
|
||
|
|
- Custom `SessionCommand("SEEK_TO_LIVE")` — exposed as a skip-forward button
|
||
|
|
|
||
|
|
### State mapping
|
||
|
|
|
||
|
|
| PlaybackState (existing) | Media3 Player.STATE_* | isPlaying |
|
||
|
|
|---|---|---|
|
||
|
|
| Idle | STATE_IDLE | false |
|
||
|
|
| Connecting | STATE_BUFFERING | false |
|
||
|
|
| Playing | STATE_READY | true |
|
||
|
|
| Paused | STATE_READY | false |
|
||
|
|
| Reconnecting | STATE_BUFFERING | false |
|
||
|
|
|
||
|
|
### Command mapping
|
||
|
|
|
||
|
|
- `play()` → `RadioController.play(currentStation)`
|
||
|
|
- `stop()` → `RadioController.stop()`
|
||
|
|
- `pause()` → `RadioController.stop()` (radio streams don't pause)
|
||
|
|
- Metadata changes from ICY events update `MediaMetadata` on the adapter, which auto-propagates to notification/lockscreen/Auto/Bluetooth
|
||
|
|
|
||
|
|
## RadioPlaybackService Changes
|
||
|
|
|
||
|
|
Converts from `LifecycleService` to `MediaLibraryService`.
|
||
|
|
|
||
|
|
### Stays the same
|
||
|
|
|
||
|
|
- Intent-based ACTION_PLAY/STOP/PAUSE/SEEK_LIVE control flow
|
||
|
|
- AudioEngine lifecycle (create, start, stop, event collection)
|
||
|
|
- Wake lock + WiFi lock management
|
||
|
|
- Reconnect loop with backoff
|
||
|
|
- Connection span / listening session DB tracking
|
||
|
|
- StreamResolver URL resolution
|
||
|
|
|
||
|
|
### Changes
|
||
|
|
|
||
|
|
- `LifecycleService` → `MediaLibraryService`
|
||
|
|
- `MediaSessionCompat` → `MediaLibrarySession` (built with `RadioPlayerAdapter`)
|
||
|
|
- `onGetSession()` returns the `MediaLibrarySession`
|
||
|
|
- `NotificationHelper` deleted — Media3 auto-generates notifications from session state
|
||
|
|
- `startForeground()` calls removed — `MediaLibraryService` handles foreground lifecycle
|
||
|
|
- On ICY metadata change, adapter's `MediaMetadata` updates → auto-propagates everywhere
|
||
|
|
- Album art: `AlbumArtResolver` resolves artwork → set on `MediaMetadata.artworkUri`
|
||
|
|
|
||
|
|
### Notification layout (auto-managed)
|
||
|
|
|
||
|
|
- Compact: Play/Stop toggle + Seek to Live (custom command button)
|
||
|
|
- Expanded: same + album art + station name + track info
|
||
|
|
- Channel importance: LOW
|
||
|
|
|
||
|
|
## Android Auto Browse Tree
|
||
|
|
|
||
|
|
```
|
||
|
|
[ROOT]
|
||
|
|
├── Playlist: "SomaFM"
|
||
|
|
│ ├── Groove Salad
|
||
|
|
│ ├── Drone Zone
|
||
|
|
│ └── ...
|
||
|
|
├── Playlist: "My Favorites"
|
||
|
|
│ ├── Station A
|
||
|
|
│ └── Station B
|
||
|
|
└── Unsorted (if any stations have no playlist)
|
||
|
|
├── Station X
|
||
|
|
└── Station Y
|
||
|
|
```
|
||
|
|
|
||
|
|
### Callbacks
|
||
|
|
|
||
|
|
- `onGetLibraryRoot()` → root MediaItem (browsable)
|
||
|
|
- `onGetChildren(rootId)` → playlists + unsorted folder (browsable MediaItems)
|
||
|
|
- `onGetChildren(playlistId)` → stations in playlist (playable MediaItems)
|
||
|
|
- `onGetChildren("unsorted")` → stations with no playlist
|
||
|
|
- `onSetMediaItem()` / `onAddMediaItems()` → parse mediaId, look up station, call `RadioController.play()`
|
||
|
|
|
||
|
|
### Media IDs
|
||
|
|
|
||
|
|
- Root: `"root"`
|
||
|
|
- Playlist folder: `"playlist:{id}"`
|
||
|
|
- Unsorted folder: `"unsorted"`
|
||
|
|
- Station item: `"station:{id}"`
|
||
|
|
|
||
|
|
### Data freshness
|
||
|
|
|
||
|
|
DAO `Flow` methods use `.first()` for snapshots. Updates visible on next navigation (acceptable for a radio catalog).
|
||
|
|
|
||
|
|
## Dependencies
|
||
|
|
|
||
|
|
### Add
|
||
|
|
|
||
|
|
```toml
|
||
|
|
media3 = "1.6.0"
|
||
|
|
media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }
|
||
|
|
media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3" }
|
||
|
|
```
|
||
|
|
|
||
|
|
### Remove
|
||
|
|
|
||
|
|
- `media-session` (`androidx.media:media:1.7.1`)
|
||
|
|
|
||
|
|
No `media3-exoplayer` needed — custom audio engine stays.
|
||
|
|
|
||
|
|
## Manifest Changes
|
||
|
|
|
||
|
|
```xml
|
||
|
|
<service
|
||
|
|
android:name=".service.RadioPlaybackService"
|
||
|
|
android:exported="true"
|
||
|
|
android:foregroundServiceType="mediaPlayback">
|
||
|
|
<intent-filter>
|
||
|
|
<action android:name="androidx.media3.session.MediaLibraryService"/>
|
||
|
|
<action android:name="android.media.browse.MediaBrowserService"/>
|
||
|
|
</intent-filter>
|
||
|
|
</service>
|
||
|
|
|
||
|
|
<meta-data
|
||
|
|
android:name="com.google.android.gms.car.application"
|
||
|
|
android:resource="@xml/automotive_app" />
|
||
|
|
```
|
||
|
|
|
||
|
|
## New File: `res/xml/automotive_app.xml`
|
||
|
|
|
||
|
|
```xml
|
||
|
|
<?xml version="1.0" encoding="utf-8"?>
|
||
|
|
<automotiveApp>
|
||
|
|
<uses name="media"/>
|
||
|
|
</automotiveApp>
|
||
|
|
```
|
||
|
|
|
||
|
|
## Files Affected
|
||
|
|
|
||
|
|
| File | Action |
|
||
|
|
|---|---|
|
||
|
|
| `libs.versions.toml` | Add media3 version + libraries, remove old media |
|
||
|
|
| `app/build.gradle.kts` | Swap dependencies |
|
||
|
|
| `AndroidManifest.xml` | Update service declaration, add Auto meta-data |
|
||
|
|
| `res/xml/automotive_app.xml` | New file |
|
||
|
|
| `RadioPlayerAdapter.kt` | New file — Player facade |
|
||
|
|
| `RadioPlaybackService.kt` | Rewrite to extend MediaLibraryService |
|
||
|
|
| `NotificationHelper.kt` | Delete |
|
||
|
|
| `RadioController.kt` | Minor — may need adapter reference for metadata updates |
|
||
|
|
| `RadioApplication.kt` | Minor — remove NotificationHelper if referenced |
|