androidandroid-jetpack-composeandroid-mediaplayerexoplayerandroid-mediasession

Android Media3 Session & Controller - Playback not starting


I'm trying to implement the Android Media3 MediaSessionService and MediaController but for some reason the playback doesn't start. What am I doing wrong? I think I did everything exactly as described in Play media in the background.

PlaybackService.kt

class PlaybackService : MediaSessionService() {

    private var mediaSession: MediaSession? = null

    override fun onCreate() {
        super.onCreate()
        val player = ExoPlayer.Builder(this).build()
        mediaSession = MediaSession.Builder(this, player).build()
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
        mediaSession

    override fun onDestroy() {
        mediaSession?.run {
            player.release()
            release()
            mediaSession = null
        }
        super.onDestroy()
    }
}

MainActivity.kt

class MainActivity : ComponentActivity() {

    private lateinit var controllerFuture: ListenableFuture<MediaController>
    private lateinit var controller: MediaController

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        log("onCreate MainActivity")
        setContent {
            TestMediaTheme {

                Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {

                    Button(onClick = {

                        //val url = "android.resource://$packageName/${R.raw.test}"
                        val url = "https://download.samplelib.com/mp3/sample-15s.mp3"

                        play(url)

                    }) {
                        Text(text = "Play")
                    }

                }

            }
        }
    }

    override fun onStart() {
        super.onStart()
        val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
        controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
        controllerFuture.addListener(
            {
                controller = controllerFuture.get()
                initController()
            },
            MoreExecutors.directExecutor()
        )
    }

    override fun onStop() {
        MediaController.releaseFuture(controllerFuture)
        super.onStop()
    }

    private fun initController() {
        //controller.playWhenReady = true
        controller.addListener(object : Player.Listener {

            override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
                super.onMediaMetadataChanged(mediaMetadata)
                log("onMediaMetadataChanged=$mediaMetadata")
            }

            override fun onIsPlayingChanged(isPlaying: Boolean) {
                super.onIsPlayingChanged(isPlaying)
                log("onIsPlayingChanged=$isPlaying")
            }

            override fun onPlaybackStateChanged(playbackState: Int) {
                super.onPlaybackStateChanged(playbackState)
                log("onPlaybackStateChanged=${getStateName(playbackState)}")
            }

            override fun onPlayerError(error: PlaybackException) {
                super.onPlayerError(error)
                log("onPlayerError=${error.stackTraceToString()}")
            }

            override fun onPlayerErrorChanged(error: PlaybackException?) {
                super.onPlayerErrorChanged(error)
                log("onPlayerErrorChanged=${error?.stackTraceToString()}")
            }
        })
        log("start=${getStateName(controller.playbackState)}")
        log("COMMAND_PREPARE=${controller.isCommandAvailable(COMMAND_PREPARE)}")
        log("COMMAND_SET_MEDIA_ITEM=${controller.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)}")
        log("COMMAND_PLAY_PAUSE=${controller.isCommandAvailable(COMMAND_PLAY_PAUSE)}")
    }

    private fun play(url: String) {
        log("play($url)")
        log("before=${getStateName(controller.playbackState)}")
        controller.setMediaItem(MediaItem.fromUri(url))
        controller.prepare()
        controller.play()
        log("after=${getStateName(controller.playbackState)}")
    }

    private fun getStateName(i: Int): String? {
        return when (i) {
            1 -> "STATE_IDLE"
            2 -> "STATE_BUFFERING"
            3 -> "STATE_READY"
            4 -> "STATE_ENDED"
            else -> null
        }
    }

    private fun log(message: String) {
        Log.e("=====[TestMedia]=====", message)
    }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.TestMedia"
        tools:targetApi="33">

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:theme="@style/Theme.TestMedia">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>

        <service
            android:name=".PlaybackService"
            android:exported="true"
            android:foregroundServiceType="mediaPlayback">
            <intent-filter>
                <action android:name="androidx.media3.session.MediaSessionService" />
                <action android:name="android.media.browse.MediaBrowserService" />
            </intent-filter>
        </service>

    </application>

</manifest>

And here's the debug log:

01:51:22.004  E  onCreate MainActivity
01:51:22.544  E  start=STATE_IDLE
01:51:22.544  E  COMMAND_PREPARE=true
01:51:22.544  E  COMMAND_SET_MEDIA_ITEM=true
01:51:22.544  E  COMMAND_PLAY_PAUSE=true
//click 1
01:51:24.027  E  play(https://download.samplelib.com/mp3/sample-15s.mp3)
01:51:24.027  E  before=STATE_IDLE
01:51:24.029  E  onPlaybackStateChanged=STATE_BUFFERING
01:51:24.029  E  after=STATE_BUFFERING
01:51:24.053  E  onPlaybackStateChanged=STATE_ENDED
//click 2
01:51:25.715  E  play(https://download.samplelib.com/mp3/sample-15s.mp3)
01:51:25.715  E  before=STATE_ENDED
01:51:25.716  E  onPlaybackStateChanged=STATE_BUFFERING
01:51:25.716  E  after=STATE_BUFFERING
//click 3
01:51:26.749  E  play(https://download.samplelib.com/mp3/sample-15s.mp3)
01:51:26.749  E  before=STATE_BUFFERING
01:51:26.750  E  after=STATE_BUFFERING
//click 4
01:51:30.172  E  play(https://download.samplelib.com/mp3/sample-15s.mp3)
01:51:30.172  E  before=STATE_BUFFERING
01:51:30.173  E  after=STATE_BUFFERING

So it looks as if after the first click the player buffers and then immediately ends, and after the second click it just buffers indefinitely. Anyone got an idea what might be the problem?


Solution

  • Update:

    It was fixed in Version 1.1.0.

    Add default implementation to MediaSession.Callback.onAddMediaItems to allow requested MediaItems to be passed onto Player if they have LocalConfiguration (e.g. URI).


    Old Answer:

    Finally I found the solution thanks to this issue and this question. It seems like the Media3 Guide is missing a very crucial part.

    From the onAddMediaItems documentation: Note that the requested media items don't have a MediaItem.LocalConfiguration (for example, a URI) and need to be updated to make them playable by the underlying Player.

    In the end I solved it by overriding MediaSession.Callback.onAddMediaItems

    class PlaybackService : MediaSessionService(), MediaSession.Callback {
    
        private var mediaSession: MediaSession? = null
    
        override fun onCreate() {
            super.onCreate()
            val player = ExoPlayer.Builder(this).build()
            mediaSession = MediaSession.Builder(this, player).setCallback(this).build()
        }
    
        override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
            mediaSession
    
        override fun onDestroy() {
            mediaSession?.run {
                player.release()
                release()
                mediaSession = null
            }
            super.onDestroy()
        }
    
        override fun onAddMediaItems(
            mediaSession: MediaSession,
            controller: MediaSession.ControllerInfo,
            mediaItems: MutableList<MediaItem>
        ): ListenableFuture<MutableList<MediaItem>> {
            val updatedMediaItems = mediaItems.map { it.buildUpon().setUri(it.mediaId).build() }.toMutableList()
            return Futures.immediateFuture(updatedMediaItems)
        }
    }
    

    and then replacing

    controller.setMediaItem(MediaItem.fromUri(url))
    

    by

    val media = MediaItem.Builder().setMediaId(url).build()
    controller.setMediaItem(media)