Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve media controller service #485

Merged
merged 28 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6f6e0be
Hide implementation behind interfaces
StaehliJ Mar 26, 2024
2e950ca
Add guava kotlin coroutines
StaehliJ Mar 26, 2024
da85615
wip Improve MediaController and Mediabrowser
StaehliJ Mar 26, 2024
22c5219
Try with MediaSessionExtras
StaehliJ Mar 27, 2024
8071d5b
Add Pillarbox MediaSession
StaehliJ Mar 28, 2024
21ac94b
Fix Android Auto, every media item have to be findable
StaehliJ Mar 28, 2024
8290f57
Fix default implementation onSetMediaItems
StaehliJ Apr 2, 2024
5cdbfa3
wip
StaehliJ Apr 3, 2024
4212c76
handle all events
StaehliJ Apr 3, 2024
3844b6b
Improve kdoc
StaehliJ Apr 4, 2024
6daad85
Use PillarboxMediaSession in TV demo
StaehliJ Apr 4, 2024
bc773fe
Update service documentation
StaehliJ Apr 4, 2024
168efd5
Remove useless interface PillarboxExoPlayer
StaehliJ Apr 4, 2024
726d181
Update README.md
StaehliJ Apr 4, 2024
6f1a24d
Improve services
StaehliJ Apr 4, 2024
e1d6c4c
Make the project build again
StaehliJ Apr 5, 2024
dda5578
Use a var instead of setPlayer to keep same api as MediaSession
StaehliJ Apr 5, 2024
1cda4ec
Update showcase
StaehliJ Apr 5, 2024
30a5189
Fix buildHealth
StaehliJ Apr 5, 2024
aa9eb75
Rebase on main
StaehliJ Apr 5, 2024
6024ff9
fix test after rebase
StaehliJ Apr 5, 2024
a1a0a03
Merge branch 'main' into improve-media-controller-service
StaehliJ Apr 8, 2024
89833a2
Update some documentation
MGaetan89 Apr 8, 2024
a879368
Ignore warnings
MGaetan89 Apr 8, 2024
3c5476c
Remove code duplication
MGaetan89 Apr 8, 2024
676a76c
Fix `TRACKER_ENABLED` command
MGaetan89 Apr 8, 2024
fd5ccc4
Add missing handling of the `TRACKER_ENABLED` command
MGaetan89 Apr 8, 2024
7b5262b
Add test
MGaetan89 Apr 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "moc
mockk-dsl = { group = "io.mockk", name = "mockk-dsl-jvm", version.ref = "mockk" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "androidx-media3" }
androidx-media3-datasource = { group = "androidx.media3", name = "media3-datasource", version.ref = "androidx-media3" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,36 @@ import ch.srgssr.pillarbox.core.business.integrationlayer.service.HttpMediaCompo
import ch.srgssr.pillarbox.core.business.integrationlayer.service.MediaCompositionService
import ch.srgssr.pillarbox.core.business.source.SRGAssetLoader
import ch.srgssr.pillarbox.core.business.tracker.DefaultMediaItemTrackerRepository
import ch.srgssr.pillarbox.player.PillarboxExoPlayer
import ch.srgssr.pillarbox.player.PillarboxLoadControl
import ch.srgssr.pillarbox.player.PillarboxPlayer
import ch.srgssr.pillarbox.player.SeekIncrement
import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory
import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider
import kotlin.time.Duration.Companion.seconds

/**
* DefaultPillarbox convenient class to create [PillarboxPlayer] that suit Default SRG needs.
* [DefaultPillarbox] is a convenient class to create a [PillarboxExoPlayer] that suits the default SRG needs.
*/
object DefaultPillarbox {
private val defaultSeekIncrement = SeekIncrement(backward = 10.seconds, forward = 30.seconds)

/**
* Invoke create an instance of [PillarboxPlayer]
* Invoke create an instance of [PillarboxExoPlayer]
*
* @param context The context.
* @param seekIncrement The seek increment.
* @param mediaItemTrackerRepository The provider of MediaItemTracker, by default [DefaultMediaItemTrackerRepository].
* @param mediaCompositionService The [MediaCompositionService] to use, by default [HttpMediaCompositionService].
* @param loadControl The load control, by default [PillarboxLoadControl].
* @return [PillarboxPlayer] suited for SRG.
* @return [PillarboxExoPlayer] suited for SRG.
*/
operator fun invoke(
context: Context,
seekIncrement: SeekIncrement = defaultSeekIncrement,
mediaItemTrackerRepository: MediaItemTrackerProvider = DefaultMediaItemTrackerRepository(),
mediaCompositionService: MediaCompositionService = HttpMediaCompositionService(),
loadControl: LoadControl = PillarboxLoadControl(),
): PillarboxPlayer {
): PillarboxExoPlayer {
return DefaultPillarbox(
context = context,
seekIncrement = seekIncrement,
Expand All @@ -54,15 +54,15 @@ object DefaultPillarbox {
}

/**
* Invoke create an instance of [PillarboxPlayer]
* Invoke create an instance of [PillarboxExoPlayer]
*
* @param context The context.
* @param seekIncrement The seek increment.
* @param mediaItemTrackerRepository The provider of MediaItemTracker, by default [DefaultMediaItemTrackerRepository].
* @param loadControl The load control, by default [DefaultLoadControl].
* @param mediaCompositionService The [MediaCompositionService] to use, by default [HttpMediaCompositionService].
* @param clock The internal clock used by the player.
* @return [PillarboxPlayer] suited for SRG.
* @return [PillarboxExoPlayer] suited for SRG.
*/
@VisibleForTesting
operator fun invoke(
Expand All @@ -72,8 +72,8 @@ object DefaultPillarbox {
loadControl: LoadControl = DefaultLoadControl(),
mediaCompositionService: MediaCompositionService = HttpMediaCompositionService(),
clock: Clock,
): PillarboxPlayer {
return PillarboxPlayer(
): PillarboxExoPlayer {
return PillarboxExoPlayer(
context = context,
seekIncrement = seekIncrement,
mediaSourceFactory = PillarboxMediaSourceFactory(context).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import androidx.media3.common.MediaMetadata
*/
class DemoBrowser {

/**
* Every Android Auto navigable [MediaItem] accessed by id.
*/
private val mapMediaIdMediaItem = mutableMapOf<String, MediaItem>()
private val mapMediaIdToChildren = mutableMapOf<String, MutableList<MediaItem>>()

Expand All @@ -41,6 +44,7 @@ class DemoBrowser {

init {
val rootList = mapMediaIdToChildren[DEMO_BROWSABLE_ROOT] ?: mutableListOf()
mapMediaIdMediaItem[DEMO_BROWSABLE_ROOT] = rootMediaItem
val listPlaylist = listOf(
Playlist.StreamUrls,
Playlist.StreamUrns,
Expand All @@ -52,7 +56,9 @@ class DemoBrowser {
Playlist.BitmovinSamples,
)
for (playlist in listPlaylist) {
rootList += playlist.toMediaItem()
val playlistRootItem = playlist.toMediaItem()
rootList += playlistRootItem
mapMediaIdMediaItem[playlistRootItem.mediaId] = playlistRootItem
for (playlistItem in playlist.items) {
val item = playlistItem.toMediaItem()
mapMediaIdMediaItem[item.mediaId] = item
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import ch.srgssr.pillarbox.core.business.source.SRGAssetLoader
import ch.srgssr.pillarbox.core.business.tracker.DefaultMediaItemTrackerRepository
import ch.srgssr.pillarbox.demo.shared.source.CustomAssetLoader
import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.ILRepository
import ch.srgssr.pillarbox.player.PillarboxPlayer
import ch.srgssr.pillarbox.player.PillarboxExoPlayer
import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory
import java.net.URL

Expand All @@ -25,8 +25,8 @@ object PlayerModule {
/**
* Provide default player that allow to play urls and urns content from the SRG
*/
fun provideDefaultPlayer(context: Context): PillarboxPlayer {
return PillarboxPlayer(
fun provideDefaultPlayer(context: Context): PillarboxExoPlayer {
return PillarboxExoPlayer(
context = context,
mediaSourceFactory = PillarboxMediaSourceFactory(context).apply {
addAssetLoader(SRGAssetLoader(context))
Expand Down
1 change: 0 additions & 1 deletion pillarbox-demo-tv/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.media3.common)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.session)
implementation(libs.androidx.media3.ui.leanback)
implementation(libs.androidx.navigation.common)
implementation(libs.androidx.navigation.compose)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,26 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.media3.session.MediaSession
import ch.srgssr.pillarbox.demo.shared.data.DemoItem
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.demo.tv.ui.player.compose.PlayerView
import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme
import ch.srgssr.pillarbox.player.PillarboxPlayer
import ch.srgssr.pillarbox.player.PillarboxExoPlayer
import ch.srgssr.pillarbox.player.session.PillarboxMediaSession

/**
* Player activity
*
* @constructor Create empty Player activity
*/
class PlayerActivity : ComponentActivity() {
private lateinit var player: PillarboxPlayer
private lateinit var mediaSession: MediaSession
private lateinit var player: PillarboxExoPlayer
private lateinit var mediaSession: PillarboxMediaSession

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
player = PlayerModule.provideDefaultPlayer(this)
mediaSession = MediaSession.Builder(this, player)
mediaSession = PillarboxMediaSession.Builder(this, player)
.build()
val demoItem = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getSerializableExtra(ARG_ITEM, DemoItem::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import androidx.media3.ui.leanback.LeanbackPlayerAdapter
import ch.srgssr.pillarbox.core.business.SRGErrorMessageProvider
import ch.srgssr.pillarbox.demo.shared.data.DemoItem
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.player.PillarboxPlayer
import ch.srgssr.pillarbox.player.PillarboxExoPlayer
import ch.srgssr.pillarbox.player.currentMediaMetadataAsFlow
import ch.srgssr.pillarbox.player.extension.setHandleAudioFocus
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

Expand All @@ -33,10 +34,10 @@ private const val UpdateInterval = 1_000
* Lot of work is still needed to have a good player experience.
*/
class LeanbackPlayerFragment : VideoSupportFragment() {
private lateinit var player: PillarboxPlayer
private lateinit var player: PillarboxExoPlayer

/**
* Set demo item to [PillarboxPlayer]
* Set demo item to [PillarboxExoPlayer]
*
* @param demoItem
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,29 @@ import androidx.media3.session.MediaSession
import ch.srgssr.pillarbox.demo.shared.data.DemoBrowser
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.demo.ui.showcases.integrations.MediaControllerActivity
import ch.srgssr.pillarbox.player.service.PillarboxMediaLibraryService
import ch.srgssr.pillarbox.player.session.PillarboxMediaLibraryService
import ch.srgssr.pillarbox.player.session.PillarboxMediaLibrarySession
import ch.srgssr.pillarbox.player.session.PillarboxMediaSession
import ch.srgssr.pillarbox.player.utils.PendingIntentUtils
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import okhttp3.internal.toImmutableList

/**
* Demo media session service to handle background playback has Media3 would us to use.
* Can be still useful when using with MediaLibrary for android auto.
*
* Limitations :
* - No custom data access from MediaController so no MediaComposition or other custom attributes integrator wants.
* The only way to handle an Android Auto application.
*
* Hints for testing : https://developer.android.com/training/cars/testing
*
* @constructor Create empty Demo media session service
*/
class DemoMediaLibraryService : PillarboxMediaLibraryService() {

private lateinit var demoBrowser: DemoBrowser

override fun onCreate() {
super.onCreate()
Log.d(TAG, "onCreate")
val player = PlayerModule.provideDefaultPlayer(this)
setPlayer(player, DemoCallback())

demoBrowser = DemoBrowser()
val player = PlayerModule.provideDefaultPlayer(this)
setPlayer(player = player, callback = DemoCallback(), sessionId = "AndroidAutoSession")
}

override fun sessionActivity(): PendingIntent {
Expand All @@ -56,78 +50,62 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() {
)
}

override fun onDestroy() {
Log.d(TAG, "onDestroy")
super.onDestroy()
}

private inner class DemoCallback : MediaLibrarySession.Callback {
/**
* Demo callback is used by Android Auto to create the navigation.
*/
private inner class DemoCallback : PillarboxMediaLibrarySession.Callback {
override fun onGetLibraryRoot(
session: MediaLibrarySession,
session: PillarboxMediaLibrarySession,
browser: MediaSession.ControllerInfo,
params: LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> {
Log.d(TAG, "onGetLibraryRoot")
val rootExtras = Bundle().apply {
putBoolean(MEDIA_SEARCH_SUPPORTED, false)
putBoolean(CONTENT_STYLE_SUPPORTED, true)
putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID)
putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST)
}
Log.d(TAG, "onGetLibraryRoot isSuggested = ${params?.isSuggested} isRecent = ${params?.isRecent}")
val libraryParams = LibraryParams.Builder().setExtras(rootExtras).build()
return Futures.immediateFuture(LibraryResult.ofItem(demoBrowser.rootMediaItem, libraryParams))
}

override fun onGetChildren(
session: MediaLibrarySession,
session: PillarboxMediaLibrarySession,
browser: MediaSession.ControllerInfo,
parentId: String,
page: Int,
pageSize: Int,
params: LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
Log.d(TAG, "onGetChildren($parentId)")
return demoBrowser.getChildren(parentId)?.let {
Futures.immediateFuture(LibraryResult.ofItemList(it.toImmutableList(), LibraryParams.Builder().build()))
} ?: super.onGetChildren(session, browser, parentId, page, pageSize, params)
}

override fun onGetItem(
session: MediaLibrarySession,
session: PillarboxMediaLibrarySession,
browser: MediaSession.ControllerInfo,
mediaId: String
): ListenableFuture<LibraryResult<MediaItem>> {
Log.d(TAG, "onGetItem $mediaId")
val mediaItem = demoBrowser.getMediaItemFromId(mediaId) ?: MediaItem.EMPTY
return Futures.immediateFuture(
LibraryResult.ofItem(
demoBrowser.getMediaItemFromId(mediaId) ?: MediaItem.EMPTY, LibraryParams.Builder().build()
)
LibraryResult.ofItem(mediaItem, LibraryParams.Builder().build())
)
}

override fun onAddMediaItems(
mediaSession: MediaSession,
mediaSession: PillarboxMediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: MutableList<MediaItem>
): ListenableFuture<MutableList<MediaItem>> {
Log.d(TAG, "onAddMediaItems")
/*
* MediaItem from Browser are directly the one we want to play.
* For MediaItem with only id, like urn, it is fine. But one with uri not, as the localConfiguration is null here.
* We have to get the orignal mediaItem with uri set.
* We have to get the original mediaItem with uri set.
*/
return Futures.immediateFuture(mediaItems.map { demoBrowser.getMediaItemFromId(it.mediaId) ?: it }.toMutableList())
}

override fun onSearch(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
params: LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
Log.d(TAG, "onSearch: $query")
return super.onSearch(session, browser, query, params)
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import android.app.PendingIntent
import android.content.Intent
import android.util.Log
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import ch.srgssr.pillarbox.demo.shared.data.DemoItem
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.demo.ui.showcases.integrations.MediaControllerActivity
import ch.srgssr.pillarbox.player.service.PillarboxMediaSessionService
import ch.srgssr.pillarbox.player.extension.setHandleAudioFocus
import ch.srgssr.pillarbox.player.session.PillarboxMediaSessionService
import ch.srgssr.pillarbox.player.utils.PendingIntentUtils

/**
Expand All @@ -31,21 +30,12 @@ class DemoMediaSessionService : PillarboxMediaSessionService() {
super.onCreate()
Log.d(TAG, "onCreate")
val player = PlayerModule.provideDefaultPlayer(this)
// TODO add item elsewhere
player.setMediaItems(
listOf(
MediaItem.Builder().setMediaId("urn:rts:video:6820736").build(),
MediaItem.Builder().setMediaId("urn:rts:video:8393241").build(),
DemoItem(title = "Swiss cheese fondue", uri = "https://swi-vod.akamaized.net/videoJson/47603186/master.m3u8").toMediaItem(),
MediaItem.Builder().setMediaId("urn:rts:video:3608506").build(),
)
)
player.setWakeMode(C.WAKE_MODE_NETWORK)
player.setHandleAudioFocus(true)
player.prepare()
player.play()

setPlayer(player)
setPlayer(player = player, sessionId = "DemoMediaSession")
}

override fun sessionActivity(): PendingIntent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.media3.common.Timeline
import androidx.media3.common.VideoSize
import ch.srgssr.pillarbox.demo.shared.data.DemoItem
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.player.extension.setHandleAudioFocus
import ch.srgssr.pillarbox.player.extension.toRational
import kotlinx.coroutines.flow.MutableStateFlow
import java.net.URL
Expand Down
Loading
Loading