I want to split an HLS audio stream into chapters and add them as a list of MediaItem
s to the Media3 player using MediaSessionService. I have this following code in MediaController.
From the composable screen:
fun getMediaItems(uri: String, chapters: List<Chapter>, token: String): List<MediaItem> {
val requestMetaData = MediaItem.RequestMetadata.Builder()
.setExtras(bundleOf( "TOKEN" to token,)) .build()
var offsetInMs = 0L
return chapters.map { chapter ->
val mediaMetaData = MediaMetadata.Builder()
.setTitle(chapter.title)
.build()
val clippingConfiguration = ClippingConfiguration.Builder()
.setStartPositionMs(offsetInMs)
.setEndPositionMs(offsetInMs + chapter.duration)
.setRelativeToDefaultPosition(true)
.build()
offsetInMs += chapter.runtime
MediaItem.Builder()
.setMediaId(chapter.id)
.setUri(uri)
.setMimeType(MimeTypes.APPLICATION_M3U8)
.setClippingConfiguration(clippingConfiguration)
.setRequestMetadata(requestMetaData)
.setMediaMetadata(mediaMetaData)
.build()
}
}
From ViewModel:
var mediaControllerFuture = MutableStateFlow<ListenableFuture<MediaController>?>(null)
fun prepare(context: Context, mediaItems: List<MediaItem>) {
val sessionToken = SessionToken(
context,
ComponentName(context, PlaybackService::class.java),
)
MediaController.Builder(context, sessionToken)
.buildAsync()
.also {
val listener = Runnable {
mediaControllerFuture.value?.get()?.let { controller ->
controller.addListener(listener)
controller.addMediaItems(mediaItems)
controller.prepare()
controller.play()
}
}
mediaControllerFuture.value = it
mediaControllerFuture.value?.addListener(listener, MoreExecutors.directExecutor())
}
}
Service:
class PlaybackService : MediaSessionService() {
private var mediaSession: MediaSession? = null
override fun onCreate() {
super.onCreate()
val customFactory = object : MediaSource.Factory {
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
val token = mediaItem.requestMetadata.extras?.getString("TOKEN") ?: ""
val dataSourceFactory = DataSource.Factory {
val dataSource = DefaultHttpDataSource.Factory().createDataSource()
dataSource.setRequestProperty("Authorization", token)
dataSource
}
return HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(mediaItem)
}
}
val player = ExoPlayer.Builder(this)
.setMediaSourceFactory(customFactory)
.build()
.apply {
playWhenReady = true
}
mediaSession = MediaSession.Builder(this, player)
.build()
}
override fun onTaskRemoved(rootIntent: Intent?) { ... }
override fun onDestroy() { ... }
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
return mediaSession
}
}
However, the player is still playing the entire stream (ignoring start/end positions) and doesn't look like it is moving to the next MediaItem. Or, if it is indeed advancing to subsequent MediaItems how can I find when the player actually advances (so that I can update the player UI to indicate the current chapter being played)? Are there any listener events I can track?
I tried this callback, but never saw it being invoked except once at the very beginning.
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int)
Turned out to be pretty simple. Need to wrap the media source with ClippingMediaSource
like below.
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
val token = mediaItem.requestMetadata.extras?.getString("TOKEN") ?: ""
val dataSourceFactory = DataSource.Factory {
val dataSource = DefaultHttpDataSource.Factory().createDataSource()
dataSource.setRequestProperty("Authorization", token)
dataSource
}
val hlsMediaSource = HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(mediaItem)
return ClippingMediaSource(
hlsMediaSource,
mediaItem.clippingConfiguration.startPositionUs,
mediaItem.clippingConfiguration.endPositionUs,
/* enableInitialDiscontinuity= */
!mediaItem.clippingConfiguration.startsAtKeyFrame,
/* allowDynamicClippingUpdates= */
mediaItem.clippingConfiguration.relativeToLiveWindow,
mediaItem.clippingConfiguration.relativeToDefaultPosition,
)
}