feat(android): MinstrelPlayerService — Media3 MediaSessionService (M8 phase 6.2)
The actual replacement for everything audio_service plugin wrapped.
Media3 owns the foreground-service lifecycle, MediaSession token,
notification card, lock-screen surface, Bluetooth/AVRCP routing,
Pixel Watch tile, and Android Auto adapter natively — no plugin
layer between us and the platform.
Service shape (~25 LOC):
- @AndroidEntryPoint MediaSessionService
- @Inject PlayerFactory builds ExoPlayer in onCreate
- onGetSession returns the live MediaSession to any binding
controller (system UI, Wear OS companion, MediaController3 clients)
- onTaskRemoved keeps playing while audio is active (standard
media-app behavior); otherwise stopSelf so notification clears
- onDestroy releases session + player
Compare with flutter_client/lib/player/audio_handler.dart's 1000+ LOC
across MinstrelAudioHandler + the soft-teardown / stall-watchdog /
recovery machinery. Media3 owns most of that natively; we'll get to
the small portions we still need (queue management, position
reporting facade) in 6.3.
Manifest registration: foregroundServiceType="mediaPlayback" +
MediaSessionService intent-filter. MediaButtonReceiver is registered
by the Media3 library; no manual receiver class needed (Flutter's
manifest had to declare audio_service's receiver explicitly).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,13 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Media3 MediaSessionService is registered in Phase 6 (Task 6.2). -->
|
||||
<service
|
||||
android:name=".player.MinstrelPlayerService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.fabledsword.minstrel.player
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* The app's foreground media-playback service. Replaces what the Flutter
|
||||
* audio_service plugin wrapped — Media3 owns the foreground lifecycle,
|
||||
* the MediaSession, the notification card, lock-screen surface, BT/AVRCP
|
||||
* routing, Pixel Watch tile, and Android Auto adapter natively.
|
||||
*
|
||||
* Manifest declares this with `foregroundServiceType="mediaPlayback"`
|
||||
* plus the standard `MediaSessionService` intent-filter — Media3's
|
||||
* MediaButtonReceiver is registered automatically by the library, no
|
||||
* manual receiver class needed.
|
||||
*
|
||||
* Lifecycle:
|
||||
* - onCreate: build the ExoPlayer + MediaSession once.
|
||||
* - onGetSession: return the live session to any binding controller
|
||||
* (system UI media card, Wear OS companion, MediaController3 clients).
|
||||
* - onTaskRemoved: if the user swipes the app away, keep playing when
|
||||
* audio is active (standard media-app behavior — music shouldn't
|
||||
* die because the app left recents); otherwise stop the service so
|
||||
* the lingering notification clears.
|
||||
* - onDestroy: release the session + player.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class MinstrelPlayerService : MediaSessionService() {
|
||||
|
||||
@Inject lateinit var playerFactory: PlayerFactory
|
||||
|
||||
private var mediaSession: MediaSession? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val player = playerFactory.build()
|
||||
mediaSession = MediaSession.Builder(this, player).build()
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
|
||||
mediaSession
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
val player = mediaSession?.player ?: return super.onTaskRemoved(rootIntent)
|
||||
val activelyPlaying = player.playWhenReady && player.playbackState != Player.STATE_ENDED
|
||||
if (!activelyPlaying) {
|
||||
stopSelf()
|
||||
}
|
||||
super.onTaskRemoved(rootIntent)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaSession?.run {
|
||||
player.release()
|
||||
release()
|
||||
}
|
||||
mediaSession = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user