From 6f6e0be2af872fa9fb8980e081e09ac7342aeb39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Tue, 26 Mar 2024 17:23:56 +0100 Subject: [PATCH 01/27] Hide implementation behind interfaces --- .../core/business/DefaultPillarbox.kt | 18 +- .../pillarbox/demo/shared/di/PlayerModule.kt | 6 +- .../demo/tv/ui/player/PlayerActivity.kt | 4 +- .../player/leanback/LeanbackPlayerFragment.kt | 6 +- .../demo/service/DemoMediaSessionService.kt | 11 - .../demo/ui/showcases/layouts/SimpleStory.kt | 4 +- .../ui/showcases/layouts/StoryViewModel.kt | 6 +- .../player/IsPlayingAllTypeOfContentTest.kt | 5 +- .../pillarbox/player/PillarboxExoPlayer.kt | 28 +- .../pillarbox/player/PillarboxPlayer.kt | 401 +----------------- .../player/exoplayer/PillarboxExoPlayer.kt | 399 +++++++++++++++++ .../service/PillarboxMediaLibraryService.kt | 6 +- .../service/PillarboxMediaSessionService.kt | 5 +- .../player/service/PlaybackService.kt | 8 +- ....kt => PillarboxExoPlayerMediaItemTest.kt} | 7 +- .../TestIsPlaybackSpeedPossibleAtPosition.kt | 1 + ...=> TestPillarboxExoPlayerPlaybackSpeed.kt} | 7 +- .../player/tracker/MediaItemTrackerTest.kt | 6 +- 18 files changed, 471 insertions(+), 457 deletions(-) create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/exoplayer/PillarboxExoPlayer.kt rename pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/{PillarboxPlayerMediaItemTest.kt => PillarboxExoPlayerMediaItemTest.kt} (96%) rename pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/{TestPillarboxPlayerPlaybackSpeed.kt => TestPillarboxExoPlayerPlaybackSpeed.kt} (95%) diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/DefaultPillarbox.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/DefaultPillarbox.kt index 7325e0274..2b4cc1861 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/DefaultPillarbox.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/DefaultPillarbox.kt @@ -13,28 +13,28 @@ 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 convenient class to create [PillarboxExoPlayer] that suit 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, @@ -42,7 +42,7 @@ object DefaultPillarbox { mediaItemTrackerRepository: MediaItemTrackerProvider = DefaultMediaItemTrackerRepository(), mediaCompositionService: MediaCompositionService = HttpMediaCompositionService(), loadControl: LoadControl = PillarboxLoadControl(), - ): PillarboxPlayer { + ): PillarboxExoPlayer { return DefaultPillarbox( context = context, seekIncrement = seekIncrement, @@ -54,7 +54,7 @@ object DefaultPillarbox { } /** - * Invoke create an instance of [PillarboxPlayer] + * Invoke create an instance of [PillarboxExoPlayer] * * @param context The context. * @param seekIncrement The seek increment. @@ -62,7 +62,7 @@ object DefaultPillarbox { * @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( @@ -72,8 +72,8 @@ object DefaultPillarbox { loadControl: LoadControl = DefaultLoadControl(), mediaCompositionService: MediaCompositionService = HttpMediaCompositionService(), clock: Clock, - ): PillarboxPlayer { - return PillarboxPlayer( + ): PillarboxExoPlayer { + return ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer( context = context, seekIncrement = seekIncrement, mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt index 940f00927..5257535f0 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt @@ -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.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import java.net.URL @@ -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)) diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt index 041f7406e..bd88964e6 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt @@ -17,7 +17,7 @@ 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.exoplayer.PillarboxExoPlayer /** * Player activity @@ -25,7 +25,7 @@ import ch.srgssr.pillarbox.player.PillarboxPlayer * @constructor Create empty Player activity */ class PlayerActivity : ComponentActivity() { - private lateinit var player: PillarboxPlayer + private lateinit var player: PillarboxExoPlayer private lateinit var mediaSession: MediaSession override fun onCreate(savedInstanceState: Bundle?) { diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt index d91d209df..b9b1f4cba 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt @@ -19,8 +19,8 @@ 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.currentMediaMetadataAsFlow +import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -33,10 +33,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 */ diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt index c024dc5ed..b62682538 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt @@ -8,8 +8,6 @@ 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 @@ -31,15 +29,6 @@ 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() diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/SimpleStory.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/SimpleStory.kt index 2ce51b54a..630862c7d 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/SimpleStory.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/SimpleStory.kt @@ -24,7 +24,7 @@ import androidx.media3.common.Player import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.shared.data.Playlist import ch.srgssr.pillarbox.demo.shared.di.PlayerModule -import ch.srgssr.pillarbox.player.PillarboxPlayer +import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.ui.ScaleMode import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface @@ -57,7 +57,7 @@ fun SimpleStory() { /** * Simple story player - * Each DemoItem have a [PillarboxPlayer], the player is released onDispose + * Each DemoItem have a [PillarboxExoPlayer], the player is released onDispose * * @param demoItem The DemoItem to play * @param isPlaying to pause or play the player diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt index 183afdc27..240414de3 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt @@ -10,7 +10,7 @@ import androidx.media3.common.C import androidx.media3.common.Player import ch.srgssr.pillarbox.demo.shared.data.Playlist import ch.srgssr.pillarbox.demo.shared.di.PlayerModule -import ch.srgssr.pillarbox.player.PillarboxPlayer +import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import kotlin.math.ceil /** @@ -48,9 +48,9 @@ class StoryViewModel(application: Application) : AndroidViewModel(application) { * Get player for page number * * @param pageNumber - * @return [PillarboxPlayer] that should be used for this [pageNumber] + * @return [PillarboxExoPlayer] that should be used for this [pageNumber] */ - fun getPlayerForPageNumber(pageNumber: Int): PillarboxPlayer { + fun getPlayerForPageNumber(pageNumber: Int): PillarboxExoPlayer { return players[playerIndex(pageNumber)] } diff --git a/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt b/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt index 5d555a11f..af0f7967e 100644 --- a/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt +++ b/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt @@ -10,6 +10,7 @@ import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.util.ConditionVariable import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.player.utils.ContentUrls import org.junit.Assert import org.junit.Test @@ -31,10 +32,10 @@ class IsPlayingAllTypeOfContentTest { fun isPlayingTest() { // Context of the app under test. val appContext = getInstrumentation().targetContext - val atomicPlayer = AtomicReference() + val atomicPlayer = AtomicReference() val waitIsPlaying = WaitIsPlaying() getInstrumentation().runOnMainSync { - val player = PillarboxPlayer( + val player = PillarboxExoPlayer( appContext ) atomicPlayer.set(player) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt index 77327ef66..423a518c2 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt @@ -4,35 +4,17 @@ */ package ch.srgssr.pillarbox.player -import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.SeekParameters /** * Pillarbox [ExoPlayer] interface extension. */ -interface PillarboxExoPlayer : ExoPlayer { - +interface PillarboxExoPlayer : PillarboxPlayer, ExoPlayer { /** - * Listener + * Handle audio focus with currently set AudioAttributes + * @param handleAudioFocus true if the player should handle audio focus, false otherwise. */ - interface Listener : Player.Listener { - /** - * On smooth seeking enabled changed - * - * @param smoothSeekingEnabled The new value of [smoothSeekingEnabled] - */ - fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) + fun setHandleAudioFocus(handleAudioFocus: Boolean) { + setAudioAttributes(audioAttributes, handleAudioFocus) } - - /** - * Smooth seeking enabled - * - * When [smoothSeekingEnabled] is true, next seek events is send only after the current is done. - * - * To have the best result it is important to - * 1) Pause the player while seeking. - * 2) Set the [ExoPlayer.setSeekParameters] to [SeekParameters.CLOSEST_SYNC]. - */ - var smoothSeekingEnabled: Boolean } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt index b9e5738c1..f4629c473 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt @@ -4,400 +4,39 @@ */ package ch.srgssr.pillarbox.player -import android.content.Context -import androidx.annotation.VisibleForTesting -import androidx.media3.common.MediaItem -import androidx.media3.common.PlaybackException -import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player -import androidx.media3.common.Timeline.Window -import androidx.media3.common.TrackSelectionParameters -import androidx.media3.common.util.Clock -import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.LoadControl -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector -import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter -import androidx.media3.exoplayer.util.EventLogger -import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed -import ch.srgssr.pillarbox.player.extension.setPreferredAudioRoleFlagsToAccessibilityManagerSettings -import ch.srgssr.pillarbox.player.extension.setSeekIncrements -import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory -import ch.srgssr.pillarbox.player.tracker.AnalyticsMediaItemTracker -import ch.srgssr.pillarbox.player.tracker.CurrentMediaItemTagTracker -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository +import androidx.media3.exoplayer.SeekParameters /** - * Pillarbox player - * - * @param exoPlayer - * @param mediaItemTrackerProvider - * - * @constructor + * Pillarbox [Player] interface extension. */ -class PillarboxPlayer internal constructor( - private val exoPlayer: ExoPlayer, - mediaItemTrackerProvider: MediaItemTrackerProvider, -) : ExoPlayer by exoPlayer, PillarboxExoPlayer { - private val listeners = HashSet() - private val itemTagTracker = CurrentMediaItemTagTracker(this) - private val analyticsTracker = AnalyticsMediaItemTracker(this, mediaItemTrackerProvider) - private val window = Window() - override var smoothSeekingEnabled: Boolean = false - set(value) { - if (value != field) { - field = value - if (!value) { - seekEnd() - } - clearSeeking() - val listeners = HashSet(listeners) - for (listener in listeners) { - listener.onSmoothSeekingEnabledChanged(value) - } - } - } - private var pendingSeek: Long? = null - private var isSeeking: Boolean = false - +interface PillarboxPlayer : Player { /** - * Enable or disable analytics tracking for the current [MediaItem]. + * Listener */ - var trackingEnabled: Boolean - get() = analyticsTracker.enabled - set(value) { - analyticsTracker.enabled = value - } - - init { - exoPlayer.addListener(ComponentListener()) - - itemTagTracker.addCallback(analyticsTracker) - - if (BuildConfig.DEBUG) { - addAnalyticsListener(EventLogger()) - } - } - - constructor( - context: Context, - mediaSourceFactory: PillarboxMediaSourceFactory = PillarboxMediaSourceFactory(context), - loadControl: LoadControl = PillarboxLoadControl(), - mediaItemTrackerProvider: MediaItemTrackerProvider = MediaItemTrackerRepository(), - seekIncrement: SeekIncrement = SeekIncrement() - ) : this( - context = context, - mediaSourceFactory = mediaSourceFactory, - loadControl = loadControl, - mediaItemTrackerProvider = mediaItemTrackerProvider, - seekIncrement = seekIncrement, - clock = Clock.DEFAULT, - ) - - @VisibleForTesting - constructor( - context: Context, - mediaSourceFactory: PillarboxMediaSourceFactory = PillarboxMediaSourceFactory(context), - loadControl: LoadControl = PillarboxLoadControl(), - mediaItemTrackerProvider: MediaItemTrackerProvider = MediaItemTrackerRepository(), - seekIncrement: SeekIncrement = SeekIncrement(), - clock: Clock, - ) : this( - ExoPlayer.Builder(context) - .setClock(clock) - .setUsePlatformDiagnostics(false) - .setSeekIncrements(seekIncrement) - .setRenderersFactory( - DefaultRenderersFactory(context) - .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF) - .setEnableDecoderFallback(true) - ) - .setBandwidthMeter(DefaultBandwidthMeter.getSingletonInstance(context)) - .setLoadControl(loadControl) - .setMediaSourceFactory(mediaSourceFactory) - .setTrackSelector( - DefaultTrackSelector( - context, - TrackSelectionParameters.Builder(context) - .setPreferredAudioRoleFlagsToAccessibilityManagerSettings(context) - .build() - ) - ) - .setDeviceVolumeControlEnabled(true) // allow player to control device volume - .build(), - mediaItemTrackerProvider = mediaItemTrackerProvider - ) - - override fun addListener(listener: Player.Listener) { - exoPlayer.addListener(listener) - if (listener is PillarboxExoPlayer.Listener) { - listeners.add(listener) - } - } - - override fun removeListener(listener: Player.Listener) { - exoPlayer.removeListener(listener) - if (listener is PillarboxExoPlayer.Listener) { - listeners.remove(listener) - } - } - - override fun setMediaItem(mediaItem: MediaItem) { - exoPlayer.setMediaItem(mediaItem.clearTag()) - } - - override fun setMediaItem(mediaItem: MediaItem, resetPosition: Boolean) { - exoPlayer.setMediaItem(mediaItem.clearTag(), resetPosition) - } - - override fun setMediaItem(mediaItem: MediaItem, startPositionMs: Long) { - exoPlayer.setMediaItem(mediaItem.clearTag(), startPositionMs) - } - - override fun setMediaItems(mediaItems: List) { - exoPlayer.setMediaItems(mediaItems.map { it.clearTag() }) - } - - override fun setMediaItems(mediaItems: List, resetPosition: Boolean) { - exoPlayer.setMediaItems(mediaItems.map { it.clearTag() }, resetPosition) - } - - override fun setMediaItems(mediaItems: List, startIndex: Int, startPositionMs: Long) { - exoPlayer.setMediaItems(mediaItems.map { it.clearTag() }, startIndex, startPositionMs) - } - - override fun addMediaItem(mediaItem: MediaItem) { - exoPlayer.addMediaItem(mediaItem.clearTag()) - } - - override fun addMediaItem(index: Int, mediaItem: MediaItem) { - exoPlayer.addMediaItem(index, mediaItem.clearTag()) - } - - override fun addMediaItems(mediaItems: List) { - exoPlayer.addMediaItems(mediaItems.map { it.clearTag() }) - } - - override fun addMediaItems(index: Int, mediaItems: List) { - exoPlayer.addMediaItems(index, mediaItems.map { it.clearTag() }) - } - - override fun replaceMediaItem(index: Int, mediaItem: MediaItem) { - exoPlayer.replaceMediaItem(index, mediaItem.clearTag()) - } - - override fun replaceMediaItems(fromIndex: Int, toIndex: Int, mediaItems: List) { - exoPlayer.replaceMediaItems(fromIndex, toIndex, mediaItems.map { it.clearTag() }) - } - - override fun seekTo(positionMs: Long) { - if (!smoothSeekingEnabled) { - exoPlayer.seekTo(positionMs) - return - } - smoothSeekTo(positionMs) - } - - private fun smoothSeekTo(positionMs: Long) { - if (isSeeking) { - pendingSeek = positionMs - return - } - isSeeking = true - exoPlayer.seekTo(positionMs) - } - - override fun seekTo(mediaItemIndex: Int, positionMs: Long) { - if (!smoothSeekingEnabled) { - exoPlayer.seekTo(mediaItemIndex, positionMs) - return - } - smoothSeekTo(mediaItemIndex, positionMs) - } - - private fun smoothSeekTo(mediaItemIndex: Int, positionMs: Long) { - if (mediaItemIndex != currentMediaItemIndex) { - clearSeeking() - exoPlayer.seekTo(mediaItemIndex, positionMs) - return - } - if (isSeeking) { - pendingSeek = positionMs - return - } - exoPlayer.seekTo(mediaItemIndex, positionMs) - } - - override fun seekToDefaultPosition() { - clearSeeking() - exoPlayer.seekToDefaultPosition() - } - - override fun seekToDefaultPosition(mediaItemIndex: Int) { - clearSeeking() - exoPlayer.seekToDefaultPosition(mediaItemIndex) - } - - override fun seekBack() { - clearSeeking() - exoPlayer.seekBack() - } - - override fun seekForward() { - clearSeeking() - exoPlayer.seekForward() - } - - override fun seekToNext() { - clearSeeking() - exoPlayer.seekToNext() - } - - override fun seekToPrevious() { - clearSeeking() - exoPlayer.seekToPrevious() - } - - override fun seekToNextMediaItem() { - clearSeeking() - exoPlayer.seekToNextMediaItem() - } - - override fun seekToPreviousMediaItem() { - clearSeeking() - exoPlayer.seekToPreviousMediaItem() + interface Listener : Player.Listener { + /** + * On smooth seeking enabled changed + * + * @param smoothSeekingEnabled The new value of [smoothSeekingEnabled] + */ + fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) } /** - * Releases the player. - * This method must be called when the player is no longer required. The player must not be used after calling this method. + * Smooth seeking enabled + * + * When [smoothSeekingEnabled] is true, next seek events is send only after the current is done. * - * Release call automatically [stop] if the player is not in [Player.STATE_IDLE]. + * To have the best result it is important to + * 1) Pause the player while seeking. + * 2) Set the [ExoPlayer.setSeekParameters] to [SeekParameters.CLOSEST_SYNC]. */ - override fun release() { - clearSeeking() - if (playbackState != Player.STATE_IDLE) { - stop() - } - exoPlayer.release() - } + var smoothSeekingEnabled: Boolean /** - * Handle audio focus with currently set AudioAttributes - * @param handleAudioFocus true if the player should handle audio focus, false otherwise. + * Enable or disable MediaItem tracking */ - fun setHandleAudioFocus(handleAudioFocus: Boolean) { - setAudioAttributes(audioAttributes, handleAudioFocus) - } - - override fun setPlaybackParameters(playbackParameters: PlaybackParameters) { - if (isPlaybackSpeedPossibleAtPosition(currentPosition, playbackParameters.speed, window)) { - exoPlayer.playbackParameters = playbackParameters - } else { - exoPlayer.playbackParameters = playbackParameters.withSpeed(NormalSpeed) - } - } - - override fun setPlaybackSpeed(speed: Float) { - playbackParameters = playbackParameters.withSpeed(speed) - } - - private fun seekEnd() { - isSeeking = false - pendingSeek?.let { pendingPosition -> - pendingSeek = null - seekTo(pendingPosition) - } - } - - private fun clearSeeking() { - isSeeking = false - pendingSeek = null - } - - private inner class ComponentListener : Player.Listener { - private val window = Window() - - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - clearSeeking() - } - - override fun onRenderedFirstFrame() { - seekEnd() - } - - override fun onPlaybackStateChanged(playbackState: Int) { - when (playbackState) { - Player.STATE_READY -> { - if (isSeeking) { - seekEnd() - } - } - - Player.STATE_IDLE, Player.STATE_ENDED -> { - clearSeeking() - } - - Player.STATE_BUFFERING -> { - // Do nothing - } - } - } - - override fun onPlayerError(error: PlaybackException) { - clearSeeking() - if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) { - setPlaybackSpeed(NormalSpeed) - seekToDefaultPosition() - prepare() - } - } - - override fun onEvents(player: Player, events: Player.Events) { - if (!player.isCurrentMediaItemLive || player.getPlaybackSpeed() == NormalSpeed) return - if (!player.isCurrentMediaItemSeekable) { - setPlaybackSpeed(NormalSpeed) - return - } - player.currentTimeline.getWindow(currentMediaItemIndex, window) - if (window.isAtDefaultPosition(currentPosition) && getPlaybackSpeed() > NormalSpeed) { - exoPlayer.setPlaybackSpeed(NormalSpeed) - } - } - } -} - -/** - * Return if the playback [speed] is possible at [position]. - * Always return true for none live content or if [Player.getCurrentTimeline] is empty. - * - * @param position The position to test the playback speed. - * @param speed The playback speed - * @param window optional window for performance purpose - * @return true if the playback [speed] can be set at [position] - */ -fun Player.isPlaybackSpeedPossibleAtPosition(position: Long, speed: Float, window: Window = Window()): Boolean { - if (currentTimeline.isEmpty || speed == NormalSpeed || !isCurrentMediaItemLive) { - return true - } - currentTimeline.getWindow(currentMediaItemIndex, window) - return window.isPlaybackSpeedPossibleAtPosition(position, speed) -} - -internal fun Window.isPlaybackSpeedPossibleAtPosition(positionMs: Long, playbackSpeed: Float): Boolean { - return when { - !isLive() || playbackSpeed == NormalSpeed -> true - !isSeekable -> false - isAtDefaultPosition(positionMs) && playbackSpeed > NormalSpeed -> false - else -> true - } -} - -internal fun Window.isAtDefaultPosition(positionMs: Long): Boolean { - return positionMs >= defaultPositionMs + var trackingEnabled: Boolean } - -private const val NormalSpeed = 1.0f - -private fun MediaItem.clearTag() = this.buildUpon().setTag(null).build() diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/exoplayer/PillarboxExoPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/exoplayer/PillarboxExoPlayer.kt new file mode 100644 index 000000000..03abb3ba0 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/exoplayer/PillarboxExoPlayer.kt @@ -0,0 +1,399 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.exoplayer + +import android.content.Context +import android.util.Log +import androidx.annotation.VisibleForTesting +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.PlaybackParameters +import androidx.media3.common.Player +import androidx.media3.common.Timeline.Window +import androidx.media3.common.TrackSelectionParameters +import androidx.media3.common.util.Clock +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.LoadControl +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector +import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter +import androidx.media3.exoplayer.util.EventLogger +import ch.srgssr.pillarbox.player.BuildConfig +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.extension.getPlaybackSpeed +import ch.srgssr.pillarbox.player.extension.setPreferredAudioRoleFlagsToAccessibilityManagerSettings +import ch.srgssr.pillarbox.player.extension.setSeekIncrements +import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory +import ch.srgssr.pillarbox.player.tracker.CurrentMediaItemTracker +import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider +import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository + +/** + * Pillarbox player + * + * @param exoPlayer + * @param mediaItemTrackerProvider + * + * @constructor + */ +class PillarboxExoPlayer internal constructor( + private val exoPlayer: ExoPlayer, + mediaItemTrackerProvider: MediaItemTrackerProvider? +) : PillarboxExoPlayer, ExoPlayer by exoPlayer { + private val listeners = HashSet() + private val itemTracker: CurrentMediaItemTracker? + private val window = Window() + override var smoothSeekingEnabled: Boolean = false + set(value) { + Log.d("Coucou", "PillarboxExoPlayer smoothSeekingEnabled to $value") + if (value != field) { + field = value + if (!value) { + seekEnd() + } + clearSeeking() + val listeners = HashSet(listeners) + for (listener in listeners) { + listener.onSmoothSeekingEnabledChanged(value) + } + } + } + private var pendingSeek: Long? = null + private var isSeeking: Boolean = false + + /** + * Enable or disable MediaItem tracking + */ + + override var trackingEnabled: Boolean + set(value) = itemTracker?.let { it.enabled = value } ?: Unit + get() = itemTracker?.enabled ?: false + + init { + exoPlayer.addListener(ComponentListener()) + itemTracker = mediaItemTrackerProvider?.let { + CurrentMediaItemTracker(this, it) + } + if (BuildConfig.DEBUG) { + addAnalyticsListener(EventLogger()) + } + } + + constructor( + context: Context, + mediaSourceFactory: PillarboxMediaSourceFactory = PillarboxMediaSourceFactory(context), + loadControl: LoadControl = PillarboxLoadControl(), + mediaItemTrackerProvider: MediaItemTrackerProvider = MediaItemTrackerRepository(), + seekIncrement: SeekIncrement = SeekIncrement() + ) : this( + context = context, + mediaSourceFactory = mediaSourceFactory, + loadControl = loadControl, + mediaItemTrackerProvider = mediaItemTrackerProvider, + seekIncrement = seekIncrement, + clock = Clock.DEFAULT, + ) + + @VisibleForTesting + constructor( + context: Context, + mediaSourceFactory: PillarboxMediaSourceFactory = PillarboxMediaSourceFactory(context), + loadControl: LoadControl = PillarboxLoadControl(), + mediaItemTrackerProvider: MediaItemTrackerProvider = MediaItemTrackerRepository(), + seekIncrement: SeekIncrement = SeekIncrement(), + clock: Clock, + ) : this( + ExoPlayer.Builder(context) + .setClock(clock) + .setUsePlatformDiagnostics(false) + .setSeekIncrements(seekIncrement) + .setRenderersFactory( + DefaultRenderersFactory(context) + .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF) + .setEnableDecoderFallback(true) + ) + .setBandwidthMeter(DefaultBandwidthMeter.getSingletonInstance(context)) + .setLoadControl(loadControl) + .setMediaSourceFactory(mediaSourceFactory) + .setTrackSelector( + DefaultTrackSelector( + context, + TrackSelectionParameters.Builder(context) + .setPreferredAudioRoleFlagsToAccessibilityManagerSettings(context) + .build() + ) + ) + .setDeviceVolumeControlEnabled(true) // allow player to control device volume + .build(), + mediaItemTrackerProvider = mediaItemTrackerProvider + ) + + override fun addListener(listener: Player.Listener) { + exoPlayer.addListener(listener) + if (listener is PillarboxPlayer.Listener) { + listeners.add(listener) + } + } + + override fun removeListener(listener: Player.Listener) { + exoPlayer.removeListener(listener) + if (listener is PillarboxPlayer.Listener) { + listeners.remove(listener) + } + } + + override fun setMediaItem(mediaItem: MediaItem) { + exoPlayer.setMediaItem(mediaItem.clearTag()) + } + + override fun setMediaItem(mediaItem: MediaItem, resetPosition: Boolean) { + exoPlayer.setMediaItem(mediaItem.clearTag(), resetPosition) + } + + override fun setMediaItem(mediaItem: MediaItem, startPositionMs: Long) { + exoPlayer.setMediaItem(mediaItem.clearTag(), startPositionMs) + } + + override fun setMediaItems(mediaItems: List) { + exoPlayer.setMediaItems(mediaItems.map { it.clearTag() }) + } + + override fun setMediaItems(mediaItems: List, resetPosition: Boolean) { + exoPlayer.setMediaItems(mediaItems.map { it.clearTag() }, resetPosition) + } + + override fun setMediaItems(mediaItems: List, startIndex: Int, startPositionMs: Long) { + exoPlayer.setMediaItems(mediaItems.map { it.clearTag() }, startIndex, startPositionMs) + } + + override fun addMediaItem(mediaItem: MediaItem) { + exoPlayer.addMediaItem(mediaItem.clearTag()) + } + + override fun addMediaItem(index: Int, mediaItem: MediaItem) { + exoPlayer.addMediaItem(index, mediaItem.clearTag()) + } + + override fun addMediaItems(mediaItems: List) { + exoPlayer.addMediaItems(mediaItems.map { it.clearTag() }) + } + + override fun addMediaItems(index: Int, mediaItems: List) { + exoPlayer.addMediaItems(index, mediaItems.map { it.clearTag() }) + } + + override fun replaceMediaItem(index: Int, mediaItem: MediaItem) { + exoPlayer.replaceMediaItem(index, mediaItem.clearTag()) + } + + override fun replaceMediaItems(fromIndex: Int, toIndex: Int, mediaItems: List) { + exoPlayer.replaceMediaItems(fromIndex, toIndex, mediaItems.map { it.clearTag() }) + } + + override fun seekTo(positionMs: Long) { + if (!smoothSeekingEnabled) { + exoPlayer.seekTo(positionMs) + return + } + smoothSeekTo(positionMs) + } + + private fun smoothSeekTo(positionMs: Long) { + if (isSeeking) { + pendingSeek = positionMs + return + } + isSeeking = true + exoPlayer.seekTo(positionMs) + } + + override fun seekTo(mediaItemIndex: Int, positionMs: Long) { + if (!smoothSeekingEnabled) { + exoPlayer.seekTo(mediaItemIndex, positionMs) + return + } + smoothSeekTo(mediaItemIndex, positionMs) + } + + private fun smoothSeekTo(mediaItemIndex: Int, positionMs: Long) { + if (mediaItemIndex != currentMediaItemIndex) { + clearSeeking() + exoPlayer.seekTo(mediaItemIndex, positionMs) + return + } + if (isSeeking) { + pendingSeek = positionMs + return + } + exoPlayer.seekTo(mediaItemIndex, positionMs) + } + + override fun seekToDefaultPosition() { + clearSeeking() + exoPlayer.seekToDefaultPosition() + } + + override fun seekToDefaultPosition(mediaItemIndex: Int) { + clearSeeking() + exoPlayer.seekToDefaultPosition(mediaItemIndex) + } + + override fun seekBack() { + clearSeeking() + exoPlayer.seekBack() + } + + override fun seekForward() { + clearSeeking() + exoPlayer.seekForward() + } + + override fun seekToNext() { + clearSeeking() + exoPlayer.seekToNext() + } + + override fun seekToPrevious() { + clearSeeking() + exoPlayer.seekToPrevious() + } + + override fun seekToNextMediaItem() { + clearSeeking() + exoPlayer.seekToNextMediaItem() + } + + override fun seekToPreviousMediaItem() { + clearSeeking() + exoPlayer.seekToPreviousMediaItem() + } + + /** + * Releases the player. + * This method must be called when the player is no longer required. The player must not be used after calling this method. + * + * Release call automatically [stop] if the player is not in [Player.STATE_IDLE]. + */ + override fun release() { + clearSeeking() + if (playbackState != Player.STATE_IDLE) { + stop() + } + exoPlayer.release() + } + + override fun setPlaybackParameters(playbackParameters: PlaybackParameters) { + if (isPlaybackSpeedPossibleAtPosition(currentPosition, playbackParameters.speed, window)) { + exoPlayer.playbackParameters = playbackParameters + } else { + exoPlayer.playbackParameters = playbackParameters.withSpeed(NormalSpeed) + } + } + + override fun setPlaybackSpeed(speed: Float) { + playbackParameters = playbackParameters.withSpeed(speed) + } + + private fun seekEnd() { + isSeeking = false + pendingSeek?.let { pendingPosition -> + pendingSeek = null + seekTo(pendingPosition) + } + } + + private fun clearSeeking() { + isSeeking = false + pendingSeek = null + } + + private inner class ComponentListener : Player.Listener { + private val window = Window() + + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + clearSeeking() + } + + override fun onRenderedFirstFrame() { + seekEnd() + } + + override fun onPlaybackStateChanged(playbackState: Int) { + when (playbackState) { + Player.STATE_READY -> { + if (isSeeking) { + seekEnd() + } + } + + Player.STATE_IDLE, Player.STATE_ENDED -> { + clearSeeking() + } + + Player.STATE_BUFFERING -> { + // Do nothing + } + } + } + + override fun onPlayerError(error: PlaybackException) { + clearSeeking() + if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) { + setPlaybackSpeed(NormalSpeed) + seekToDefaultPosition() + prepare() + } + } + + override fun onEvents(player: Player, events: Player.Events) { + if (!player.isCurrentMediaItemLive || player.getPlaybackSpeed() == NormalSpeed) return + if (!player.isCurrentMediaItemSeekable) { + setPlaybackSpeed(NormalSpeed) + return + } + player.currentTimeline.getWindow(currentMediaItemIndex, window) + if (window.isAtDefaultPosition(currentPosition) && getPlaybackSpeed() > NormalSpeed) { + exoPlayer.setPlaybackSpeed(NormalSpeed) + } + } + } +} + +/** + * Return if the playback [speed] is possible at [position]. + * Always return true for none live content or if [Player.getCurrentTimeline] is empty. + * + * @param position The position to test the playback speed. + * @param speed The playback speed + * @param window optional window for performance purpose + * @return true if the playback [speed] can be set at [position] + */ +fun Player.isPlaybackSpeedPossibleAtPosition(position: Long, speed: Float, window: Window = Window()): Boolean { + if (currentTimeline.isEmpty || speed == NormalSpeed || !isCurrentMediaItemLive) { + return true + } + currentTimeline.getWindow(currentMediaItemIndex, window) + return window.isPlaybackSpeedPossibleAtPosition(position, speed) +} + +internal fun Window.isPlaybackSpeedPossibleAtPosition(positionMs: Long, playbackSpeed: Float): Boolean { + return when { + !isLive() || playbackSpeed == NormalSpeed -> true + !isSeekable -> false + isAtDefaultPosition(positionMs) && playbackSpeed > NormalSpeed -> false + else -> true + } +} + +internal fun Window.isAtDefaultPosition(positionMs: Long): Boolean { + return positionMs >= defaultPositionMs +} + +private const val NormalSpeed = 1.0f + +private fun MediaItem.clearTag() = this.buildUpon().setTag(null).build() diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaLibraryService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaLibraryService.kt index 311c09ca5..a1a4425e6 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaLibraryService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaLibraryService.kt @@ -10,14 +10,14 @@ import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession -import ch.srgssr.pillarbox.player.PillarboxPlayer +import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.player.utils.PendingIntentUtils /** * `PillarboxMediaLibraryService` implementation of [MediaLibraryService]. * It is the recommended way to make background playback for Android and sharing content with Android Auto. * - * It handles only one [MediaSession] with one [PillarboxPlayer]. + * It handles only one [MediaSession] with one [PillarboxExoPlayer]. * * Usage: * Add these permissions inside your manifest: @@ -70,7 +70,7 @@ abstract class PillarboxMediaLibraryService : MediaLibraryService() { /** * Set player to use with this Service. */ - fun setPlayer(player: PillarboxPlayer, callback: MediaLibrarySession.Callback) { + fun setPlayer(player: PillarboxExoPlayer, callback: MediaLibrarySession.Callback) { if (this.player == null) { this.player = player player.setWakeMode(C.WAKE_MODE_NETWORK) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt index 8446450dc..fdee7fe01 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt @@ -11,13 +11,14 @@ import androidx.media3.common.Player import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService import ch.srgssr.pillarbox.player.PillarboxPlayer +import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.player.utils.PendingIntentUtils /** * `PillarboxMediaSessionService` implementation of [MediaSessionService]. * It is the recommended way to make background playback for Android. * - * It handles only one [MediaSession] with one [PillarboxPlayer]. + * It handles only one [MediaSession] with one [PillarboxExoPlayer]. * * Usage: * Add these permissions inside your manifest: @@ -64,8 +65,8 @@ abstract class PillarboxMediaSessionService : MediaSessionService() { * @param mediaSessionCallback The MediaSession.Callback to use [MediaSession.Builder.setCallback]. */ fun setPlayer( - player: PillarboxPlayer, mediaSessionCallback: MediaSession.Callback = object : DefaultMediaSessionCallback {} + player: PillarboxExoPlayer, ) { if (this.player == null) { this.player = player diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PlaybackService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PlaybackService.kt index d02d0f444..5c7371a7e 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PlaybackService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PlaybackService.kt @@ -15,7 +15,7 @@ import androidx.media3.common.C import androidx.media3.common.util.NotificationUtil import androidx.media3.session.MediaSession import androidx.media3.ui.PlayerNotificationManager -import ch.srgssr.pillarbox.player.PillarboxPlayer +import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.player.notification.PillarboxMediaDescriptionAdapter /** @@ -42,7 +42,7 @@ import ch.srgssr.pillarbox.player.notification.PillarboxMediaDescriptionAdapter */ abstract class PlaybackService : Service() { private val binder = ServiceBinder() - private var player: PillarboxPlayer? = null + private var player: PillarboxExoPlayer? = null private var mediaSession: MediaSession? = null protected lateinit var notificationManager: PlayerNotificationManager @@ -83,7 +83,7 @@ abstract class PlaybackService : Service() { * * @param player Player to be linked with this PlaybackService */ - fun setPlayer(player: PillarboxPlayer) { + fun setPlayer(player: PillarboxExoPlayer) { if (this.player != player) { this.player?.setWakeMode(C.WAKE_MODE_NONE) player.setWakeMode(C.WAKE_MODE_NETWORK) @@ -129,7 +129,7 @@ abstract class PlaybackService : Service() { * * @param player */ - fun setPlayer(player: PillarboxPlayer) { + fun setPlayer(player: PillarboxExoPlayer) { this@PlaybackService.setPlayer(player) } } diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxPlayerMediaItemTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxExoPlayerMediaItemTest.kt similarity index 96% rename from pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxPlayerMediaItemTest.kt rename to pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxExoPlayerMediaItemTest.kt index dc3df3c4f..d36127c67 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxPlayerMediaItemTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxExoPlayerMediaItemTest.kt @@ -11,6 +11,7 @@ import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.test.utils.FakeClock import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.player.extension.getCurrentMediaItems import org.junit.Before import org.junit.runner.RunWith @@ -18,13 +19,13 @@ import kotlin.test.Test import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) -class PillarboxPlayerMediaItemTest { - private lateinit var player: PillarboxPlayer +class PillarboxExoPlayerMediaItemTest { + private lateinit var player: PillarboxExoPlayer @Before fun createPlayer() { val context = ApplicationProvider.getApplicationContext() - player = PillarboxPlayer( + player = PillarboxExoPlayer( context = context, seekIncrement = SeekIncrement(), loadControl = DefaultLoadControl(), diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestIsPlaybackSpeedPossibleAtPosition.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestIsPlaybackSpeedPossibleAtPosition.kt index 4273cc7e0..5f1131a59 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestIsPlaybackSpeedPossibleAtPosition.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestIsPlaybackSpeedPossibleAtPosition.kt @@ -8,6 +8,7 @@ import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.Timeline.EMPTY import androidx.media3.common.Timeline.Window +import ch.srgssr.pillarbox.player.exoplayer.isPlaybackSpeedPossibleAtPosition import ch.srgssr.pillarbox.player.test.utils.TestTimeline import io.mockk.clearAllMocks import io.mockk.every diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxPlayerPlaybackSpeed.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxExoPlayerPlaybackSpeed.kt similarity index 95% rename from pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxPlayerPlaybackSpeed.kt rename to pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxExoPlayerPlaybackSpeed.kt index fbe4634d5..ae8d159f7 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxPlayerPlaybackSpeed.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxExoPlayerPlaybackSpeed.kt @@ -13,6 +13,7 @@ import androidx.media3.test.utils.FakeClock import androidx.media3.test.utils.robolectric.TestPlayerRunHelper import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed import ch.srgssr.pillarbox.player.test.utils.TestPillarboxRunHelper import org.junit.After @@ -22,13 +23,13 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class TestPillarboxPlayerPlaybackSpeed { - private lateinit var player: PillarboxPlayer +class TestPillarboxExoPlayerPlaybackSpeed { + private lateinit var player: PillarboxExoPlayer @Before fun createPlayer() { val context = ApplicationProvider.getApplicationContext() - player = PillarboxPlayer( + player = PillarboxExoPlayer( context = context, clock = FakeClock(true), ) diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerTest.kt index 1ec473e81..16599fef0 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerTest.kt @@ -13,8 +13,8 @@ import androidx.media3.test.utils.robolectric.RobolectricUtil import androidx.media3.test.utils.robolectric.TestPlayerRunHelper import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import ch.srgssr.pillarbox.player.PillarboxPlayer import ch.srgssr.pillarbox.player.SeekIncrement +import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.player.extension.getMediaItemTrackerData import ch.srgssr.pillarbox.player.extension.getMediaItemTrackerDataOrNull import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory @@ -33,7 +33,7 @@ import kotlin.test.assertNotNull @RunWith(AndroidJUnit4::class) class MediaItemTrackerTest { - private lateinit var player: PillarboxPlayer + private lateinit var player: PillarboxExoPlayer private lateinit var fakeMediaItemTracker: FakeMediaItemTracker private lateinit var fakeClock: FakeClock @@ -42,7 +42,7 @@ class MediaItemTrackerTest { val context = ApplicationProvider.getApplicationContext() fakeMediaItemTracker = spyk(FakeMediaItemTracker()) fakeClock = FakeClock(true) - player = PillarboxPlayer( + player = PillarboxExoPlayer( context = context, seekIncrement = SeekIncrement(), loadControl = DefaultLoadControl(), From 2e950caeff50a24791ce399b688c62c2d55bf7d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Tue, 26 Mar 2024 17:24:51 +0100 Subject: [PATCH 02/27] Add guava kotlin coroutines --- gradle/libs.versions.toml | 1 + pillarbox-player/build.gradle.kts | 1 + 2 files changed, 2 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 711d719e9..52a77921d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/pillarbox-player/build.gradle.kts b/pillarbox-player/build.gradle.kts index 2de9f09c2..4fede1f6d 100644 --- a/pillarbox-player/build.gradle.kts +++ b/pillarbox-player/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { api(libs.androidx.media3.session) api(libs.androidx.media3.ui) api(libs.guava) + api(libs.kotlinx.coroutines.guava) runtimeOnly(libs.kotlinx.coroutines.android) api(libs.kotlinx.coroutines.core) From da85615e89de26ad830506987629325d5ce91f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Tue, 26 Mar 2024 17:25:17 +0100 Subject: [PATCH 03/27] wip Improve MediaController and Mediabrowser --- .../service/DefaultMediaSessionCallback.kt | 72 +- .../service/PillarboxMediaSessionService.kt | 19 +- .../player/session/PillarboxMediaBrowser.kt | 75 ++ .../session/PillarboxMediaController.kt | 746 ++++++++++++++++++ .../session/PillarboxSessionCommands.kt | 22 + 5 files changed, 930 insertions(+), 4 deletions(-) create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallback.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallback.kt index 0a7f5079d..80c86a2af 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallback.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallback.kt @@ -4,8 +4,17 @@ */ package ch.srgssr.pillarbox.player.service +import android.os.Bundle import androidx.media3.common.MediaItem +import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSession.ConnectionResult +import androidx.media3.session.SessionCommand +import androidx.media3.session.SessionCommands +import androidx.media3.session.SessionResult +import ch.srgssr.pillarbox.player.PillarboxPlayer +import ch.srgssr.pillarbox.player.session.PillarboxSessionCommands +import ch.srgssr.pillarbox.player.utils.DebugLogger import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @@ -14,7 +23,7 @@ import com.google.common.util.concurrent.ListenableFuture * * @see [MediaSession.Builder.setCallback] */ -interface DefaultMediaSessionCallback : MediaSession.Callback { +class DefaultMediaSessionCallback : MediaSession.Callback { override fun onAddMediaItems( mediaSession: MediaSession, @@ -28,4 +37,65 @@ interface DefaultMediaSessionCallback : MediaSession.Callback { } return Futures.immediateFuture(mediaItems) } + + override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): ConnectionResult { + val availableSessionCommands = SessionCommands.Builder().apply { + if (session is MediaLibraryService.MediaLibrarySession) { + addSessionCommands(ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.commands) + } else { + addSessionCommands(ConnectionResult.DEFAULT_SESSION_COMMANDS.commands) + } + add(PillarboxSessionCommands.COMMAND_SEEK_DISABLED) + add(PillarboxSessionCommands.COMMAND_SEEK_ENABLED) + add(PillarboxSessionCommands.COMMAND_SEEK_GET) + }.build() + return ConnectionResult.accept(availableSessionCommands, ConnectionResult.DEFAULT_PLAYER_COMMANDS) + } + + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + DebugLogger.debug(TAG, "onCustomCommand ${customCommand.customAction} $args") + when (customCommand.customAction) { + PillarboxSessionCommands.SMOOTH_SEEKING_ENABLED -> { + if (session.player is PillarboxPlayer) { + (session.player as PillarboxPlayer).smoothSeekingEnabled = true + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + } + + PillarboxSessionCommands.SMOOTH_SEEKING_DISABLED -> { + if (session.player is PillarboxPlayer) { + (session.player as PillarboxPlayer).smoothSeekingEnabled = false + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + } + + PillarboxSessionCommands.SMOOTH_SEEKING_GET -> { + if (session.player is PillarboxPlayer) { + val state = (session.player as PillarboxPlayer).smoothSeekingEnabled + return Futures.immediateFuture( + SessionResult( + SessionResult.RESULT_SUCCESS, + Bundle().apply { + putBoolean( + "smoothSeekingEnabled", + state + ) + } + ) + ) + } + } + } + DebugLogger.warning(TAG, "Unsupported session command ${customCommand.customAction}") + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) + } + + companion object { + private const val TAG = "PillarboxMediaSession" + } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt index fdee7fe01..9c15a5258 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt @@ -6,12 +6,14 @@ package ch.srgssr.pillarbox.player.service import android.app.PendingIntent import android.content.Intent +import android.os.Bundle import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService import ch.srgssr.pillarbox.player.PillarboxPlayer import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.session.PillarboxSessionCommands import ch.srgssr.pillarbox.player.utils.PendingIntentUtils /** @@ -65,8 +67,8 @@ abstract class PillarboxMediaSessionService : MediaSessionService() { * @param mediaSessionCallback The MediaSession.Callback to use [MediaSession.Builder.setCallback]. */ fun setPlayer( - mediaSessionCallback: MediaSession.Callback = object : DefaultMediaSessionCallback {} player: PillarboxExoPlayer, + mediaSessionCallback: MediaSession.Callback = DefaultMediaSessionCallback() ) { if (this.player == null) { this.player = player @@ -74,10 +76,19 @@ abstract class PillarboxMediaSessionService : MediaSessionService() { player.setHandleAudioFocus(true) val builder = MediaSession.Builder(this, player) .setCallback(mediaSessionCallback) - .setId(packageName) + .setId("MediaService/$packageName") sessionActivity()?.let { builder.setSessionActivity(it) } + player.addListener(object : PillarboxPlayer.Listener { + override fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) { + mediaSession?.let { + for (controllerInfo in it.connectedControllers) { + it.sendCustomCommand(controllerInfo, PillarboxSessionCommands.seekChangedCommand(smoothSeekingEnabled), Bundle.EMPTY) + } + } + } + }) mediaSession = builder.build() } } @@ -108,7 +119,9 @@ abstract class PillarboxMediaSessionService : MediaSessionService() { // Return a MediaSession to link with the MediaController that is making // this request. - override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { + return mediaSession + } /** * We choose to stop playback when user remove application from the tasks diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt new file mode 100644 index 000000000..5d60e59a4 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.session + +import android.content.ComponentName +import android.content.Context +import androidx.annotation.IntRange +import androidx.media3.session.MediaBrowser +import androidx.media3.session.MediaLibraryService.LibraryParams +import androidx.media3.session.SessionToken +import ch.srgssr.pillarbox.player.service.PillarboxMediaSessionService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.guava.await +import kotlinx.coroutines.withContext + +class PillarboxMediaBrowser private constructor() : PillarboxMediaController(), MediaBrowser.Listener { + private lateinit var mediaBrowser: MediaBrowser + + class Builder(private val context: Context, private val clazz: Class) { + + suspend fun build(): PillarboxMediaController { + return withContext(Dispatchers.IO) { + val pillarboxMediaController = PillarboxMediaBrowser() + val componentName = ComponentName(context, clazz) + val sessionToken = SessionToken(context, componentName) + val mediaBrowser = MediaBrowser.Builder(context, sessionToken) + .setListener(pillarboxMediaController) + .buildAsync() + .await() + pillarboxMediaController.setMediaBrowser(mediaBrowser) + pillarboxMediaController + } + } + } + + override fun onChildrenChanged(browser: MediaBrowser, parentId: String, itemCount: Int, params: LibraryParams?) { + super.onChildrenChanged(browser, parentId, itemCount, params) + TODO("Implement maybe a custom listener") + } + + override fun onSearchResultChanged(browser: MediaBrowser, query: String, itemCount: Int, params: LibraryParams?) { + super.onSearchResultChanged(browser, query, itemCount, params) + } + + internal fun setMediaBrowser(mediaBrowser: MediaBrowser) { + setMediaController(mediaBrowser) + this.mediaBrowser = mediaBrowser + } + + fun getLibraryRoot(params: LibraryParams? = null) = mediaBrowser.getLibraryRoot(params) + + fun subscribe(parentId: String, params: LibraryParams? = null) = mediaBrowser.subscribe(parentId, params) + + fun unsubscribe(parentId: String) = mediaBrowser.unsubscribe(parentId) + + fun getChildren( + parentId: String, + @IntRange(from = 0) page: Int, + @IntRange(from = 1) pageSize: Int, + params: LibraryParams? = null + ) = mediaBrowser.getChildren(parentId, page, pageSize, params) + + fun getItem(mediaId: String) = mediaBrowser.getItem(mediaId) + + fun search(query: String, params: LibraryParams? = null) = mediaBrowser.search(query, params) + + fun getSearchResult( + query: String, + @IntRange(from = 0) page: Int, + @IntRange(from = 1) pageSize: Int, + params: LibraryParams? = null + ) = mediaBrowser.getSearchResult(query, page, pageSize, params) +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt new file mode 100644 index 000000000..e5cbe6409 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt @@ -0,0 +1,746 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.session + +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.os.Bundle +import android.os.Looper +import android.view.Surface +import android.view.SurfaceHolder +import android.view.SurfaceView +import android.view.TextureView +import androidx.annotation.FloatRange +import androidx.annotation.IntRange +import androidx.media3.common.AudioAttributes +import androidx.media3.common.DeviceInfo +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.PlaybackException +import androidx.media3.common.PlaybackParameters +import androidx.media3.common.Player +import androidx.media3.common.Rating +import androidx.media3.common.Timeline +import androidx.media3.common.TrackSelectionParameters +import androidx.media3.common.Tracks +import androidx.media3.common.VideoSize +import androidx.media3.common.text.CueGroup +import androidx.media3.common.util.Size +import androidx.media3.common.util.UnstableApi +import androidx.media3.session.CommandButton +import androidx.media3.session.MediaController +import androidx.media3.session.SessionCommand +import androidx.media3.session.SessionCommands +import androidx.media3.session.SessionResult +import androidx.media3.session.SessionToken +import ch.srgssr.pillarbox.player.PillarboxPlayer +import ch.srgssr.pillarbox.player.service.PillarboxMediaSessionService +import ch.srgssr.pillarbox.player.utils.DebugLogger +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.MoreExecutors +import kotlinx.coroutines.guava.await + +/** + * Pillarbox media controller + * + * @constructor Create empty Pillarbox media controller + */ +open class PillarboxMediaController internal constructor() : PillarboxPlayer, MediaController.Listener { + + class Builder(private val context: Context, private val clazz: Class) { + + suspend fun build(): PillarboxMediaController { + val pillarboxMediaController = PillarboxMediaController() + val componentName = ComponentName(context, clazz) + val sessionToken = SessionToken(context, componentName) + val mediaController = MediaController.Builder(context, sessionToken) + .setListener(pillarboxMediaController) + .buildAsync() + .await() + + pillarboxMediaController.setMediaController(mediaController) + return pillarboxMediaController + } + } + + private lateinit var mediaController: MediaController + private val listeners = HashSet() + val connectedToken: SessionToken? + get() = mediaController.connectedToken + + val isConnected: Boolean + get() = mediaController.isConnected + + val sessionActivity: PendingIntent? + get() = mediaController.sessionActivity + + @get:UnstableApi + val customLayout: ImmutableList + get() = mediaController.getCustomLayout() + + @get:UnstableApi + val sessionExtras: Bundle + get() = mediaController.getSessionExtras() + + val availableSessionCommands: SessionCommands + get() = mediaController.getAvailableSessionCommands() + + override var smoothSeekingEnabled: Boolean = false + set(value) { + if (field != value) { + if (value) { + sendCustomCommand(PillarboxSessionCommands.COMMAND_SEEK_ENABLED, Bundle.EMPTY) + } else { + sendCustomCommand(PillarboxSessionCommands.COMMAND_SEEK_DISABLED, Bundle.EMPTY) + } + field = value + val listeners = HashSet(listeners) + for (listener in listeners) { + listener.onSmoothSeekingEnabledChanged(value) + } + } + } + + override var trackingEnabled: Boolean + get() = TODO("Not yet implemented") + set(value) {} + + internal fun setMediaController(mediaController: MediaController) { + this.mediaController = mediaController + + // TODO: Fetch initial data + // Called from wrong thread if we load not from application thread + sendCustomCommand(PillarboxSessionCommands.COMMAND_SEEK_GET, Bundle.EMPTY).also { + it.addListener({ + val result = it.get() + DebugLogger.debug(TAG, "Fetch initial data ${result.extras}") + if (result.resultCode == SessionResult.RESULT_SUCCESS) { + smoothSeekingEnabled = result.extras.getBoolean("smoothSeekingEnabled") + } + }, MoreExecutors.directExecutor()) + } + } + + /** + * @see [MediaController.setRating] + */ + fun setRating(mediaId: String, rating: Rating): ListenableFuture { + return mediaController.setRating(mediaId, rating) + } + + /** + * @See [MediaController.setRating] + */ + fun setRating(rating: Rating): ListenableFuture { + return mediaController.setRating(rating) + } + + /** + * @See [MediaController.sendCustomCommand] + */ + fun sendCustomCommand(command: SessionCommand, args: Bundle): ListenableFuture { + return mediaController.sendCustomCommand(command, args) + } + + override fun onCustomCommand(controller: MediaController, command: SessionCommand, args: Bundle): ListenableFuture { + DebugLogger.debug(TAG, "onCustomCommand ${command.customAction} ${command.customExtras}") + when (command.customAction) { + PillarboxSessionCommands.SMOOTH_SEEKING_CHANGED -> { + val smoothSeeking = command.customExtras.getBoolean("smoothSeekingEnabled") + this.smoothSeekingEnabled = smoothSeeking + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + } + return super.onCustomCommand(controller, command, args) + } + + override fun onAvailableSessionCommandsChanged(controller: MediaController, commands: SessionCommands) { + super.onAvailableSessionCommandsChanged(controller, commands) + } + + override fun onDisconnected(controller: MediaController) { + super.onDisconnected(controller) + } + + override fun onExtrasChanged(controller: MediaController, extras: Bundle) { + super.onExtrasChanged(controller, extras) + } + + /** + * @see [MediaController.isSessionCommandAvailable] + */ + fun isSessionCommandAvailable(sessionCommandCode: Int): Boolean { + return mediaController.isSessionCommandAvailable(sessionCommandCode) + } + + /** + * @see [MediaController.isSessionCommandAvailable] + */ + fun isSessionCommandAvailable(sessionCommand: SessionCommand): Boolean { + return mediaController.isSessionCommandAvailable(sessionCommand) + } + + override fun getApplicationLooper(): Looper { + return mediaController.applicationLooper + } + + override fun addListener(listener: Player.Listener) { + mediaController.addListener(listener) + if (listener is PillarboxPlayer.Listener) { + listeners.add(listener) + } + } + + override fun removeListener(listener: Player.Listener) { + mediaController.removeListener(listener) + if (listener is PillarboxPlayer.Listener) { + listeners.remove(listener) + } + } + + override fun setMediaItems(mediaItems: List) { + mediaController.setMediaItems(mediaItems) + } + + override fun setMediaItems(mediaItems: List, resetPosition: Boolean) { + mediaController.setMediaItems(mediaItems, resetPosition) + } + + override fun setMediaItems(mediaItems: List, startIndex: Int, startPositionMs: Long) { + mediaController.setMediaItems(mediaItems, startIndex, startPositionMs) + } + + override fun setMediaItem(mediaItem: MediaItem) { + mediaController.setMediaItem(mediaItem) + } + + override fun setMediaItem(mediaItem: MediaItem, startPositionMs: Long) { + mediaController.setMediaItem(mediaItem, startPositionMs) + } + + override fun setMediaItem(mediaItem: MediaItem, resetPosition: Boolean) { + mediaController.setMediaItem(mediaItem, resetPosition) + } + + override fun addMediaItem(mediaItem: MediaItem) { + mediaController.addMediaItem(mediaItem) + } + + override fun addMediaItem(index: Int, mediaItem: MediaItem) { + mediaController.addMediaItem(index, mediaItem) + } + + override fun addMediaItems(mediaItems: List) { + mediaController.addMediaItems(mediaItems) + } + + override fun addMediaItems(index: Int, mediaItems: List) { + mediaController.addMediaItems(index, mediaItems) + } + + override fun moveMediaItem(currentIndex: Int, newIndex: Int) { + mediaController.moveMediaItem(currentIndex, newIndex) + } + + override fun moveMediaItems(fromIndex: Int, toIndex: Int, newIndex: Int) { + mediaController.moveMediaItems(fromIndex, toIndex, newIndex) + } + + override fun replaceMediaItem(index: Int, mediaItem: MediaItem) { + mediaController.replaceMediaItem(index, mediaItem) + } + + override fun replaceMediaItems(fromIndex: Int, toIndex: Int, mediaItems: List) { + mediaController.replaceMediaItems(fromIndex, toIndex, mediaItems) + } + + override fun removeMediaItem(index: Int) { + mediaController.removeMediaItem(index) + } + + override fun removeMediaItems(fromIndex: Int, toIndex: Int) { + mediaController.removeMediaItems(fromIndex, toIndex) + } + + override fun clearMediaItems() { + mediaController.clearMediaItems() + } + + override fun isCommandAvailable(command: Int): Boolean { + return mediaController.isCommandAvailable(command) + } + + override fun canAdvertiseSession(): Boolean { + return mediaController.canAdvertiseSession() + } + + override fun getAvailableCommands(): Player.Commands { + return mediaController.getAvailableCommands() + } + + override fun prepare() { + mediaController.prepare() + } + + override fun getPlaybackState(): Int { + return mediaController.getPlaybackState() + } + + override fun getPlaybackSuppressionReason(): Int { + return mediaController.getPlaybackSuppressionReason() + } + + override fun isPlaying(): Boolean { + return mediaController.isPlaying() + } + + override fun getPlayerError(): PlaybackException? { + return mediaController.getPlayerError() + } + + override fun play() { + mediaController.play() + } + + override fun pause() { + mediaController.pause() + } + + override fun setPlayWhenReady(playWhenReady: Boolean) { + mediaController.setPlayWhenReady(playWhenReady) + } + + override fun getPlayWhenReady(): Boolean { + return mediaController.getPlayWhenReady() + } + + override fun setRepeatMode(repeatMode: Int) { + mediaController.setRepeatMode(repeatMode) + } + + override fun getRepeatMode(): Int { + return mediaController.getRepeatMode() + } + + override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) { + mediaController.setShuffleModeEnabled(shuffleModeEnabled) + } + + override fun getShuffleModeEnabled(): Boolean { + return mediaController.getShuffleModeEnabled() + } + + override fun isLoading(): Boolean { + return mediaController.isLoading() + } + + override fun seekToDefaultPosition() { + mediaController.seekToDefaultPosition() + } + + override fun seekToDefaultPosition(mediaItemIndex: Int) { + mediaController.seekToDefaultPosition(mediaItemIndex) + } + + override fun seekTo(positionMs: Long) { + mediaController.seekTo(positionMs) + } + + override fun seekTo(mediaItemIndex: Int, positionMs: Long) { + mediaController.seekTo(mediaItemIndex, positionMs) + } + + override fun getSeekBackIncrement(): Long { + return mediaController.getSeekBackIncrement() + } + + override fun seekBack() { + mediaController.seekBack() + } + + override fun getSeekForwardIncrement(): Long { + return mediaController.getSeekForwardIncrement() + } + + override fun seekForward() { + mediaController.seekForward() + } + + @UnstableApi + @Deprecated("") + override fun hasPrevious(): Boolean { + return mediaController.hasPrevious() + } + + @UnstableApi + @Deprecated("") + override fun hasPreviousWindow(): Boolean { + return mediaController.hasPreviousWindow() + } + + override fun hasPreviousMediaItem(): Boolean { + return mediaController.hasPreviousMediaItem() + } + + @UnstableApi + @Deprecated("") + override fun previous() { + mediaController.previous() + } + + @UnstableApi + @Deprecated("") + override fun seekToPreviousWindow() { + mediaController.seekToPreviousWindow() + } + + override fun seekToPreviousMediaItem() { + mediaController.seekToPreviousMediaItem() + } + + override fun getMaxSeekToPreviousPosition(): Long { + return mediaController.getMaxSeekToPreviousPosition() + } + + override fun seekToPrevious() { + mediaController.seekToPrevious() + } + + @UnstableApi + @Deprecated("") + override fun hasNext(): Boolean { + return mediaController.hasNext() + } + + @UnstableApi + @Deprecated("") + override fun hasNextWindow(): Boolean { + return mediaController.hasNextWindow() + } + + override fun hasNextMediaItem(): Boolean { + return mediaController.hasNextMediaItem() + } + + @UnstableApi + @Deprecated("") + override fun next() { + mediaController.next() + } + + @UnstableApi + @Deprecated("") + override fun seekToNextWindow() { + mediaController.seekToNextWindow() + } + + override fun seekToNextMediaItem() { + mediaController.seekToNextMediaItem() + } + + override fun seekToNext() { + mediaController.seekToNext() + } + + override fun setPlaybackParameters(playbackParameters: PlaybackParameters) { + mediaController.setPlaybackParameters(playbackParameters) + } + + override fun setPlaybackSpeed(speed: Float) { + mediaController.setPlaybackSpeed(speed) + } + + override fun getPlaybackParameters(): PlaybackParameters { + return mediaController.getPlaybackParameters() + } + + override fun stop() { + mediaController.stop() + } + + override fun release() { + mediaController.release() + } + + override fun getCurrentTracks(): Tracks { + return mediaController.getCurrentTracks() + } + + override fun getTrackSelectionParameters(): TrackSelectionParameters { + return mediaController.getTrackSelectionParameters() + } + + override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) { + mediaController.setTrackSelectionParameters(parameters) + } + + override fun getMediaMetadata(): MediaMetadata { + return mediaController.getMediaMetadata() + } + + override fun getPlaylistMetadata(): MediaMetadata { + return mediaController.getPlaylistMetadata() + } + + override fun setPlaylistMetadata(mediaMetadata: MediaMetadata) { + mediaController.setPlaylistMetadata(mediaMetadata) + } + + @UnstableApi + override fun getCurrentManifest(): Any? { + return mediaController.currentManifest + } + + override fun getCurrentTimeline(): Timeline { + return mediaController.getCurrentTimeline() + } + + override fun getCurrentPeriodIndex(): Int { + return mediaController.getCurrentPeriodIndex() + } + + @UnstableApi + @Deprecated("") + override fun getCurrentWindowIndex(): Int { + return mediaController.currentWindowIndex + } + + override fun getCurrentMediaItemIndex(): Int { + return mediaController.getCurrentMediaItemIndex() + } + + @UnstableApi + @Deprecated("") + override fun getNextWindowIndex(): Int { + return mediaController.nextWindowIndex + } + + override fun getNextMediaItemIndex(): Int { + return mediaController.getNextMediaItemIndex() + } + + @UnstableApi + @Deprecated("") + override fun getPreviousWindowIndex(): Int { + return mediaController.previousWindowIndex + } + + override fun getPreviousMediaItemIndex(): Int { + return mediaController.getPreviousMediaItemIndex() + } + + override fun getCurrentMediaItem(): MediaItem? { + return mediaController.getCurrentMediaItem() + } + + override fun getMediaItemCount(): Int { + return mediaController.mediaItemCount + } + + override fun getMediaItemAt(index: Int): MediaItem { + return mediaController.getMediaItemAt(index) + } + + override fun getDuration(): Long { + return mediaController.getDuration() + } + + override fun getCurrentPosition(): Long { + return mediaController.getCurrentPosition() + } + + override fun getBufferedPosition(): Long { + return mediaController.getBufferedPosition() + } + + @IntRange(from = 0L, to = 100L) + override fun getBufferedPercentage(): Int { + return mediaController.getBufferedPercentage() + } + + override fun getTotalBufferedDuration(): Long { + return mediaController.getTotalBufferedDuration() + } + + @UnstableApi + @Deprecated("") + override fun isCurrentWindowDynamic(): Boolean { + return mediaController.isCurrentWindowDynamic + } + + override fun isCurrentMediaItemDynamic(): Boolean { + return mediaController.isCurrentMediaItemDynamic() + } + + @UnstableApi + @Deprecated("") + override fun isCurrentWindowLive(): Boolean { + return mediaController.isCurrentWindowLive + } + + override fun isCurrentMediaItemLive(): Boolean { + return mediaController.isCurrentMediaItemLive() + } + + override fun getCurrentLiveOffset(): Long { + return mediaController.getCurrentLiveOffset() + } + + @UnstableApi + @Deprecated("") + override fun isCurrentWindowSeekable(): Boolean { + return mediaController.isCurrentWindowSeekable + } + + override fun isCurrentMediaItemSeekable(): Boolean { + return mediaController.isCurrentMediaItemSeekable() + } + + override fun isPlayingAd(): Boolean { + return mediaController.isPlayingAd() + } + + override fun getCurrentAdGroupIndex(): Int { + return mediaController.getCurrentAdGroupIndex() + } + + override fun getCurrentAdIndexInAdGroup(): Int { + return mediaController.getCurrentAdIndexInAdGroup() + } + + override fun getContentDuration(): Long { + return mediaController.getContentDuration() + } + + override fun getContentPosition(): Long { + return mediaController.getContentPosition() + } + + override fun getContentBufferedPosition(): Long { + return mediaController.getContentBufferedPosition() + } + + override fun getAudioAttributes(): AudioAttributes { + return mediaController.getAudioAttributes() + } + + override fun setVolume(volume: Float) { + mediaController.setVolume(volume) + } + + @FloatRange(from = 0.0, to = 1.0) + override fun getVolume(): Float { + return mediaController.getVolume() + } + + override fun clearVideoSurface() { + mediaController.clearVideoSurface() + } + + override fun clearVideoSurface(surface: Surface?) { + mediaController.clearVideoSurface(surface) + } + + override fun setVideoSurface(surface: Surface?) { + mediaController.setVideoSurface(surface) + } + + override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) { + mediaController.setVideoSurfaceHolder(surfaceHolder) + } + + override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) { + mediaController.clearVideoSurfaceHolder(surfaceHolder) + } + + override fun setVideoSurfaceView(surfaceView: SurfaceView?) { + mediaController.setVideoSurfaceView(surfaceView) + } + + override fun clearVideoSurfaceView(surfaceView: SurfaceView?) { + mediaController.clearVideoSurfaceView(surfaceView) + } + + override fun setVideoTextureView(textureView: TextureView?) { + mediaController.setVideoTextureView(textureView) + } + + override fun clearVideoTextureView(textureView: TextureView?) { + mediaController.clearVideoTextureView(textureView) + } + + override fun getVideoSize(): VideoSize { + return mediaController.getVideoSize() + } + + @UnstableApi + override fun getSurfaceSize(): Size { + return mediaController.getSurfaceSize() + } + + override fun getCurrentCues(): CueGroup { + return mediaController.getCurrentCues() + } + + override fun getDeviceInfo(): DeviceInfo { + return mediaController.getDeviceInfo() + } + + @IntRange(from = 0L) + override fun getDeviceVolume(): Int { + return mediaController.getDeviceVolume() + } + + override fun isDeviceMuted(): Boolean { + return mediaController.isDeviceMuted() + } + + @Deprecated("") + override fun setDeviceVolume(volume: Int) { + mediaController.setDeviceVolume(volume) + } + + override fun setDeviceVolume(volume: Int, flags: Int) { + mediaController.setDeviceVolume(volume, flags) + } + + @Deprecated("") + override fun increaseDeviceVolume() { + mediaController.increaseDeviceVolume() + } + + override fun increaseDeviceVolume(flags: Int) { + mediaController.increaseDeviceVolume(flags) + } + + @Deprecated("") + override fun decreaseDeviceVolume() { + mediaController.decreaseDeviceVolume() + } + + override fun decreaseDeviceVolume(flags: Int) { + mediaController.decreaseDeviceVolume(flags) + } + + @Deprecated("") + override fun setDeviceMuted(muted: Boolean) { + mediaController.setDeviceMuted(muted) + } + + override fun setDeviceMuted(muted: Boolean, flags: Int) { + mediaController.setDeviceMuted(muted, flags) + } + + override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) { + mediaController.setAudioAttributes(audioAttributes, handleAudioFocus) + } + + companion object { + private const val TAG = " PillarboxMediaController" + } +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt new file mode 100644 index 000000000..3a965db1d --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.session + +import android.os.Bundle +import androidx.media3.session.SessionCommand + +internal object PillarboxSessionCommands { + const val SMOOTH_SEEKING_ENABLED = "pillarbox.smooth.seeking.enabled" + const val SMOOTH_SEEKING_DISABLED = "pillarbox.smooth.seeking.disabled" + const val SMOOTH_SEEKING_CHANGED = "pillarbox.smooth.seeking.changed" + const val SMOOTH_SEEKING_GET = "pillarbox.smooth.seeking.get" + + val COMMAND_SEEK_ENABLED = SessionCommand(SMOOTH_SEEKING_ENABLED, Bundle.EMPTY) + val COMMAND_SEEK_DISABLED = SessionCommand(SMOOTH_SEEKING_DISABLED, Bundle.EMPTY) + val COMMAND_SEEK_GET = SessionCommand(SMOOTH_SEEKING_GET, Bundle.EMPTY) + + fun seekChangedCommand(smoothSeekingEnabled: Boolean) = + SessionCommand(SMOOTH_SEEKING_CHANGED, Bundle().apply { putBoolean("smoothSeekingEnabled", smoothSeekingEnabled) }) +} From 22c5219c3c8e92080b3a5edecd530350ba42e405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 27 Mar 2024 17:31:59 +0100 Subject: [PATCH 04/27] Try with MediaSessionExtras --- .../service/DefaultMediaSessionCallback.kt | 12 ++++++++++++ .../service/PillarboxMediaSessionService.kt | 17 +++++++++++++++++ .../player/session/PillarboxMediaController.kt | 3 +++ 3 files changed, 32 insertions(+) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallback.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallback.kt index 80c86a2af..55d9e1c84 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallback.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallback.kt @@ -5,6 +5,7 @@ package ch.srgssr.pillarbox.player.service import android.os.Bundle +import android.util.Log import androidx.media3.common.MediaItem import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession @@ -52,6 +53,17 @@ class DefaultMediaSessionCallback : MediaSession.Callback { return ConnectionResult.accept(availableSessionCommands, ConnectionResult.DEFAULT_PLAYER_COMMANDS) } + override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { + val pillarbox = session.player as PillarboxPlayer + Log.d(TAG, "onPostConnect") + session.setSessionExtras( + controller, + Bundle().apply { + putBoolean("smoothSeekingEnabled", pillarbox.smoothSeekingEnabled) + } + ) + } + override fun onCustomCommand( session: MediaSession, controller: MediaSession.ControllerInfo, diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt index 9c15a5258..6ba29e26a 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt @@ -85,11 +85,28 @@ abstract class PillarboxMediaSessionService : MediaSessionService() { mediaSession?.let { for (controllerInfo in it.connectedControllers) { it.sendCustomCommand(controllerInfo, PillarboxSessionCommands.seekChangedCommand(smoothSeekingEnabled), Bundle.EMPTY) + it.setSessionExtras( + controllerInfo, + Bundle().apply { + putBoolean("smoothSeekingEnabled", smoothSeekingEnabled) + } + ) } } } }) mediaSession = builder.build() + mediaSession?.let { + for (controllerInfo in it.connectedControllers) { + // it.sendCustomCommand(controllerInfo, PillarboxSessionCommands.seekChangedCommand(smoothSeekingEnabled), Bundle.EMPTY) + it.setSessionExtras( + controllerInfo, + Bundle().apply { + putBoolean("smoothSeekingEnabled", player.smoothSeekingEnabled) + } + ) + } + } } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt index e5cbe6409..b273ff026 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt @@ -9,6 +9,7 @@ import android.content.ComponentName import android.content.Context import android.os.Bundle import android.os.Looper +import android.util.Log import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView @@ -124,6 +125,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer, Me } }, MoreExecutors.directExecutor()) } + Log.d(TAG, "fromSessionExtras = $sessionExtras") } /** @@ -169,6 +171,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer, Me override fun onExtrasChanged(controller: MediaController, extras: Bundle) { super.onExtrasChanged(controller, extras) + Log.i(TAG, "onExtrasChanged $extras") } /** From 8071d5bbe2a81c593f1589754401cc57f7e17386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Thu, 28 Mar 2024 16:48:24 +0100 Subject: [PATCH 05/27] Add Pillarbox MediaSession --- .../demo/service/DemoMediaLibraryService.kt | 17 +- .../service/DefaultMediaSessionCallback.kt | 113 --------- .../service/PillarboxMediaLibraryService.kt | 20 +- .../service/PillarboxMediaSessionService.kt | 50 +--- .../session/PillarboxMediaController.kt | 4 +- .../session/PillarboxMediaLibrarySession.kt | 156 ++++++++++++ .../player/session/PillarboxMediaSession.kt | 234 ++++++++++++++++++ .../DefaultMediaSessionCallbackTest.kt | 80 ------ 8 files changed, 424 insertions(+), 250 deletions(-) delete mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallback.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt delete mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallbackTest.kt diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt index 3bf28d935..43fbead97 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt @@ -15,6 +15,8 @@ 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.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 @@ -61,9 +63,9 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() { super.onDestroy() } - private inner class DemoCallback : MediaLibrarySession.Callback { + private inner class DemoCallback : PillarboxMediaLibrarySession.Callback { override fun onGetLibraryRoot( - session: MediaLibrarySession, + session: PillarboxMediaLibrarySession, browser: MediaSession.ControllerInfo, params: LibraryParams? ): ListenableFuture> { @@ -79,7 +81,7 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() { } override fun onGetChildren( - session: MediaLibrarySession, + session: PillarboxMediaLibrarySession, browser: MediaSession.ControllerInfo, parentId: String, page: Int, @@ -93,11 +95,12 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() { } override fun onGetItem( - session: MediaLibrarySession, + session: PillarboxMediaLibrarySession, browser: MediaSession.ControllerInfo, mediaId: String ): ListenableFuture> { - Log.d(TAG, "onGetItem $mediaId") + val mediaItem = demoBrowser.getMediaItemFromId(mediaId) ?: MediaItem.EMPTY + Log.d(TAG, "onGetItem $mediaId // media ${mediaItem.mediaId} ${mediaItem.localConfiguration?.uri}") return Futures.immediateFuture( LibraryResult.ofItem( demoBrowser.getMediaItemFromId(mediaId) ?: MediaItem.EMPTY, LibraryParams.Builder().build() @@ -106,7 +109,7 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() { } override fun onAddMediaItems( - mediaSession: MediaSession, + mediaSession: PillarboxMediaSession, controller: MediaSession.ControllerInfo, mediaItems: MutableList ): ListenableFuture> { @@ -120,7 +123,7 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() { } override fun onSearch( - session: MediaLibrarySession, + session: PillarboxMediaLibrarySession, browser: MediaSession.ControllerInfo, query: String, params: LibraryParams? diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallback.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallback.kt deleted file mode 100644 index 55d9e1c84..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallback.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.service - -import android.os.Bundle -import android.util.Log -import androidx.media3.common.MediaItem -import androidx.media3.session.MediaLibraryService -import androidx.media3.session.MediaSession -import androidx.media3.session.MediaSession.ConnectionResult -import androidx.media3.session.SessionCommand -import androidx.media3.session.SessionCommands -import androidx.media3.session.SessionResult -import ch.srgssr.pillarbox.player.PillarboxPlayer -import ch.srgssr.pillarbox.player.session.PillarboxSessionCommands -import ch.srgssr.pillarbox.player.utils.DebugLogger -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture - -/** - * Default media session callback that allow to add [MediaItem] with an url or an mediaId to the MediaController. - * - * @see [MediaSession.Builder.setCallback] - */ -class DefaultMediaSessionCallback : MediaSession.Callback { - - override fun onAddMediaItems( - mediaSession: MediaSession, - controller: MediaSession.ControllerInfo, - mediaItems: MutableList - ): ListenableFuture> { - for (mediaItem in mediaItems) { - if (mediaItem.localConfiguration == null && mediaItem.mediaId.isBlank()) { - return Futures.immediateFailedFuture(UnsupportedOperationException()) - } - } - return Futures.immediateFuture(mediaItems) - } - - override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): ConnectionResult { - val availableSessionCommands = SessionCommands.Builder().apply { - if (session is MediaLibraryService.MediaLibrarySession) { - addSessionCommands(ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.commands) - } else { - addSessionCommands(ConnectionResult.DEFAULT_SESSION_COMMANDS.commands) - } - add(PillarboxSessionCommands.COMMAND_SEEK_DISABLED) - add(PillarboxSessionCommands.COMMAND_SEEK_ENABLED) - add(PillarboxSessionCommands.COMMAND_SEEK_GET) - }.build() - return ConnectionResult.accept(availableSessionCommands, ConnectionResult.DEFAULT_PLAYER_COMMANDS) - } - - override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { - val pillarbox = session.player as PillarboxPlayer - Log.d(TAG, "onPostConnect") - session.setSessionExtras( - controller, - Bundle().apply { - putBoolean("smoothSeekingEnabled", pillarbox.smoothSeekingEnabled) - } - ) - } - - override fun onCustomCommand( - session: MediaSession, - controller: MediaSession.ControllerInfo, - customCommand: SessionCommand, - args: Bundle - ): ListenableFuture { - DebugLogger.debug(TAG, "onCustomCommand ${customCommand.customAction} $args") - when (customCommand.customAction) { - PillarboxSessionCommands.SMOOTH_SEEKING_ENABLED -> { - if (session.player is PillarboxPlayer) { - (session.player as PillarboxPlayer).smoothSeekingEnabled = true - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } - } - - PillarboxSessionCommands.SMOOTH_SEEKING_DISABLED -> { - if (session.player is PillarboxPlayer) { - (session.player as PillarboxPlayer).smoothSeekingEnabled = false - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } - } - - PillarboxSessionCommands.SMOOTH_SEEKING_GET -> { - if (session.player is PillarboxPlayer) { - val state = (session.player as PillarboxPlayer).smoothSeekingEnabled - return Futures.immediateFuture( - SessionResult( - SessionResult.RESULT_SUCCESS, - Bundle().apply { - putBoolean( - "smoothSeekingEnabled", - state - ) - } - ) - ) - } - } - } - DebugLogger.warning(TAG, "Unsupported session command ${customCommand.customAction}") - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) - } - - companion object { - private const val TAG = "PillarboxMediaSession" - } -} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaLibraryService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaLibraryService.kt index a1a4425e6..e69690ffd 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaLibraryService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaLibraryService.kt @@ -11,6 +11,7 @@ import androidx.media3.common.Player import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.session.PillarboxMediaLibrarySession import ch.srgssr.pillarbox.player.utils.PendingIntentUtils /** @@ -60,7 +61,7 @@ import ch.srgssr.pillarbox.player.utils.PendingIntentUtils */ abstract class PillarboxMediaLibraryService : MediaLibraryService() { private var player: Player? = null - private var mediaSession: MediaLibrarySession? = null + private var mediaSession: PillarboxMediaLibrarySession? = null /** * Release on task removed @@ -70,22 +71,23 @@ abstract class PillarboxMediaLibraryService : MediaLibraryService() { /** * Set player to use with this Service. */ - fun setPlayer(player: PillarboxExoPlayer, callback: MediaLibrarySession.Callback) { + fun setPlayer(player: PillarboxExoPlayer, callback: PillarboxMediaLibrarySession.Callback) { if (this.player == null) { this.player = player player.setWakeMode(C.WAKE_MODE_NETWORK) player.setHandleAudioFocus(true) - val builder = MediaLibrarySession.Builder(this, player, callback) - .setId(packageName) - sessionActivity()?.let { - builder.setSessionActivity(it) + mediaSession = PillarboxMediaLibrarySession.Builder(this, player, callback).apply { + setId(packageName) + sessionActivity()?.let { + setSessionActivity(it) + } } - mediaSession = builder.build() + .build() } } override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? { - return mediaSession + return mediaSession?.mediaSession } /** @@ -103,8 +105,8 @@ abstract class PillarboxMediaLibraryService : MediaLibraryService() { mediaSession?.run { player.release() release() - mediaSession = null } + mediaSession = null } override fun onDestroy() { diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt index 6ba29e26a..603f29a1b 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt @@ -6,14 +6,12 @@ package ch.srgssr.pillarbox.player.service import android.app.PendingIntent import android.content.Intent -import android.os.Bundle import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService -import ch.srgssr.pillarbox.player.PillarboxPlayer import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer -import ch.srgssr.pillarbox.player.session.PillarboxSessionCommands +import ch.srgssr.pillarbox.player.session.PillarboxMediaSession import ch.srgssr.pillarbox.player.utils.PendingIntentUtils /** @@ -54,7 +52,7 @@ import ch.srgssr.pillarbox.player.utils.PendingIntentUtils @Suppress("MemberVisibilityCanBePrivate") abstract class PillarboxMediaSessionService : MediaSessionService() { private var player: Player? = null - private var mediaSession: MediaSession? = null + private var mediaSession: PillarboxMediaSession? = null /** * Release on task removed @@ -68,45 +66,19 @@ abstract class PillarboxMediaSessionService : MediaSessionService() { */ fun setPlayer( player: PillarboxExoPlayer, - mediaSessionCallback: MediaSession.Callback = DefaultMediaSessionCallback() + mediaSessionCallback: PillarboxMediaSession.Callback = PillarboxMediaSession.Callback.Default ) { if (this.player == null) { this.player = player player.setWakeMode(C.WAKE_MODE_NETWORK) player.setHandleAudioFocus(true) - val builder = MediaSession.Builder(this, player) - .setCallback(mediaSessionCallback) - .setId("MediaService/$packageName") - sessionActivity()?.let { - builder.setSessionActivity(it) - } - player.addListener(object : PillarboxPlayer.Listener { - override fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) { - mediaSession?.let { - for (controllerInfo in it.connectedControllers) { - it.sendCustomCommand(controllerInfo, PillarboxSessionCommands.seekChangedCommand(smoothSeekingEnabled), Bundle.EMPTY) - it.setSessionExtras( - controllerInfo, - Bundle().apply { - putBoolean("smoothSeekingEnabled", smoothSeekingEnabled) - } - ) - } - } + mediaSession = PillarboxMediaSession.Builder(this, player).apply { + sessionActivity()?.let { + setSessionActivity(it) } - }) - mediaSession = builder.build() - mediaSession?.let { - for (controllerInfo in it.connectedControllers) { - // it.sendCustomCommand(controllerInfo, PillarboxSessionCommands.seekChangedCommand(smoothSeekingEnabled), Bundle.EMPTY) - it.setSessionExtras( - controllerInfo, - Bundle().apply { - putBoolean("smoothSeekingEnabled", player.smoothSeekingEnabled) - } - ) - } - } + setId("MediaService/$packageName") + setCallback(mediaSessionCallback) + }.build() } } @@ -130,14 +102,14 @@ abstract class PillarboxMediaSessionService : MediaSessionService() { mediaSession?.run { player.release() release() - mediaSession = null } + mediaSession = null } // Return a MediaSession to link with the MediaController that is making // this request. override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { - return mediaSession + return mediaSession?.mediaSession } /** diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt index b273ff026..36e5c9c34 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt @@ -33,12 +33,12 @@ import androidx.media3.common.util.Size import androidx.media3.common.util.UnstableApi import androidx.media3.session.CommandButton import androidx.media3.session.MediaController +import androidx.media3.session.MediaSessionService import androidx.media3.session.SessionCommand import androidx.media3.session.SessionCommands import androidx.media3.session.SessionResult import androidx.media3.session.SessionToken import ch.srgssr.pillarbox.player.PillarboxPlayer -import ch.srgssr.pillarbox.player.service.PillarboxMediaSessionService import ch.srgssr.pillarbox.player.utils.DebugLogger import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures @@ -53,7 +53,7 @@ import kotlinx.coroutines.guava.await */ open class PillarboxMediaController internal constructor() : PillarboxPlayer, MediaController.Listener { - class Builder(private val context: Context, private val clazz: Class) { + class Builder(private val context: Context, private val clazz: Class) { suspend fun build(): PillarboxMediaController { val pillarboxMediaController = PillarboxMediaController() diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt new file mode 100644 index 000000000..f9784b5c6 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.session + +import android.app.PendingIntent +import android.content.Context +import androidx.media3.common.MediaItem +import androidx.media3.session.LibraryResult +import androidx.media3.session.MediaLibraryService +import androidx.media3.session.MediaLibraryService.MediaLibrarySession +import androidx.media3.session.MediaSession +import ch.srgssr.pillarbox.player.PillarboxPlayer +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 + +open class PillarboxMediaLibrarySession internal constructor(callback: Callback) : + PillarboxMediaSession(callback), + MediaLibrarySession.Callback { + + interface Callback : PillarboxMediaSession.Callback { + fun onGetLibraryRoot( + session: PillarboxMediaLibrarySession, + browser: MediaSession.ControllerInfo, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture> { + return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) + } + + fun onGetChildren( + session: PillarboxMediaLibrarySession, + browser: MediaSession.ControllerInfo, + parentId: String, + page: Int, + pageSize: Int, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture>> { + return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) + } + + fun onGetItem( + session: PillarboxMediaLibrarySession, + browser: MediaSession.ControllerInfo, + mediaId: String + ): ListenableFuture> { + return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) + } + + fun onSearch( + session: PillarboxMediaLibrarySession, + browser: MediaSession.ControllerInfo, + query: String, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture> { + return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) + } + + fun onGetSearchResult( + session: PillarboxMediaLibrarySession, + browser: MediaSession.ControllerInfo, + query: String, + page: Int, + pageSize: Int, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture>> { + return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) + } + } + + class Builder(private val context: Context, private val player: PillarboxPlayer, private val callback: Callback) { + private var pendingIntent: PendingIntent? = PendingIntentUtils.getDefaultPendingIntent(context) + private var id: String? = null + + fun setSessionActivity(pendingIntent: PendingIntent): Builder { + this.pendingIntent = pendingIntent + return this + } + + fun setId(id: String): Builder { + this.id = id + return this + } + + fun build(): PillarboxMediaLibrarySession { + val pillarboxMediaSession = PillarboxMediaLibrarySession(callback) + val mediaSessionBuilder = MediaLibrarySession.Builder(context, player, pillarboxMediaSession) + val mediaSession = mediaSessionBuilder.apply { + id?.let { setId(it) } + pendingIntent?.let { setSessionActivity(it) } + }.build() + pillarboxMediaSession.setMediaSession(mediaSession) + return pillarboxMediaSession + } + } + + override val mediaSession: MediaLibrarySession + get() = super.mediaSession as MediaLibrarySession + + override fun onGetLibraryRoot( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture> { + return (callback as Callback).onGetLibraryRoot(this, browser, params) + } + + override fun onGetChildren( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + parentId: String, + page: Int, + pageSize: Int, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture>> { + return (callback as Callback).onGetChildren(this, browser, parentId, page, pageSize, params) + } + + override fun onGetItem( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + mediaId: String + ): ListenableFuture> { + return (callback as Callback).onGetItem(this, browser, mediaId) + } + + override fun onSearch( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + query: String, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture> { + return (callback as Callback).onSearch(this, browser, query, params) + } + + override fun onGetSearchResult( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + query: String, + page: Int, + pageSize: Int, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture>> { + return (callback as Callback).onGetSearchResult(this, browser, query, page, pageSize, params) + } + + override fun onAddMediaItems( + mediaSession: MediaSession, + controller: MediaSession.ControllerInfo, + mediaItems: MutableList + ): ListenableFuture> { + return (callback as Callback).onAddMediaItems(this, controller, mediaItems) + } +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt new file mode 100644 index 000000000..77162c815 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt @@ -0,0 +1,234 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.session + +import android.app.PendingIntent +import android.content.Context +import android.os.Bundle +import android.util.Log +import androidx.media3.common.MediaItem +import androidx.media3.session.MediaLibraryService +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSession.MediaItemsWithStartPosition +import androidx.media3.session.SessionCommand +import androidx.media3.session.SessionCommands +import androidx.media3.session.SessionResult +import ch.srgssr.pillarbox.player.PillarboxPlayer +import ch.srgssr.pillarbox.player.utils.DebugLogger +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture + +/** + * PillarboxMediaSession link together a [MediaSession] to a [PillarboxPlayer]. + */ +open class PillarboxMediaSession internal constructor(protected val callback: Callback) : MediaSession.Callback { + + interface Callback { + fun onSetMediaItems( + mediaSession: PillarboxMediaSession, + controller: MediaSession.ControllerInfo, + mediaItems: MutableList, + startIndex: Int, + startPositionMs: Long + ): ListenableFuture { + for (mediaItem in mediaItems) { + if (mediaItem.localConfiguration == null) { + return Futures.immediateFailedFuture(UnsupportedOperationException()) + } + } + return Futures.immediateFuture(MediaItemsWithStartPosition(mediaItems, startIndex, startPositionMs)) + } + + fun onAddMediaItems( + mediaSession: PillarboxMediaSession, + controller: MediaSession.ControllerInfo, + mediaItems: MutableList + ): ListenableFuture> { + for (mediaItem in mediaItems) { + if (mediaItem.localConfiguration == null) { + return Futures.immediateFailedFuture(UnsupportedOperationException()) + } + } + return Futures.immediateFuture(mediaItems) + } + + object Default : Callback + } + + class Builder(context: Context, player: PillarboxPlayer) { + private val mediaSessionBuilder = MediaSession.Builder(context, player) + private var callback: Callback = object : Callback {} + + fun setSessionActivity(pendingIntent: PendingIntent): Builder { + mediaSessionBuilder.setSessionActivity(pendingIntent) + return this + } + + fun setId(id: String): Builder { + mediaSessionBuilder.setId(id) + return this + } + + fun setCallback(callback: Callback) { + this.callback = callback + } + + fun build(): PillarboxMediaSession { + val pillarboxMediaSession = PillarboxMediaSession(callback) + val mediaSession = mediaSessionBuilder + .setCallback(pillarboxMediaSession) + .build() + pillarboxMediaSession.setMediaSession(mediaSession) + return pillarboxMediaSession + } + } + + private lateinit var _mediaSession: MediaSession + private val listener = ComponentListener() + open val mediaSession: MediaSession + get() { + return _mediaSession + } + val player: PillarboxPlayer + get() { + return _mediaSession.player as PillarboxPlayer + } + + fun setPlayer(player: PillarboxPlayer) { + if (player != this.player) { + this.player.removeListener(listener) + _mediaSession.player = player + player.addListener(listener) + + // Update MediaSession with new PillarboxPlayer state + for (controllerInfo in _mediaSession.connectedControllers) { + // it.sendCustomCommand(controllerInfo, PillarboxSessionCommands.seekChangedCommand(smoothSeekingEnabled), Bundle.EMPTY) + _mediaSession.setSessionExtras( + controllerInfo, + Bundle().apply { + putBoolean("smoothSeekingEnabled", player.smoothSeekingEnabled) + } + ) + } + } + } + + internal fun setMediaSession(mediaSession: MediaSession) { + this._mediaSession = mediaSession + player.addListener(listener) + } + + /** + * Release the underlying [MediaSession] + */ + fun release() { + player.removeListener(listener) + _mediaSession.release() + } + + override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult { + val availableSessionCommands = SessionCommands.Builder().apply { + if (session is MediaLibraryService.MediaLibrarySession) { + addSessionCommands(MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.commands) + } else { + addSessionCommands(MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.commands) + } + add(PillarboxSessionCommands.COMMAND_SEEK_DISABLED) + add(PillarboxSessionCommands.COMMAND_SEEK_ENABLED) + add(PillarboxSessionCommands.COMMAND_SEEK_GET) + }.build() + return MediaSession.ConnectionResult.accept(availableSessionCommands, MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS) + } + + override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { + val pillarbox = session.player as PillarboxPlayer + Log.d(TAG, "onPostConnect") + session.setSessionExtras( + controller, + Bundle().apply { + putBoolean("smoothSeekingEnabled", pillarbox.smoothSeekingEnabled) + } + ) + } + + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + DebugLogger.debug(TAG, "onCustomCommand ${customCommand.customAction} $args") + when (customCommand.customAction) { + PillarboxSessionCommands.SMOOTH_SEEKING_ENABLED -> { + if (session.player is PillarboxPlayer) { + (session.player as PillarboxPlayer).smoothSeekingEnabled = true + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + } + + PillarboxSessionCommands.SMOOTH_SEEKING_DISABLED -> { + if (session.player is PillarboxPlayer) { + (session.player as PillarboxPlayer).smoothSeekingEnabled = false + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + } + + PillarboxSessionCommands.SMOOTH_SEEKING_GET -> { + if (session.player is PillarboxPlayer) { + val state = (session.player as PillarboxPlayer).smoothSeekingEnabled + return Futures.immediateFuture( + SessionResult( + SessionResult.RESULT_SUCCESS, + Bundle().apply { + putBoolean( + "smoothSeekingEnabled", + state + ) + } + ) + ) + } + } + } + DebugLogger.warning(TAG, "Unsupported session command ${customCommand.customAction}") + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) + } + + override fun onSetMediaItems( + mediaSession: MediaSession, + controller: MediaSession.ControllerInfo, + mediaItems: MutableList, + startIndex: Int, + startPositionMs: Long + ): ListenableFuture { + return callback.onSetMediaItems(this, controller, mediaItems, startIndex, startPositionMs) + } + + override fun onAddMediaItems( + mediaSession: MediaSession, + controller: MediaSession.ControllerInfo, + mediaItems: MutableList + ): ListenableFuture> { + return callback.onAddMediaItems(this, controller, mediaItems) + } + + private inner class ComponentListener : PillarboxPlayer.Listener { + override fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) { + for (controllerInfo in _mediaSession.connectedControllers) { + _mediaSession.sendCustomCommand(controllerInfo, PillarboxSessionCommands.seekChangedCommand(smoothSeekingEnabled), Bundle.EMPTY) + _mediaSession.setSessionExtras( + controllerInfo, + Bundle().apply { + putBoolean("smoothSeekingEnabled", smoothSeekingEnabled) + } + ) + } + } + } + + companion object { + private const val TAG = "PillarboxMediaSession" + } +} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallbackTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallbackTest.kt deleted file mode 100644 index 85586858f..000000000 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/service/DefaultMediaSessionCallbackTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.service - -import androidx.media3.common.MediaItem -import androidx.media3.session.MediaSession -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.mockk.mockk -import org.junit.runner.RunWith -import java.util.concurrent.ExecutionException -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -@RunWith(AndroidJUnit4::class) -class DefaultMediaSessionCallbackTest { - private lateinit var mediaSessionCallback: MediaSession.Callback - - @BeforeTest - fun setup() { - mediaSessionCallback = object : DefaultMediaSessionCallback {} - } - - @Test(expected = ExecutionException::class) - fun `onAddMediaItems with missing localConfiguration`() { - val mediaItems = listOf( - MediaItem.Builder().setUri("https://host/media.mp4").build(), - MediaItem.Builder().build(), - ) - val future = mediaSessionCallback.onAddMediaItems(mockk(), mockk(), mediaItems) - - assertTrue(future.isDone) - - future.get() - } - - @Test(expected = ExecutionException::class) - fun `onAddMediaItems with empty mediaId`() { - val mediaItems = listOf( - MediaItem.Builder().setUri("https://host/media.mp4").build(), - MediaItem.Builder().setMediaId("").build(), - ) - val future = mediaSessionCallback.onAddMediaItems(mockk(), mockk(), mediaItems) - - assertTrue(future.isDone) - - future.get() - } - - @Test(expected = ExecutionException::class) - fun `onAddMediaItems with blank mediaId`() { - val mediaItems = listOf( - MediaItem.Builder().setUri("https://host/media.mp4").build(), - MediaItem.Builder().setMediaId(" ").build(), - ) - val future = mediaSessionCallback.onAddMediaItems(mockk(), mockk(), mediaItems) - - assertTrue(future.isDone) - - future.get() - } - - @Test - fun `onAddMediaItems with valid MediaItem`() { - val mediaItems = listOf( - MediaItem.Builder().setUri("https://host/media1.mp4").build(), - MediaItem.Builder().setUri("https://host/media2.mp4").build(), - ) - val future = mediaSessionCallback.onAddMediaItems(mockk(), mockk(), mediaItems) - - assertTrue(future.isDone) - - val result = future.get() - - assertEquals(mediaItems, result) - } -} From 21ac94b3e33b072b8a8921f61ddea46cc8be3f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Thu, 28 Mar 2024 17:28:15 +0100 Subject: [PATCH 06/27] Fix Android Auto, every media item have to be findable --- .../java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt index 87c7c51db..594e6e2bd 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt @@ -41,6 +41,7 @@ class DemoBrowser { init { val rootList = mapMediaIdToChildren[DEMO_BROWSABLE_ROOT] ?: mutableListOf() + mapMediaIdMediaItem[DEMO_BROWSABLE_ROOT] = rootMediaItem val listPlaylist = listOf( Playlist.StreamUrls, Playlist.StreamUrns, @@ -52,7 +53,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 From 8290f5799efe25af1c1a276e2df1c77e15413aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Tue, 2 Apr 2024 09:35:31 +0200 Subject: [PATCH 07/27] Fix default implementation onSetMediaItems --- .../player/session/PillarboxMediaSession.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt index 77162c815..a45565667 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt @@ -9,6 +9,7 @@ import android.content.Context import android.os.Bundle import android.util.Log import androidx.media3.common.MediaItem +import androidx.media3.common.util.Util import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession.MediaItemsWithStartPosition @@ -32,13 +33,10 @@ open class PillarboxMediaSession internal constructor(protected val callback: Ca mediaItems: MutableList, startIndex: Int, startPositionMs: Long - ): ListenableFuture { - for (mediaItem in mediaItems) { - if (mediaItem.localConfiguration == null) { - return Futures.immediateFailedFuture(UnsupportedOperationException()) - } - } - return Futures.immediateFuture(MediaItemsWithStartPosition(mediaItems, startIndex, startPositionMs)) + ): ListenableFuture { + return Util.transformFutureAsync( + onAddMediaItems(mediaSession, controller, mediaItems) + ) { input -> Futures.immediateFuture(MediaItemsWithStartPosition(input!!, startIndex, startPositionMs)) } } fun onAddMediaItems( From 5cdbfa3af2024323c36c9afce129154f44172080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 3 Apr 2024 09:13:03 +0200 Subject: [PATCH 08/27] wip --- .../pillarbox/demo/shared/data/DemoBrowser.kt | 3 + .../demo/service/DemoMediaLibraryService.kt | 43 +--- .../demo/service/DemoMediaSessionService.kt | 2 +- .../integrations/MediaControllerViewModel.kt | 26 +- .../player/session/PillarboxMediaBrowser.kt | 65 +++-- .../session/PillarboxMediaController.kt | 130 +++++----- .../PillarboxMediaLibraryService.kt | 9 +- .../session/PillarboxMediaLibrarySession.kt | 223 +++++++++++++----- .../player/session/PillarboxMediaSession.kt | 184 +++++++-------- .../PillarboxMediaSessionService.kt | 3 +- .../session/PillarboxSessionCommands.kt | 8 +- 11 files changed, 403 insertions(+), 293 deletions(-) rename pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/{service => session}/PillarboxMediaLibraryService.kt (94%) rename pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/{service => session}/PillarboxMediaSessionService.kt (97%) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt index 594e6e2bd..6cdb89e7a 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt @@ -20,6 +20,9 @@ import androidx.media3.common.MediaMetadata */ class DemoBrowser { + /** + * Every android auto navigable MediaItem accessed by id. + */ private val mapMediaIdMediaItem = mutableMapOf() private val mapMediaIdToChildren = mutableMapOf>() diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt index 43fbead97..f8218e4c5 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt @@ -14,7 +14,7 @@ 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 @@ -24,15 +24,9 @@ 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 Android auto application. * * Hints for testing : https://developer.android.com/training/cars/testing - * - * @constructor Create empty Demo media session service */ class DemoMediaLibraryService : PillarboxMediaLibraryService() { @@ -40,11 +34,9 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() { override fun onCreate() { super.onCreate() - Log.d(TAG, "onCreate") + demoBrowser = DemoBrowser() val player = PlayerModule.provideDefaultPlayer(this) setPlayer(player, DemoCallback()) - - demoBrowser = DemoBrowser() } override fun sessionActivity(): PendingIntent { @@ -58,24 +50,22 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() { ) } - override fun onDestroy() { - Log.d(TAG, "onDestroy") - super.onDestroy() - } - + /** + * Demo callback is used by Android Auto to create the navigation. + * */ private inner class DemoCallback : PillarboxMediaLibrarySession.Callback { override fun onGetLibraryRoot( session: PillarboxMediaLibrarySession, browser: MediaSession.ControllerInfo, params: LibraryParams? ): ListenableFuture> { - 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)) } @@ -88,7 +78,6 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() { pageSize: Int, params: LibraryParams? ): ListenableFuture>> { - 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) @@ -100,11 +89,8 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() { mediaId: String ): ListenableFuture> { val mediaItem = demoBrowser.getMediaItemFromId(mediaId) ?: MediaItem.EMPTY - Log.d(TAG, "onGetItem $mediaId // media ${mediaItem.mediaId} ${mediaItem.localConfiguration?.uri}") return Futures.immediateFuture( - LibraryResult.ofItem( - demoBrowser.getMediaItemFromId(mediaId) ?: MediaItem.EMPTY, LibraryParams.Builder().build() - ) + LibraryResult.ofItem(mediaItem, LibraryParams.Builder().build()) ) } @@ -113,24 +99,13 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() { controller: MediaSession.ControllerInfo, mediaItems: MutableList ): ListenableFuture> { - 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: PillarboxMediaLibrarySession, - browser: MediaSession.ControllerInfo, - query: String, - params: LibraryParams? - ): ListenableFuture> { - Log.d(TAG, "onSearch: $query") - return super.onSearch(session, browser, query, params) - } } companion object { diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt index b62682538..5849ff5b1 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt @@ -10,7 +10,7 @@ import android.util.Log import androidx.media3.common.C 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.session.PillarboxMediaSessionService import ch.srgssr.pillarbox.player.utils.PendingIntentUtils /** diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/integrations/MediaControllerViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/integrations/MediaControllerViewModel.kt index 85ef2ea19..c15e0e629 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/integrations/MediaControllerViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/integrations/MediaControllerViewModel.kt @@ -5,16 +5,13 @@ package ch.srgssr.pillarbox.demo.ui.showcases.integrations import android.app.Application -import android.content.ComponentName import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope -import androidx.media3.session.MediaBrowser -import androidx.media3.session.SessionToken import ch.srgssr.pillarbox.demo.service.DemoMediaLibraryService import ch.srgssr.pillarbox.player.extension.RATIONAL_ONE import ch.srgssr.pillarbox.player.extension.toRational +import ch.srgssr.pillarbox.player.session.PillarboxMediaBrowser import ch.srgssr.pillarbox.player.videoSizeAsFlow -import com.google.common.util.concurrent.MoreExecutors import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.MutableStateFlow @@ -30,20 +27,16 @@ import kotlinx.coroutines.flow.stateIn * * @param application */ -class MediaControllerViewModel(application: Application) : AndroidViewModel(application), MediaBrowser.Listener { - private val sessionToken = SessionToken(application, ComponentName(application, DemoMediaLibraryService::class.java)) - private val listenableFuture = MediaBrowser.Builder(application, sessionToken).setListener(this).buildAsync() - +class MediaControllerViewModel(application: Application) : AndroidViewModel(application) { /** * Player */ - val player = callbackFlow { - listenableFuture.addListener({ - val mediaBrowser = listenableFuture.get() // or using listenableFuture.await inside a coroutine - trySend(mediaBrowser) - }, MoreExecutors.directExecutor()) + val player = callbackFlow { + val mediaBrowser = PillarboxMediaBrowser.Builder(application, DemoMediaLibraryService::class.java).build() + mediaBrowser.smoothSeekingEnabled = true + trySend(mediaBrowser) awaitClose { - listenableFuture.cancel(false) + mediaBrowser.release() } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) @@ -59,9 +52,4 @@ class MediaControllerViewModel(application: Application) : AndroidViewModel(appl var pictureInPictureRatio = player.filterNotNull().flatMapLatest { mediaBrowser -> mediaBrowser.videoSizeAsFlow().map { it.toRational() } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), RATIONAL_ONE) - - override fun onCleared() { - super.onCleared() - MediaBrowser.releaseFuture(listenableFuture) - } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt index 5d60e59a4..eb9615e4f 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt @@ -8,40 +8,59 @@ import android.content.ComponentName import android.content.Context import androidx.annotation.IntRange import androidx.media3.session.MediaBrowser +import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService.LibraryParams import androidx.media3.session.SessionToken -import ch.srgssr.pillarbox.player.service.PillarboxMediaSessionService -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.guava.await -import kotlinx.coroutines.withContext -class PillarboxMediaBrowser private constructor() : PillarboxMediaController(), MediaBrowser.Listener { +class PillarboxMediaBrowser private constructor() : PillarboxMediaController() { private lateinit var mediaBrowser: MediaBrowser - class Builder(private val context: Context, private val clazz: Class) { - - suspend fun build(): PillarboxMediaController { - return withContext(Dispatchers.IO) { - val pillarboxMediaController = PillarboxMediaBrowser() - val componentName = ComponentName(context, clazz) - val sessionToken = SessionToken(context, componentName) - val mediaBrowser = MediaBrowser.Builder(context, sessionToken) - .setListener(pillarboxMediaController) - .buildAsync() - .await() - pillarboxMediaController.setMediaBrowser(mediaBrowser) - pillarboxMediaController - } + private class MediaBrowserListenerImpl( + listener: Listener, + mediaBrowser: PillarboxMediaBrowser + ) : MediaControllerListenerImpl(listener, mediaBrowser), MediaBrowser.Listener { + + override fun onChildrenChanged(browser: MediaBrowser, parentId: String, itemCount: Int, params: LibraryParams?) { + (listener as Listener).onChildrenChanged(mediaController as PillarboxMediaBrowser, parentId, itemCount, params) + } + + override fun onSearchResultChanged(browser: MediaBrowser, query: String, itemCount: Int, params: LibraryParams?) { + (listener as Listener).onSearchResultChanged(mediaController as PillarboxMediaBrowser, query, itemCount, params) } } - override fun onChildrenChanged(browser: MediaBrowser, parentId: String, itemCount: Int, params: LibraryParams?) { - super.onChildrenChanged(browser, parentId, itemCount, params) - TODO("Implement maybe a custom listener") + class Builder(private val context: Context, private val clazz: Class) { + private var listener: Listener = object : Listener {} + + fun setListener(listener: Listener): Builder { + this.listener = listener + return this + } + + suspend fun build(): PillarboxMediaBrowser { + val pillarboxMediaController = PillarboxMediaBrowser() + val listener = MediaBrowserListenerImpl(listener, pillarboxMediaController) + val componentName = ComponentName(context, clazz) + val sessionToken = SessionToken(context, componentName) + val mediaBrowser = MediaBrowser.Builder(context, sessionToken) + .setListener(listener) + .buildAsync() + .await() + pillarboxMediaController.setMediaBrowser(mediaBrowser) + return pillarboxMediaController + } } - override fun onSearchResultChanged(browser: MediaBrowser, query: String, itemCount: Int, params: LibraryParams?) { - super.onSearchResultChanged(browser, query, itemCount, params) + interface Listener : PillarboxMediaController.Listener { + fun onChildrenChanged( + browser: PillarboxMediaBrowser, + parentId: String, + itemCount: Int, + params: LibraryParams? + ) {} + + fun onSearchResultChanged(browser: PillarboxMediaBrowser, query: String, itemCount: Int, params: LibraryParams?) {} } internal fun setMediaBrowser(mediaBrowser: MediaBrowser) { diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt index 36e5c9c34..ba1b72815 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt @@ -51,16 +51,24 @@ import kotlinx.coroutines.guava.await * * @constructor Create empty Pillarbox media controller */ -open class PillarboxMediaController internal constructor() : PillarboxPlayer, MediaController.Listener { +open class PillarboxMediaController internal constructor() : PillarboxPlayer { class Builder(private val context: Context, private val clazz: Class) { + private var listener: Listener = object : Listener {} + + fun setListener(listener: Listener): Builder { + this.listener = listener + return this + } + suspend fun build(): PillarboxMediaController { val pillarboxMediaController = PillarboxMediaController() + val listener = MediaControllerListenerImpl(listener, pillarboxMediaController) val componentName = ComponentName(context, clazz) val sessionToken = SessionToken(context, componentName) val mediaController = MediaController.Builder(context, sessionToken) - .setListener(pillarboxMediaController) + .setListener(listener) .buildAsync() .await() @@ -69,6 +77,56 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer, Me } } + interface Listener { + fun onCustomCommand( + controller: PillarboxMediaController, + command: SessionCommand, + args: Bundle + ): ListenableFuture { + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) + } + + fun onAvailableSessionCommandsChanged(controller: PillarboxMediaController, commands: SessionCommands) {} + + fun onDisconnected(controller: PillarboxMediaController) {} + + fun onExtrasChanged(controller: PillarboxMediaController, extras: Bundle) {} + } + + internal open class MediaControllerListenerImpl( + val listener: Listener, + val mediaController: PillarboxMediaController + ) : MediaController.Listener { + override fun onCustomCommand( + controller: MediaController, + command: SessionCommand, + args: Bundle + ): ListenableFuture { + DebugLogger.debug(TAG, "onCustomCommand ${command.customAction} ${command.customExtras}") + when (command.customAction) { + PillarboxSessionCommands.SMOOTH_SEEKING_CHANGED -> { + val smoothSeeking = command.customExtras.getBoolean("smoothSeekingEnabled") + mediaController.smoothSeekingEnabled = smoothSeeking + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + } + return listener.onCustomCommand(mediaController, command, args) + } + + override fun onAvailableSessionCommandsChanged(controller: MediaController, commands: SessionCommands) { + listener.onAvailableSessionCommandsChanged(mediaController, commands) + } + + override fun onDisconnected(controller: MediaController) { + listener.onDisconnected(mediaController) + } + + override fun onExtrasChanged(controller: MediaController, extras: Bundle) { + Log.i(TAG, "onExtrasChanged $extras") + listener.onExtrasChanged(mediaController, extras) + } + } + private lateinit var mediaController: MediaController private val listeners = HashSet() val connectedToken: SessionToken? @@ -91,20 +149,12 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer, Me val availableSessionCommands: SessionCommands get() = mediaController.getAvailableSessionCommands() - override var smoothSeekingEnabled: Boolean = false + override var smoothSeekingEnabled: Boolean set(value) { - if (field != value) { - if (value) { - sendCustomCommand(PillarboxSessionCommands.COMMAND_SEEK_ENABLED, Bundle.EMPTY) - } else { - sendCustomCommand(PillarboxSessionCommands.COMMAND_SEEK_DISABLED, Bundle.EMPTY) - } - field = value - val listeners = HashSet(listeners) - for (listener in listeners) { - listener.onSmoothSeekingEnabledChanged(value) - } - } + sendCustomCommand(PillarboxSessionCommands.setSmoothSeekingCommand(value), Bundle.EMPTY) + } + get() { + return sessionExtras.getBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG) } override var trackingEnabled: Boolean @@ -113,19 +163,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer, Me internal fun setMediaController(mediaController: MediaController) { this.mediaController = mediaController - - // TODO: Fetch initial data - // Called from wrong thread if we load not from application thread - sendCustomCommand(PillarboxSessionCommands.COMMAND_SEEK_GET, Bundle.EMPTY).also { - it.addListener({ - val result = it.get() - DebugLogger.debug(TAG, "Fetch initial data ${result.extras}") - if (result.resultCode == SessionResult.RESULT_SUCCESS) { - smoothSeekingEnabled = result.extras.getBoolean("smoothSeekingEnabled") - } - }, MoreExecutors.directExecutor()) - } - Log.d(TAG, "fromSessionExtras = $sessionExtras") + DebugLogger.debug(TAG, "setMediaController $mediaController smoothSeekingEnabled = $smoothSeekingEnabled") } /** @@ -146,34 +184,18 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer, Me * @See [MediaController.sendCustomCommand] */ fun sendCustomCommand(command: SessionCommand, args: Bundle): ListenableFuture { + val result = mediaController.sendCustomCommand(command, args) + result.addListener( + { + val resultSession = result.get() + if (resultSession.resultCode != SessionResult.RESULT_SUCCESS) { + DebugLogger.warning(TAG, "SessionResult ${command.customAction} code ${resultSession.resultCode}") + } + }, MoreExecutors.directExecutor() + ) return mediaController.sendCustomCommand(command, args) } - override fun onCustomCommand(controller: MediaController, command: SessionCommand, args: Bundle): ListenableFuture { - DebugLogger.debug(TAG, "onCustomCommand ${command.customAction} ${command.customExtras}") - when (command.customAction) { - PillarboxSessionCommands.SMOOTH_SEEKING_CHANGED -> { - val smoothSeeking = command.customExtras.getBoolean("smoothSeekingEnabled") - this.smoothSeekingEnabled = smoothSeeking - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } - } - return super.onCustomCommand(controller, command, args) - } - - override fun onAvailableSessionCommandsChanged(controller: MediaController, commands: SessionCommands) { - super.onAvailableSessionCommandsChanged(controller, commands) - } - - override fun onDisconnected(controller: MediaController) { - super.onDisconnected(controller) - } - - override fun onExtrasChanged(controller: MediaController, extras: Bundle) { - super.onExtrasChanged(controller, extras) - Log.i(TAG, "onExtrasChanged $extras") - } - /** * @see [MediaController.isSessionCommandAvailable] */ @@ -744,6 +766,6 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer, Me } companion object { - private const val TAG = " PillarboxMediaController" + private const val TAG = PillarboxMediaSession.TAG } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaLibraryService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt similarity index 94% rename from pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaLibraryService.kt rename to pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt index e69690ffd..ac4080083 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaLibraryService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt @@ -2,7 +2,7 @@ * Copyright (c) SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ -package ch.srgssr.pillarbox.player.service +package ch.srgssr.pillarbox.player.session import android.app.PendingIntent import android.content.Intent @@ -10,8 +10,8 @@ import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSession.ControllerInfo import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer -import ch.srgssr.pillarbox.player.session.PillarboxMediaLibrarySession import ch.srgssr.pillarbox.player.utils.PendingIntentUtils /** @@ -81,12 +81,11 @@ abstract class PillarboxMediaLibraryService : MediaLibraryService() { sessionActivity()?.let { setSessionActivity(it) } - } - .build() + }.build() } } - override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? { + override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession? { return mediaSession?.mediaSession } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt index f9784b5c6..bed4a0a97 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt @@ -5,42 +5,88 @@ package ch.srgssr.pillarbox.player.session import android.app.PendingIntent -import android.content.Context +import androidx.annotation.IntRange import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata import androidx.media3.session.LibraryResult import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService.MediaLibrarySession import androidx.media3.session.MediaSession import ch.srgssr.pillarbox.player.PillarboxPlayer +import ch.srgssr.pillarbox.player.session.PillarboxMediaLibrarySession.Builder 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 -open class PillarboxMediaLibrarySession internal constructor(callback: Callback) : - PillarboxMediaSession(callback), - MediaLibrarySession.Callback { +/** + * An extended [PillarboxMediaSession] for the [PillarboxMediaLibraryService]. + * Build an instance with [Builder] and return it from [PillarboxMediaLibraryService.onGetPillarboxSession] + * or [PillarboxMediaLibraryService.onGetSession] with [PillarboxMediaLibrarySession.mediaSession]. + * + * @see MediaLibrarySession + * @see PillarboxMediaLibraryService + * @see PillarboxMediaBrowser + */ +open class PillarboxMediaLibrarySession internal constructor() : + PillarboxMediaSession() { + /** + * An extended [PillarboxMediaSession.Callback] for the [PillarboxMediaLibrarySession]. + *

When you return [LibraryResult] with [MediaItem] media items, each item must + * have valid [MediaItem.mediaId ]and specify [MediaMetadata.isBrowsable] and [MediaMetadata.isPlayable] in its [MediaItem.mediaMetadata]. + * @see MediaLibrarySession.Callback + */ interface Callback : PillarboxMediaSession.Callback { + /** + * Called when a [PillarboxMediaBrowser] requests the root [MediaItem]. + * @see MediaLibrarySession.Callback.onGetLibraryRoot + * + * @param session The session for this event. + * @param browser The browser information. + * @param params The optional parameters passed by the browser. + * @return A pending result that will be resolved with a root media item. + */ fun onGetLibraryRoot( session: PillarboxMediaLibrarySession, browser: MediaSession.ControllerInfo, - params: MediaLibraryService.LibraryParams? + params: MediaLibraryService.LibraryParams?, ): ListenableFuture> { return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) } + /** + * Called when a [PillarboxMediaBrowser] requests the child media items of the given parent id. + * @see MediaLibrarySession.Callback.onGetChildren + * + * @param session The session for this event. + * @param browser The browser information. + * @param parentId The non-empty parent id. + * @param page The page number to get the paginated result starting from 0. + * @param pageSize The page size to get the paginated result. Will be greater than 0. + * @param params The optional parameters passed by the browser. + * @return A pending result that will be resolved with a list of media items. + */ fun onGetChildren( session: PillarboxMediaLibrarySession, browser: MediaSession.ControllerInfo, parentId: String, - page: Int, - pageSize: Int, - params: MediaLibraryService.LibraryParams? + @IntRange(from = 0) page: Int, + @IntRange(from = 1) pageSize: Int, + params: MediaLibraryService.LibraryParams?, ): ListenableFuture>> { return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) } + /** + * Called when a [PillarboxMediaBrowser] requests a [MediaItem] from mediaId. + * @see MediaLibrarySession.Callback.onGetItem + * + * @param session The session for this event. + * @param browser The browser information. + * @param mediaId The non-empty media id of the requested item. + * @return A pending result that will be resolved with a media item. + */ fun onGetItem( session: PillarboxMediaLibrarySession, browser: MediaSession.ControllerInfo, @@ -49,6 +95,16 @@ open class PillarboxMediaLibrarySession internal constructor(callback: Callback) return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) } + /** + * Called when a [androidx.media3.session.MediaBrowser] requests a search. + * @see MediaLibrarySession.Callback.onSearch + * + * @param session The session for this event. + * @param browser The browser information. + * @param query The non-empty search query. + * @param params The optional parameters passed by the browser. + * @return A pending result that will be resolved with a result code. + */ fun onSearch( session: PillarboxMediaLibrarySession, browser: MediaSession.ControllerInfo, @@ -58,6 +114,18 @@ open class PillarboxMediaLibrarySession internal constructor(callback: Callback) return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) } + /** + * Called when a [PillarboxMediaBrowser] requests the child media items of the given parent id. + * @see MediaLibrarySession.Callback.onGetSearchResult + * + * @param session The session for this event. + * @param browser The browser information. + * @param query The non-empty search query. + * @param page The page number to get the paginated result starting from 0. + * @param pageSize The page size to get the paginated result. Will be greater than 0. + * @param params The optional parameters passed by the browser. + * @return A pending result that will be resolved with a list of media items. + */ fun onGetSearchResult( session: PillarboxMediaLibrarySession, browser: MediaSession.ControllerInfo, @@ -70,23 +138,55 @@ open class PillarboxMediaLibrarySession internal constructor(callback: Callback) } } - class Builder(private val context: Context, private val player: PillarboxPlayer, private val callback: Callback) { - private var pendingIntent: PendingIntent? = PendingIntentUtils.getDefaultPendingIntent(context) + /** + * A builder for [PillarboxMediaLibrarySession]. + * + * Any incoming requests from the [PillarboxMediaBrowser] will be handled on the application + * thread of the underlying [PillarboxPlayer]. + * + * @property service The [MediaLibraryService] that instantiates the [PillarboxMediaLibrarySession]. + * @property player The underlying player to perform playback and handle transport controls. + * @property callback The [Callback] to handle requests from [PillarboxMediaBrowser]. + */ + class Builder( + private val service: MediaLibraryService, + private val player: PillarboxPlayer, + private val callback: Callback, + ) { + private var pendingIntent: PendingIntent? = PendingIntentUtils.getDefaultPendingIntent(service) private var id: String? = null + /** + * Set session activity + * @see MediaLibrarySession.Builder.setSessionActivity + * @param pendingIntent The [PendingIntent]. + * @return the builder for convenience. + */ fun setSessionActivity(pendingIntent: PendingIntent): Builder { this.pendingIntent = pendingIntent return this } + /** + * Set id + * @see MediaLibrarySession.Builder.setId + * @param id The ID. Must be unique among all sessions per package. + * @return the builder for convenience. + */ fun setId(id: String): Builder { this.id = id return this } + /** + * Build + * + * @return a new [PillarboxMediaLibrarySession] + */ fun build(): PillarboxMediaLibrarySession { - val pillarboxMediaSession = PillarboxMediaLibrarySession(callback) - val mediaSessionBuilder = MediaLibrarySession.Builder(context, player, pillarboxMediaSession) + val pillarboxMediaSession = PillarboxMediaLibrarySession() + val media3Callback = MediaLibraryCallbackImpl(callback, pillarboxMediaSession) + val mediaSessionBuilder = MediaLibrarySession.Builder(service, player, media3Callback) val mediaSession = mediaSessionBuilder.apply { id?.let { setId(it) } pendingIntent?.let { setSessionActivity(it) } @@ -99,58 +199,61 @@ open class PillarboxMediaLibrarySession internal constructor(callback: Callback) override val mediaSession: MediaLibrarySession get() = super.mediaSession as MediaLibrarySession - override fun onGetLibraryRoot( - session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, - params: MediaLibraryService.LibraryParams? - ): ListenableFuture> { - return (callback as Callback).onGetLibraryRoot(this, browser, params) - } + internal class MediaLibraryCallbackImpl(callback: Callback, mediaSession: PillarboxMediaLibrarySession) : + MediaSessionCallbackImpl(callback, mediaSession), MediaLibrarySession.Callback { + override fun onGetLibraryRoot( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture> { + return (callback as Callback).onGetLibraryRoot(this.mediaSession as PillarboxMediaLibrarySession, browser, params) + } - override fun onGetChildren( - session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, - parentId: String, - page: Int, - pageSize: Int, - params: MediaLibraryService.LibraryParams? - ): ListenableFuture>> { - return (callback as Callback).onGetChildren(this, browser, parentId, page, pageSize, params) - } + override fun onGetChildren( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + parentId: String, + page: Int, + pageSize: Int, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture>> { + return (callback as Callback).onGetChildren(this.mediaSession as PillarboxMediaLibrarySession, browser, parentId, page, pageSize, params) + } - override fun onGetItem( - session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, - mediaId: String - ): ListenableFuture> { - return (callback as Callback).onGetItem(this, browser, mediaId) - } + override fun onGetItem( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + mediaId: String + ): ListenableFuture> { + return (callback as Callback).onGetItem(this.mediaSession as PillarboxMediaLibrarySession, browser, mediaId) + } - override fun onSearch( - session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, - query: String, - params: MediaLibraryService.LibraryParams? - ): ListenableFuture> { - return (callback as Callback).onSearch(this, browser, query, params) - } + override fun onSearch( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + query: String, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture> { + return (callback as Callback).onSearch(this.mediaSession as PillarboxMediaLibrarySession, browser, query, params) + } - override fun onGetSearchResult( - session: MediaLibrarySession, - browser: MediaSession.ControllerInfo, - query: String, - page: Int, - pageSize: Int, - params: MediaLibraryService.LibraryParams? - ): ListenableFuture>> { - return (callback as Callback).onGetSearchResult(this, browser, query, page, pageSize, params) - } + override fun onGetSearchResult( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + query: String, + page: Int, + pageSize: Int, + params: MediaLibraryService.LibraryParams? + ): ListenableFuture>> { + return (callback as Callback).onGetSearchResult(this.mediaSession as PillarboxMediaLibrarySession, browser, query, page, pageSize, params) + } - override fun onAddMediaItems( - mediaSession: MediaSession, - controller: MediaSession.ControllerInfo, - mediaItems: MutableList - ): ListenableFuture> { - return (callback as Callback).onAddMediaItems(this, controller, mediaItems) + override fun onAddMediaItems( + mediaSession: MediaSession, + controller: MediaSession.ControllerInfo, + mediaItems: MutableList + ): ListenableFuture> { + return (callback as Callback).onAddMediaItems(this.mediaSession as PillarboxMediaLibrarySession, controller, mediaItems) + } } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt index a45565667..8e52ad7b0 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt @@ -24,9 +24,10 @@ import com.google.common.util.concurrent.ListenableFuture /** * PillarboxMediaSession link together a [MediaSession] to a [PillarboxPlayer]. */ -open class PillarboxMediaSession internal constructor(protected val callback: Callback) : MediaSession.Callback { +open class PillarboxMediaSession internal constructor() { interface Callback { + fun onSetMediaItems( mediaSession: PillarboxMediaSession, controller: MediaSession.ControllerInfo, @@ -74,9 +75,10 @@ open class PillarboxMediaSession internal constructor(protected val callback: Ca } fun build(): PillarboxMediaSession { - val pillarboxMediaSession = PillarboxMediaSession(callback) + val pillarboxMediaSession = PillarboxMediaSession() + val media3SessionCallback = MediaSessionCallbackImpl(callback, pillarboxMediaSession) val mediaSession = mediaSessionBuilder - .setCallback(pillarboxMediaSession) + .setCallback(media3SessionCallback) .build() pillarboxMediaSession.setMediaSession(mediaSession) return pillarboxMediaSession @@ -99,14 +101,11 @@ open class PillarboxMediaSession internal constructor(protected val callback: Ca this.player.removeListener(listener) _mediaSession.player = player player.addListener(listener) - - // Update MediaSession with new PillarboxPlayer state for (controllerInfo in _mediaSession.connectedControllers) { - // it.sendCustomCommand(controllerInfo, PillarboxSessionCommands.seekChangedCommand(smoothSeekingEnabled), Bundle.EMPTY) _mediaSession.setSessionExtras( controllerInfo, - Bundle().apply { - putBoolean("smoothSeekingEnabled", player.smoothSeekingEnabled) + Bundle(_mediaSession.sessionExtras).apply { + putBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG, player.smoothSeekingEnabled) } ) } @@ -126,107 +125,108 @@ open class PillarboxMediaSession internal constructor(protected val callback: Ca _mediaSession.release() } - override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult { - val availableSessionCommands = SessionCommands.Builder().apply { - if (session is MediaLibraryService.MediaLibrarySession) { - addSessionCommands(MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.commands) - } else { - addSessionCommands(MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.commands) - } - add(PillarboxSessionCommands.COMMAND_SEEK_DISABLED) - add(PillarboxSessionCommands.COMMAND_SEEK_ENABLED) - add(PillarboxSessionCommands.COMMAND_SEEK_GET) - }.build() - return MediaSession.ConnectionResult.accept(availableSessionCommands, MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS) - } - - override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { - val pillarbox = session.player as PillarboxPlayer - Log.d(TAG, "onPostConnect") - session.setSessionExtras( - controller, - Bundle().apply { - putBoolean("smoothSeekingEnabled", pillarbox.smoothSeekingEnabled) + private inner class ComponentListener : PillarboxPlayer.Listener { + override fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) { + for (controllerInfo in _mediaSession.connectedControllers) { + // _mediaSession.sendCustomCommand(controllerInfo, PillarboxSessionCommands.seekChangedCommand(smoothSeekingEnabled), Bundle.EMPTY) + Log.d(TAG, "onSmoothSeekingEnabledChanged $smoothSeekingEnabled") + _mediaSession.sessionExtras = Bundle(_mediaSession.sessionExtras).apply { + putBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG, player.smoothSeekingEnabled) + } + /* + _mediaSession.setSessionExtras( + controllerInfo, + Bundle(_mediaSession.sessionExtras).apply { + putBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG, smoothSeekingEnabled) + } + )*/ } - ) + } } - override fun onCustomCommand( - session: MediaSession, - controller: MediaSession.ControllerInfo, - customCommand: SessionCommand, - args: Bundle - ): ListenableFuture { - DebugLogger.debug(TAG, "onCustomCommand ${customCommand.customAction} $args") - when (customCommand.customAction) { - PillarboxSessionCommands.SMOOTH_SEEKING_ENABLED -> { - if (session.player is PillarboxPlayer) { - (session.player as PillarboxPlayer).smoothSeekingEnabled = true - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + internal open class MediaSessionCallbackImpl( + val callback: Callback, + val mediaSession: PillarboxMediaSession + ) : MediaSession.Callback { + + override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult { + val availableSessionCommands = SessionCommands.Builder().apply { + if (session is MediaLibraryService.MediaLibrarySession) { + addSessionCommands(MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.commands) + } else { + addSessionCommands(MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.commands) } + add(PillarboxSessionCommands.COMMAND_SEEK_ENABLED) + }.build() + val pillarbox = session.player as PillarboxPlayer + Log.d(TAG, "onConnect") + val sessionExtras = Bundle(session.sessionExtras).apply { + putBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG, pillarbox.smoothSeekingEnabled) } + return MediaSession.ConnectionResult.AcceptedResultBuilder(session) + .setAvailableSessionCommands(availableSessionCommands) + .setSessionExtras(sessionExtras) + .build() + // return MediaSession.ConnectionResult.accept(availableSessionCommands, MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS) + } + + override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { + } - PillarboxSessionCommands.SMOOTH_SEEKING_DISABLED -> { - if (session.player is PillarboxPlayer) { - (session.player as PillarboxPlayer).smoothSeekingEnabled = false - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + DebugLogger.debug(TAG, "onCustomCommand ${customCommand.customAction} ${customCommand.customExtras}") + when (customCommand.customAction) { + PillarboxSessionCommands.SMOOTH_SEEKING_ENABLED -> { + if (session.player is PillarboxPlayer) { + (session.player as PillarboxPlayer).smoothSeekingEnabled = + customCommand.customExtras.getBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG) + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } } - } - PillarboxSessionCommands.SMOOTH_SEEKING_GET -> { - if (session.player is PillarboxPlayer) { - val state = (session.player as PillarboxPlayer).smoothSeekingEnabled - return Futures.immediateFuture( - SessionResult( - SessionResult.RESULT_SUCCESS, - Bundle().apply { - putBoolean( - "smoothSeekingEnabled", - state - ) - } + PillarboxSessionCommands.SMOOTH_SEEKING_GET -> { + if (session.player is PillarboxPlayer) { + val state = (session.player as PillarboxPlayer).smoothSeekingEnabled + return Futures.immediateFuture( + SessionResult( + SessionResult.RESULT_SUCCESS, + Bundle().apply { + putBoolean("smoothSeekingEnabled", state) + } + ) ) - ) + } } } + DebugLogger.warning(TAG, "Unsupported session command ${customCommand.customAction}") + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) } - DebugLogger.warning(TAG, "Unsupported session command ${customCommand.customAction}") - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) - } - - override fun onSetMediaItems( - mediaSession: MediaSession, - controller: MediaSession.ControllerInfo, - mediaItems: MutableList, - startIndex: Int, - startPositionMs: Long - ): ListenableFuture { - return callback.onSetMediaItems(this, controller, mediaItems, startIndex, startPositionMs) - } - override fun onAddMediaItems( - mediaSession: MediaSession, - controller: MediaSession.ControllerInfo, - mediaItems: MutableList - ): ListenableFuture> { - return callback.onAddMediaItems(this, controller, mediaItems) - } + override fun onSetMediaItems( + mediaSession: MediaSession, + controller: MediaSession.ControllerInfo, + mediaItems: MutableList, + startIndex: Int, + startPositionMs: Long + ): ListenableFuture { + return callback.onSetMediaItems(this.mediaSession, controller, mediaItems, startIndex, startPositionMs) + } - private inner class ComponentListener : PillarboxPlayer.Listener { - override fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) { - for (controllerInfo in _mediaSession.connectedControllers) { - _mediaSession.sendCustomCommand(controllerInfo, PillarboxSessionCommands.seekChangedCommand(smoothSeekingEnabled), Bundle.EMPTY) - _mediaSession.setSessionExtras( - controllerInfo, - Bundle().apply { - putBoolean("smoothSeekingEnabled", smoothSeekingEnabled) - } - ) - } + override fun onAddMediaItems( + mediaSession: MediaSession, + controller: MediaSession.ControllerInfo, + mediaItems: MutableList + ): ListenableFuture> { + return callback.onAddMediaItems(this.mediaSession, controller, mediaItems) } } companion object { - private const val TAG = "PillarboxMediaSession" + internal const val TAG = "PillarboxMediaSession" } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt similarity index 97% rename from pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt rename to pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt index 603f29a1b..e392b5bc1 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PillarboxMediaSessionService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt @@ -2,7 +2,7 @@ * Copyright (c) SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ -package ch.srgssr.pillarbox.player.service +package ch.srgssr.pillarbox.player.session import android.app.PendingIntent import android.content.Intent @@ -11,7 +11,6 @@ import androidx.media3.common.Player import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer -import ch.srgssr.pillarbox.player.session.PillarboxMediaSession import ch.srgssr.pillarbox.player.utils.PendingIntentUtils /** diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt index 3a965db1d..bc14a2035 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt @@ -8,15 +8,17 @@ import android.os.Bundle import androidx.media3.session.SessionCommand internal object PillarboxSessionCommands { + const val SMOOTH_SEEKING_ARG = "pillarbox.smoothSeekingEnabled" const val SMOOTH_SEEKING_ENABLED = "pillarbox.smooth.seeking.enabled" const val SMOOTH_SEEKING_DISABLED = "pillarbox.smooth.seeking.disabled" const val SMOOTH_SEEKING_CHANGED = "pillarbox.smooth.seeking.changed" const val SMOOTH_SEEKING_GET = "pillarbox.smooth.seeking.get" val COMMAND_SEEK_ENABLED = SessionCommand(SMOOTH_SEEKING_ENABLED, Bundle.EMPTY) - val COMMAND_SEEK_DISABLED = SessionCommand(SMOOTH_SEEKING_DISABLED, Bundle.EMPTY) - val COMMAND_SEEK_GET = SessionCommand(SMOOTH_SEEKING_GET, Bundle.EMPTY) + + fun setSmoothSeekingCommand(smoothSeekingEnabled: Boolean) = + SessionCommand(SMOOTH_SEEKING_ENABLED, Bundle().apply { putBoolean(SMOOTH_SEEKING_ARG, smoothSeekingEnabled) }) fun seekChangedCommand(smoothSeekingEnabled: Boolean) = - SessionCommand(SMOOTH_SEEKING_CHANGED, Bundle().apply { putBoolean("smoothSeekingEnabled", smoothSeekingEnabled) }) + SessionCommand(SMOOTH_SEEKING_CHANGED, Bundle().apply { putBoolean(SMOOTH_SEEKING_ARG, smoothSeekingEnabled) }) } From 4212c7679c137f9e3ae81552e6b1bbc0d05c2922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 3 Apr 2024 16:32:23 +0200 Subject: [PATCH 09/27] handle all events --- .../integrations/MediaControllerViewModel.kt | 2 +- .../pillarbox/player/PillarboxPlayer.kt | 4 +- .../player/exoplayer/PillarboxExoPlayer.kt | 12 +++- .../session/PillarboxMediaController.kt | 43 ++++++++----- .../player/session/PillarboxMediaSession.kt | 63 ++++++++----------- .../session/PillarboxSessionCommands.kt | 18 ++++-- .../player/session/PlayerSessionState.kt | 30 +++++++++ 7 files changed, 108 insertions(+), 64 deletions(-) create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PlayerSessionState.kt diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/integrations/MediaControllerViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/integrations/MediaControllerViewModel.kt index c15e0e629..a9ce1cdd0 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/integrations/MediaControllerViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/integrations/MediaControllerViewModel.kt @@ -28,12 +28,12 @@ import kotlinx.coroutines.flow.stateIn * @param application */ class MediaControllerViewModel(application: Application) : AndroidViewModel(application) { + /** * Player */ val player = callbackFlow { val mediaBrowser = PillarboxMediaBrowser.Builder(application, DemoMediaLibraryService::class.java).build() - mediaBrowser.smoothSeekingEnabled = true trySend(mediaBrowser) awaitClose { mediaBrowser.release() diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt index f4629c473..145a2802d 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt @@ -21,7 +21,9 @@ interface PillarboxPlayer : Player { * * @param smoothSeekingEnabled The new value of [smoothSeekingEnabled] */ - fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) + fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) {} + + fun onTrackingEnabledChanged(trackingEnabled: Boolean) {} } /** diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/exoplayer/PillarboxExoPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/exoplayer/PillarboxExoPlayer.kt index 03abb3ba0..5d0f92f9b 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/exoplayer/PillarboxExoPlayer.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/exoplayer/PillarboxExoPlayer.kt @@ -5,7 +5,6 @@ package ch.srgssr.pillarbox.player.exoplayer import android.content.Context -import android.util.Log import androidx.annotation.VisibleForTesting import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException @@ -50,7 +49,6 @@ class PillarboxExoPlayer internal constructor( private val window = Window() override var smoothSeekingEnabled: Boolean = false set(value) { - Log.d("Coucou", "PillarboxExoPlayer smoothSeekingEnabled to $value") if (value != field) { field = value if (!value) { @@ -71,7 +69,15 @@ class PillarboxExoPlayer internal constructor( */ override var trackingEnabled: Boolean - set(value) = itemTracker?.let { it.enabled = value } ?: Unit + set(value) = itemTracker?.let { + if (it.enabled != value) { + it.enabled = value + val listeners = HashSet(listeners) + for (listener in listeners) { + listener.onTrackingEnabledChanged(value) + } + } + } ?: Unit get() = itemTracker?.enabled ?: false init { diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt index ba1b72815..3b4af5319 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt @@ -9,7 +9,6 @@ import android.content.ComponentName import android.content.Context import android.os.Bundle import android.os.Looper -import android.util.Log import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView @@ -103,13 +102,6 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { args: Bundle ): ListenableFuture { DebugLogger.debug(TAG, "onCustomCommand ${command.customAction} ${command.customExtras}") - when (command.customAction) { - PillarboxSessionCommands.SMOOTH_SEEKING_CHANGED -> { - val smoothSeeking = command.customExtras.getBoolean("smoothSeekingEnabled") - mediaController.smoothSeekingEnabled = smoothSeeking - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } - } return listener.onCustomCommand(mediaController, command, args) } @@ -122,12 +114,14 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } override fun onExtrasChanged(controller: MediaController, extras: Bundle) { - Log.i(TAG, "onExtrasChanged $extras") + mediaController.onSessionExtrasChanged(extras) listener.onExtrasChanged(mediaController, extras) } } private lateinit var mediaController: MediaController + private lateinit var playerSessionState: PlayerSessionState + private val listeners = HashSet() val connectedToken: SessionToken? get() = mediaController.connectedToken @@ -151,19 +145,36 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { override var smoothSeekingEnabled: Boolean set(value) { - sendCustomCommand(PillarboxSessionCommands.setSmoothSeekingCommand(value), Bundle.EMPTY) + sendCustomCommand(PillarboxSessionCommands.setSmoothSeekingCommand(value)) } get() { - return sessionExtras.getBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG) + return playerSessionState.smoothSeekingEnabled } override var trackingEnabled: Boolean - get() = TODO("Not yet implemented") - set(value) {} + set(value) { + sendCustomCommand(PillarboxSessionCommands.setTrackerEnabled(value)) + } + get() { + return playerSessionState.trackingEnabled + } internal fun setMediaController(mediaController: MediaController) { this.mediaController = mediaController - DebugLogger.debug(TAG, "setMediaController $mediaController smoothSeekingEnabled = $smoothSeekingEnabled") + playerSessionState = PlayerSessionState(sessionExtras) + DebugLogger.debug(TAG, "setMediaController $mediaController state = $playerSessionState") + } + + private fun onSessionExtrasChanged(extras: Bundle) { + val oldValue = playerSessionState + val newValue = PlayerSessionState(extras) + DebugLogger.debug(TAG, "onSessionExtrasChanged($oldValue -> $newValue)") + playerSessionState = newValue + if (oldValue.smoothSeekingEnabled != newValue.smoothSeekingEnabled) { + for (listener in listeners) { + listener.onSmoothSeekingEnabledChanged(newValue.smoothSeekingEnabled) + } + } } /** @@ -183,7 +194,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { /** * @See [MediaController.sendCustomCommand] */ - fun sendCustomCommand(command: SessionCommand, args: Bundle): ListenableFuture { + fun sendCustomCommand(command: SessionCommand, args: Bundle = Bundle.EMPTY): ListenableFuture { val result = mediaController.sendCustomCommand(command, args) result.addListener( { @@ -766,6 +777,6 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } companion object { - private const val TAG = PillarboxMediaSession.TAG + private const val TAG = "PillarboxMediaController" } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt index 8e52ad7b0..51e8f6aab 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt @@ -7,7 +7,6 @@ package ch.srgssr.pillarbox.player.session import android.app.PendingIntent import android.content.Context import android.os.Bundle -import android.util.Log import androidx.media3.common.MediaItem import androidx.media3.common.util.Util import androidx.media3.session.MediaLibraryService @@ -96,6 +95,11 @@ open class PillarboxMediaSession internal constructor() { return _mediaSession.player as PillarboxPlayer } + private val playerSessionState: PlayerSessionState + get() { + return PlayerSessionState(player) + } + fun setPlayer(player: PillarboxPlayer) { if (player != this.player) { this.player.removeListener(listener) @@ -104,9 +108,7 @@ open class PillarboxMediaSession internal constructor() { for (controllerInfo in _mediaSession.connectedControllers) { _mediaSession.setSessionExtras( controllerInfo, - Bundle(_mediaSession.sessionExtras).apply { - putBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG, player.smoothSeekingEnabled) - } + playerSessionState.toBundle(_mediaSession.sessionExtras) ) } } @@ -126,22 +128,23 @@ open class PillarboxMediaSession internal constructor() { } private inner class ComponentListener : PillarboxPlayer.Listener { - override fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) { + + private fun updateMediaSessionExtras() { for (controllerInfo in _mediaSession.connectedControllers) { - // _mediaSession.sendCustomCommand(controllerInfo, PillarboxSessionCommands.seekChangedCommand(smoothSeekingEnabled), Bundle.EMPTY) - Log.d(TAG, "onSmoothSeekingEnabledChanged $smoothSeekingEnabled") - _mediaSession.sessionExtras = Bundle(_mediaSession.sessionExtras).apply { - putBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG, player.smoothSeekingEnabled) - } - /* _mediaSession.setSessionExtras( controllerInfo, - Bundle(_mediaSession.sessionExtras).apply { - putBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG, smoothSeekingEnabled) - } - )*/ + playerSessionState.toBundle(_mediaSession.sessionExtras) + ) } } + + override fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) { + updateMediaSessionExtras() + } + + override fun onTrackingEnabledChanged(trackingEnabled: Boolean) { + updateMediaSessionExtras() + } } internal open class MediaSessionCallbackImpl( @@ -156,21 +159,18 @@ open class PillarboxMediaSession internal constructor() { } else { addSessionCommands(MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.commands) } + // TODO maybe add a way integrators can add custom commands add(PillarboxSessionCommands.COMMAND_SEEK_ENABLED) + add(PillarboxSessionCommands.COMMAND_TRACKER_ENABLED) }.build() - val pillarbox = session.player as PillarboxPlayer - Log.d(TAG, "onConnect") - val sessionExtras = Bundle(session.sessionExtras).apply { - putBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG, pillarbox.smoothSeekingEnabled) - } + val pillarboxPlayer = session.player as PillarboxPlayer + val playerSessionState = PlayerSessionState(pillarboxPlayer) + DebugLogger.debug(TAG, "onConnect with state = $playerSessionState") + val sessionExtras = playerSessionState.toBundle() return MediaSession.ConnectionResult.AcceptedResultBuilder(session) .setAvailableSessionCommands(availableSessionCommands) .setSessionExtras(sessionExtras) .build() - // return MediaSession.ConnectionResult.accept(availableSessionCommands, MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS) - } - - override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { } override fun onCustomCommand( @@ -179,6 +179,7 @@ open class PillarboxMediaSession internal constructor() { customCommand: SessionCommand, args: Bundle ): ListenableFuture { + // TODO maybe add a way integrators can add custom commands DebugLogger.debug(TAG, "onCustomCommand ${customCommand.customAction} ${customCommand.customExtras}") when (customCommand.customAction) { PillarboxSessionCommands.SMOOTH_SEEKING_ENABLED -> { @@ -188,20 +189,6 @@ open class PillarboxMediaSession internal constructor() { return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } } - - PillarboxSessionCommands.SMOOTH_SEEKING_GET -> { - if (session.player is PillarboxPlayer) { - val state = (session.player as PillarboxPlayer).smoothSeekingEnabled - return Futures.immediateFuture( - SessionResult( - SessionResult.RESULT_SUCCESS, - Bundle().apply { - putBoolean("smoothSeekingEnabled", state) - } - ) - ) - } - } } DebugLogger.warning(TAG, "Unsupported session command ${customCommand.customAction}") return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt index bc14a2035..eba61e32a 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt @@ -9,16 +9,24 @@ import androidx.media3.session.SessionCommand internal object PillarboxSessionCommands { const val SMOOTH_SEEKING_ARG = "pillarbox.smoothSeekingEnabled" + const val TRACKER_ENABLED_ARG = "pillarbox.trackerEnabled" + const val SMOOTH_SEEKING_ENABLED = "pillarbox.smooth.seeking.enabled" - const val SMOOTH_SEEKING_DISABLED = "pillarbox.smooth.seeking.disabled" - const val SMOOTH_SEEKING_CHANGED = "pillarbox.smooth.seeking.changed" - const val SMOOTH_SEEKING_GET = "pillarbox.smooth.seeking.get" + const val TRACKER_ENABLED = "pillarbox.tracker.enabled" + /** + * Place holder command + */ val COMMAND_SEEK_ENABLED = SessionCommand(SMOOTH_SEEKING_ENABLED, Bundle.EMPTY) + /** + * Place holder command + */ + val COMMAND_TRACKER_ENABLED = SessionCommand(TRACKER_ENABLED, Bundle.EMPTY) + fun setSmoothSeekingCommand(smoothSeekingEnabled: Boolean) = SessionCommand(SMOOTH_SEEKING_ENABLED, Bundle().apply { putBoolean(SMOOTH_SEEKING_ARG, smoothSeekingEnabled) }) - fun seekChangedCommand(smoothSeekingEnabled: Boolean) = - SessionCommand(SMOOTH_SEEKING_CHANGED, Bundle().apply { putBoolean(SMOOTH_SEEKING_ARG, smoothSeekingEnabled) }) + fun setTrackerEnabled(enabled: Boolean) = + SessionCommand(SMOOTH_SEEKING_ENABLED, Bundle().apply { putBoolean(TRACKER_ENABLED_ARG, enabled) }) } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PlayerSessionState.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PlayerSessionState.kt new file mode 100644 index 000000000..8ea703950 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PlayerSessionState.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.session + +import android.os.Bundle +import ch.srgssr.pillarbox.player.PillarboxPlayer + +/** + * The player state that is bundled as media session extras for each connected controller. + */ +internal data class PlayerSessionState(val smoothSeekingEnabled: Boolean, val trackingEnabled: Boolean) { + constructor(extras: Bundle) : this( + smoothSeekingEnabled = extras.getBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG), + trackingEnabled = extras.getBoolean(PillarboxSessionCommands.TRACKER_ENABLED_ARG), + ) + + constructor(player: PillarboxPlayer) : this( + smoothSeekingEnabled = player.smoothSeekingEnabled, + trackingEnabled = player.trackingEnabled, + ) + + fun toBundle(bundle: Bundle = Bundle.EMPTY): Bundle { + return Bundle(bundle).apply { + putBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG, smoothSeekingEnabled) + putBoolean(PillarboxSessionCommands.TRACKER_ENABLED_ARG, trackingEnabled) + } + } +} From 3844b6bb8d32e1a35d005a870b41824228f09bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Thu, 4 Apr 2024 11:23:41 +0200 Subject: [PATCH 10/27] Improve kdoc --- .../pillarbox/player/PillarboxPlayer.kt | 7 +- .../player/session/PillarboxMediaBrowser.kt | 135 +++++++++++++++--- .../session/PillarboxMediaController.kt | 74 +++++++++- .../session/PillarboxMediaLibrarySession.kt | 38 +---- .../player/session/PillarboxMediaSession.kt | 57 ++++++++ 5 files changed, 255 insertions(+), 56 deletions(-) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt index 145a2802d..25d1a5f7e 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt @@ -19,10 +19,15 @@ interface PillarboxPlayer : Player { /** * On smooth seeking enabled changed * - * @param smoothSeekingEnabled The new value of [smoothSeekingEnabled] + * @param smoothSeekingEnabled The new value of [PillarboxPlayer.smoothSeekingEnabled] */ fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) {} + /** + * On tracking enabled changed + * + * @param trackingEnabled The new value of [PillarboxPlayer.trackingEnabled] + */ fun onTrackingEnabledChanged(trackingEnabled: Boolean) {} } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt index eb9615e4f..dc035d2e3 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt @@ -13,6 +13,11 @@ import androidx.media3.session.MediaLibraryService.LibraryParams import androidx.media3.session.SessionToken import kotlinx.coroutines.guava.await +/** + * PillarboxMediaBrowser extends [PillarboxMediaController] but connect to a [PillarboxMediaLibrarySession] from a [MediaLibraryService]. + * @see MediaBrowser + * @see MediaLibraryService + */ class PillarboxMediaBrowser private constructor() : PillarboxMediaController() { private lateinit var mediaBrowser: MediaBrowser @@ -30,14 +35,31 @@ class PillarboxMediaBrowser private constructor() : PillarboxMediaController() { } } + /** + * Builder for [PillarboxMediaBrowser]. + * + * @param context The context. + * @param clazz The class of the [MediaLibraryService] that holds the [PillarboxMediaLibrarySession]. + */ class Builder(private val context: Context, private val clazz: Class) { private var listener: Listener = object : Listener {} + /** + * Set listener + * + * @param listener The listener + * @return this builder for convenience. + */ fun setListener(listener: Listener): Builder { this.listener = listener return this } + /** + * Create a new [PillarboxMediaBrowser] and connect to a [PillarboxMediaBrowser]. + * + * @return a [PillarboxMediaBrowser]. + */ suspend fun build(): PillarboxMediaBrowser { val pillarboxMediaController = PillarboxMediaBrowser() val listener = MediaBrowserListenerImpl(listener, pillarboxMediaController) @@ -52,15 +74,35 @@ class PillarboxMediaBrowser private constructor() : PillarboxMediaController() { } } + /** + * A listener for events and incoming commands from [PillarboxMediaLibrarySession]. + */ interface Listener : PillarboxMediaController.Listener { + /** + * Called when there's a change in the parent's children after you've subscribed to the parent with subscribe. + * + * @see MediaBrowser.Listener.onChildrenChanged + */ fun onChildrenChanged( browser: PillarboxMediaBrowser, parentId: String, itemCount: Int, params: LibraryParams? - ) {} + ) { + } - fun onSearchResultChanged(browser: PillarboxMediaBrowser, query: String, itemCount: Int, params: LibraryParams?) {} + /** + * Called when there's change in the search result requested by the previous search(String, MediaLibraryService.LibraryParams). + * + * @see MediaBrowser.Listener.onSearchResultChanged + */ + fun onSearchResultChanged( + browser: PillarboxMediaBrowser, + query: String, + itemCount: Int, + params: LibraryParams? + ) { + } } internal fun setMediaBrowser(mediaBrowser: MediaBrowser) { @@ -68,27 +110,86 @@ class PillarboxMediaBrowser private constructor() : PillarboxMediaController() { this.mediaBrowser = mediaBrowser } - fun getLibraryRoot(params: LibraryParams? = null) = mediaBrowser.getLibraryRoot(params) - - fun subscribe(parentId: String, params: LibraryParams? = null) = mediaBrowser.subscribe(parentId, params) - - fun unsubscribe(parentId: String) = mediaBrowser.unsubscribe(parentId) - - fun getChildren( + /** + * Get library root + * + * @param params The optional parameters for getting library root item. + * @see MediaBrowser.getLibraryRoot + */ + @JvmOverloads + suspend fun getLibraryRoot(params: LibraryParams? = null) = mediaBrowser.getLibraryRoot(params).await() + + /** + * Subscribes to a parent id for changes to its children. + * When there's a change, [PillarboxMediaBrowser.Listener.onChildrenChanged] will be called with the MediaLibraryService.LibraryParams. + * You may call [PillarboxMediaBrowser.getChildren] to get the children. + * + * @param parentId A non-empty parent id to subscribe to. + * @param params Optional parameters. + * @see MediaBrowser.subscribe + */ + @JvmOverloads + suspend fun subscribe( + parentId: String, + params: LibraryParams? = null + ) = mediaBrowser.subscribe(parentId, params).await() + + /** + * Unsubscribes from a parent id for changes to its children, which was previously subscribed by subscribe. + * + * @param parentId A non-empty parent id to unsubscribe from. + * @see MediaBrowser.unsubscribe + */ + suspend fun unsubscribe(parentId: String) = mediaBrowser.unsubscribe(parentId).await() + + /** + * Get children for the parentId + * + * @param parentId A non-empty parent id for getting the children. + * @param page A page number to get the paginated result starting from 0. + * @param pageSize A page size to get the paginated result. + * @param params Optional parameters. + * @see MediaBrowser.getChildren + */ + @JvmOverloads + suspend fun getChildren( parentId: String, @IntRange(from = 0) page: Int, @IntRange(from = 1) pageSize: Int, params: LibraryParams? = null - ) = mediaBrowser.getChildren(parentId, page, pageSize, params) - - fun getItem(mediaId: String) = mediaBrowser.getItem(mediaId) - - fun search(query: String, params: LibraryParams? = null) = mediaBrowser.search(query, params) - - fun getSearchResult( + ) = mediaBrowser.getChildren(parentId, page, pageSize, params).await() + + /** + * Get item + * + * @param mediaId A non-empty media id. + * @see MediaBrowser.getItem + */ + suspend fun getItem(mediaId: String) = mediaBrowser.getItem(mediaId).await() + + /** + * Requests a search from the library service. + * + * @param query A non-empty search query. + * @param params Optional parameters. + * @see MediaBrowser.search + */ + @JvmOverloads + suspend fun search(query: String, params: LibraryParams? = null) = mediaBrowser.search(query, params).await() + + /** + * Returns the search result from the library service. + + * @param query A non-empty search query that you've specified with [search]. + * @param page A page number to get the paginated result starting from 0 + * @param pageSize A page size to get the paginated result. + * @param params Optional parameters. + */ + @JvmOverloads + suspend fun getSearchResult( query: String, @IntRange(from = 0) page: Int, @IntRange(from = 1) pageSize: Int, params: LibraryParams? = null - ) = mediaBrowser.getSearchResult(query, page, pageSize, params) + ) = mediaBrowser.getSearchResult(query, page, pageSize, params).await() } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt index 3b4af5319..b731ec08e 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt @@ -46,21 +46,37 @@ import com.google.common.util.concurrent.MoreExecutors import kotlinx.coroutines.guava.await /** - * Pillarbox media controller - * - * @constructor Create empty Pillarbox media controller + * Pillarbox media controller implements [PillarboxPlayer] and wrap a [MediaController]. + * @see MediaController */ open class PillarboxMediaController internal constructor() : PillarboxPlayer { + /** + * Builder for [PillarboxMediaController]. + * + * @param context The context. + * @param clazz The class of the [MediaSessionService] that holds the [PillarboxMediaSession]. + */ class Builder(private val context: Context, private val clazz: Class) { private var listener: Listener = object : Listener {} + /** + * Set listener + * + * @param listener The [Listener]. + * @return [Builder] for convenience. + */ fun setListener(listener: Listener): Builder { this.listener = listener return this } + /** + * Create a new [PillarboxMediaController] and connect to a [PillarboxMediaSession]. + * + * @return a [PillarboxMediaController]. + */ suspend fun build(): PillarboxMediaController { val pillarboxMediaController = PillarboxMediaController() val listener = MediaControllerListenerImpl(listener, pillarboxMediaController) @@ -76,7 +92,16 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } } + /** + * A listener for events and incoming commands from [PillarboxMediaSession]. + */ interface Listener { + + /** + * Called when the session sends a custom command. + * + * @see MediaController.Listener.onCustomCommand + */ fun onCustomCommand( controller: PillarboxMediaController, command: SessionCommand, @@ -85,13 +110,30 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) } + /** + * Called when the available session commands are changed by session. + * @see MediaController.Listener.onAvailableSessionCommandsChanged + */ fun onAvailableSessionCommandsChanged(controller: PillarboxMediaController, commands: SessionCommands) {} + /** + * Called when the controller is disconnected from the session. + * The controller becomes unavailable afterwards and this listener won't be called anymore. + * + * @see MediaController.Listener.onDisconnected + */ fun onDisconnected(controller: PillarboxMediaController) {} + /** + * Called when the session extras are set on the session side. + * @see MediaController.Listener.onExtrasChanged + */ fun onExtrasChanged(controller: PillarboxMediaController, extras: Bundle) {} } + /** + * Forward [MediaController.Listener] to the listener and apply it to the mediaController. + */ internal open class MediaControllerListenerImpl( val listener: Listener, val mediaController: PillarboxMediaController @@ -123,23 +165,48 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { private lateinit var playerSessionState: PlayerSessionState private val listeners = HashSet() + + /** + * The SessionToken of the connected session, or null if it is not connected. + * @see MediaController.getConnectedToken + */ val connectedToken: SessionToken? get() = mediaController.connectedToken + /** + * Is connected + * @see MediaController.isConnected + */ val isConnected: Boolean get() = mediaController.isConnected + /** + * Session activity + * @see MediaController.getSessionActivity + */ val sessionActivity: PendingIntent? get() = mediaController.sessionActivity + /** + * Custom layout + * @see MediaController.getCustomLayout + */ @get:UnstableApi val customLayout: ImmutableList get() = mediaController.getCustomLayout() + /** + * Session extras + * @see MediaController.getSessionActivity + */ @get:UnstableApi val sessionExtras: Bundle get() = mediaController.getSessionExtras() + /** + * Available session commands + * @see MediaController.getAvailableSessionCommands + */ val availableSessionCommands: SessionCommands get() = mediaController.getAvailableSessionCommands() @@ -194,6 +261,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { /** * @See [MediaController.sendCustomCommand] */ + @JvmOverloads fun sendCustomCommand(command: SessionCommand, args: Bundle = Bundle.EMPTY): ListenableFuture { val result = mediaController.sendCustomCommand(command, args) result.addListener( diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt index bed4a0a97..a1255c6cb 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt @@ -41,11 +41,6 @@ open class PillarboxMediaLibrarySession internal constructor() : /** * Called when a [PillarboxMediaBrowser] requests the root [MediaItem]. * @see MediaLibrarySession.Callback.onGetLibraryRoot - * - * @param session The session for this event. - * @param browser The browser information. - * @param params The optional parameters passed by the browser. - * @return A pending result that will be resolved with a root media item. */ fun onGetLibraryRoot( session: PillarboxMediaLibrarySession, @@ -58,14 +53,6 @@ open class PillarboxMediaLibrarySession internal constructor() : /** * Called when a [PillarboxMediaBrowser] requests the child media items of the given parent id. * @see MediaLibrarySession.Callback.onGetChildren - * - * @param session The session for this event. - * @param browser The browser information. - * @param parentId The non-empty parent id. - * @param page The page number to get the paginated result starting from 0. - * @param pageSize The page size to get the paginated result. Will be greater than 0. - * @param params The optional parameters passed by the browser. - * @return A pending result that will be resolved with a list of media items. */ fun onGetChildren( session: PillarboxMediaLibrarySession, @@ -81,11 +68,6 @@ open class PillarboxMediaLibrarySession internal constructor() : /** * Called when a [PillarboxMediaBrowser] requests a [MediaItem] from mediaId. * @see MediaLibrarySession.Callback.onGetItem - * - * @param session The session for this event. - * @param browser The browser information. - * @param mediaId The non-empty media id of the requested item. - * @return A pending result that will be resolved with a media item. */ fun onGetItem( session: PillarboxMediaLibrarySession, @@ -98,12 +80,6 @@ open class PillarboxMediaLibrarySession internal constructor() : /** * Called when a [androidx.media3.session.MediaBrowser] requests a search. * @see MediaLibrarySession.Callback.onSearch - * - * @param session The session for this event. - * @param browser The browser information. - * @param query The non-empty search query. - * @param params The optional parameters passed by the browser. - * @return A pending result that will be resolved with a result code. */ fun onSearch( session: PillarboxMediaLibrarySession, @@ -117,14 +93,6 @@ open class PillarboxMediaLibrarySession internal constructor() : /** * Called when a [PillarboxMediaBrowser] requests the child media items of the given parent id. * @see MediaLibrarySession.Callback.onGetSearchResult - * - * @param session The session for this event. - * @param browser The browser information. - * @param query The non-empty search query. - * @param page The page number to get the paginated result starting from 0. - * @param pageSize The page size to get the paginated result. Will be greater than 0. - * @param params The optional parameters passed by the browser. - * @return A pending result that will be resolved with a list of media items. */ fun onGetSearchResult( session: PillarboxMediaLibrarySession, @@ -144,9 +112,9 @@ open class PillarboxMediaLibrarySession internal constructor() : * Any incoming requests from the [PillarboxMediaBrowser] will be handled on the application * thread of the underlying [PillarboxPlayer]. * - * @property service The [MediaLibraryService] that instantiates the [PillarboxMediaLibrarySession]. - * @property player The underlying player to perform playback and handle transport controls. - * @property callback The [Callback] to handle requests from [PillarboxMediaBrowser]. + * @param service The [MediaLibraryService] that instantiates the [PillarboxMediaLibrarySession]. + * @param player The underlying player to perform playback and handle transport controls. + * @param callback The [Callback] to handle requests from [PillarboxMediaBrowser]. */ class Builder( private val service: MediaLibraryService, diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt index 51e8f6aab..d353b2820 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt @@ -25,8 +25,14 @@ import com.google.common.util.concurrent.ListenableFuture */ open class PillarboxMediaSession internal constructor() { + /** + * Callback + */ interface Callback { + /** + * @see MediaSession.Callback.onSetMediaItems + */ fun onSetMediaItems( mediaSession: PillarboxMediaSession, controller: MediaSession.ControllerInfo, @@ -39,6 +45,9 @@ open class PillarboxMediaSession internal constructor() { ) { input -> Futures.immediateFuture(MediaItemsWithStartPosition(input!!, startIndex, startPositionMs)) } } + /** + * @see MediaSession.Callback.onAddMediaItems + */ fun onAddMediaItems( mediaSession: PillarboxMediaSession, controller: MediaSession.ControllerInfo, @@ -52,27 +61,63 @@ open class PillarboxMediaSession internal constructor() { return Futures.immediateFuture(mediaItems) } + /** + * Default implementation + */ object Default : Callback } + /** + * Builder + * + * @param context + * @param player + */ class Builder(context: Context, player: PillarboxPlayer) { private val mediaSessionBuilder = MediaSession.Builder(context, player) private var callback: Callback = object : Callback {} + /** + * Sets a PendingIntent to launch an android.app.Activity for the MediaSession. + * This can be used as a quick link to an ongoing media screen. + * + * @param pendingIntent The [PendingIntent]. + * @return this builder for convenience. + * @see MediaSession.Builder.setSessionActivity + */ fun setSessionActivity(pendingIntent: PendingIntent): Builder { mediaSessionBuilder.setSessionActivity(pendingIntent) return this } + /** + * Sets an ID of the [PillarboxMediaSession]. If not set, an empty string will be used. + * Use this if and only if your app supports multiple playback at the same time and also wants to provide external apps to have + * finer-grained controls. + * + * @param id The ID. Must be unique among all sessions per package. + * @return this builder for convenience. + * @see MediaSession.Builder.setId + */ fun setId(id: String): Builder { mediaSessionBuilder.setId(id) return this } + /** + * Set callback + * + * @param callback + */ fun setCallback(callback: Callback) { this.callback = callback } + /** + * Build + * + * @return create a [PillarboxMediaSession]. + */ fun build(): PillarboxMediaSession { val pillarboxMediaSession = PillarboxMediaSession() val media3SessionCallback = MediaSessionCallbackImpl(callback, pillarboxMediaSession) @@ -86,10 +131,18 @@ open class PillarboxMediaSession internal constructor() { private lateinit var _mediaSession: MediaSession private val listener = ComponentListener() + + /** + * The underlying [androidx.media3.session.MediaSession]. + */ open val mediaSession: MediaSession get() { return _mediaSession } + + /** + * Player + */ val player: PillarboxPlayer get() { return _mediaSession.player as PillarboxPlayer @@ -100,6 +153,10 @@ open class PillarboxMediaSession internal constructor() { return PlayerSessionState(player) } + /** + * Sets the underlying Player for this session to dispatch incoming events to. + * @see MediaSession.setPlayer + */ fun setPlayer(player: PillarboxPlayer) { if (player != this.player) { this.player.removeListener(listener) From 6daad85a3e6b38c40d2388629ac73769d2028377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Thu, 4 Apr 2024 13:57:24 +0200 Subject: [PATCH 11/27] Use PillarboxMediaSession in TV demo --- .../ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt index bd88964e6..92dad2598 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt @@ -12,12 +12,12 @@ 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.exoplayer.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.session.PillarboxMediaSession /** * Player activity @@ -26,12 +26,12 @@ import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer */ class PlayerActivity : ComponentActivity() { private lateinit var player: PillarboxExoPlayer - private lateinit var mediaSession: MediaSession + 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) From bc773fe5300e4a58976fed60761cb3bebae101eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Thu, 4 Apr 2024 14:10:41 +0200 Subject: [PATCH 12/27] Update service documentation --- .../player/session/PillarboxMediaLibraryService.kt | 10 +++------- .../player/session/PillarboxMediaSessionService.kt | 10 ++++++---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt index ac4080083..9b54e9173 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt @@ -28,7 +28,7 @@ import ch.srgssr.pillarbox.player.utils.PendingIntentUtils * * ``` * - * And add your `PlaybackService` to the application manifest as follow: + * And add your `PillarboxMediaLibraryService` to the application manifest as follow: * * ```xml * @@ -45,14 +45,10 @@ import ch.srgssr.pillarbox.player.utils.PendingIntentUtils * * ``` * - * Use [MediaBrowser.Builder][androidx.media3.session.MediaBrowser.Builder] to connect this Service to a `MediaBrowser`: + * Use [PillarboxMediaBrowser.Builder] to connect this Service to a `PillarboxMediaBrowser`: * ```kotlin - * val sessionToken = SessionToken(context, ComponentName(application, DemoMediaLibraryService::class.java)) - * val listenableFuture = MediaBrowser.Builder(context, sessionToken) - * .setListener(MediaBrowser.Listener()...) // Optional - * .buildAsync() * coroutineScope.launch(){ - * val mediaBrowser = listenableFuture.await() // suspend method to retrieve MediaBrowser + * val mediaBrowser = PillarboxMediaBrowser.Builder(application,DemoMediaLibraryService::class.java) * doSomethingWith(mediaBrowser) * } * ... diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt index e392b5bc1..30a584ba2 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt @@ -40,12 +40,14 @@ import ch.srgssr.pillarbox.player.utils.PendingIntentUtils * * ``` * - * Use [MediaControllerConnection] to connect this Service to a `MediaController`. + * Use [PillarboxMediaController.Builder] to connect this Service to a `PillarboxMediaController`: * ```kotlin - * val connection = MediaControllerConnection(context, ComponentName(application, DemoMediaSessionService::class.java)) - * connection.mediaController.collectLatest { useController(it) } + * coroutineScope.launch(){ + * val mediaController: PillarboxPlayer = PillarboxMediaController.Builder(application,DemoMediaLibraryService::class.java) + * doSomethingWith(mediaController) + * } * ... - * connection.release() // when controller no more needed. + * mediaController.release() // when mediaController no more needed. * ``` */ @Suppress("MemberVisibilityCanBePrivate") From 168efd5b771df50a4ac3726153788c1ab073d26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Thu, 4 Apr 2024 15:37:47 +0200 Subject: [PATCH 13/27] Remove useless interface PillarboxExoPlayer --- .../core/business/DefaultPillarbox.kt | 2 +- .../pillarbox/demo/shared/di/PlayerModule.kt | 2 +- .../demo/tv/ui/player/PlayerActivity.kt | 2 +- .../player/leanback/LeanbackPlayerFragment.kt | 2 +- .../demo/service/DemoMediaSessionService.kt | 1 + .../demo/ui/player/SimplePlayerViewModel.kt | 1 + .../demo/ui/showcases/layouts/SimpleStory.kt | 2 +- .../ui/showcases/layouts/StoryViewModel.kt | 2 +- .../player/IsPlayingAllTypeOfContentTest.kt | 1 - .../pillarbox/player/PillarboxExoPlayer.kt | 392 ++++++++++++++++- .../player/exoplayer/PillarboxExoPlayer.kt | 405 ------------------ .../pillarbox/player/extension/Player.kt | 8 + .../player/service/PlaybackService.kt | 3 +- .../session/PillarboxMediaLibraryService.kt | 3 +- .../session/PillarboxMediaSessionService.kt | 3 +- .../player/PillarboxExoPlayerMediaItemTest.kt | 1 - .../TestIsPlaybackSpeedPossibleAtPosition.kt | 1 - .../TestPillarboxExoPlayerPlaybackSpeed.kt | 1 - .../player/tracker/MediaItemTrackerTest.kt | 2 +- 19 files changed, 409 insertions(+), 425 deletions(-) delete mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/exoplayer/PillarboxExoPlayer.kt diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/DefaultPillarbox.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/DefaultPillarbox.kt index 2b4cc1861..a66a0d5fc 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/DefaultPillarbox.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/DefaultPillarbox.kt @@ -73,7 +73,7 @@ object DefaultPillarbox { mediaCompositionService: MediaCompositionService = HttpMediaCompositionService(), clock: Clock, ): PillarboxExoPlayer { - return ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer( + return PillarboxExoPlayer( context = context, seekIncrement = seekIncrement, mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt index 5257535f0..e34e2c45f 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt @@ -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.exoplayer.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.PillarboxExoPlayer import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import java.net.URL diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt index 92dad2598..a6085b278 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/PlayerActivity.kt @@ -16,7 +16,7 @@ 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.exoplayer.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.PillarboxExoPlayer import ch.srgssr.pillarbox.player.session.PillarboxMediaSession /** diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt index b9b1f4cba..33386a8b9 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt @@ -19,8 +19,8 @@ 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.PillarboxExoPlayer import ch.srgssr.pillarbox.player.currentMediaMetadataAsFlow -import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt index 5849ff5b1..160d1d45d 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt @@ -10,6 +10,7 @@ import android.util.Log import androidx.media3.common.C import ch.srgssr.pillarbox.demo.shared.di.PlayerModule import ch.srgssr.pillarbox.demo.ui.showcases.integrations.MediaControllerActivity +import ch.srgssr.pillarbox.player.extension.setHandleAudioFocus import ch.srgssr.pillarbox.player.session.PillarboxMediaSessionService import ch.srgssr.pillarbox.player.utils.PendingIntentUtils diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt index fb9f6b36e..70b55a4c8 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt @@ -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 diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/SimpleStory.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/SimpleStory.kt index 630862c7d..ec2c47c0b 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/SimpleStory.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/SimpleStory.kt @@ -24,7 +24,7 @@ import androidx.media3.common.Player import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.shared.data.Playlist import ch.srgssr.pillarbox.demo.shared.di.PlayerModule -import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.PillarboxExoPlayer import ch.srgssr.pillarbox.ui.ScaleMode import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt index 240414de3..d4937b31d 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt @@ -10,7 +10,7 @@ import androidx.media3.common.C import androidx.media3.common.Player import ch.srgssr.pillarbox.demo.shared.data.Playlist import ch.srgssr.pillarbox.demo.shared.di.PlayerModule -import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.PillarboxExoPlayer import kotlin.math.ceil /** diff --git a/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt b/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt index af0f7967e..ba2fcef21 100644 --- a/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt +++ b/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt @@ -10,7 +10,6 @@ import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.util.ConditionVariable import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.player.utils.ContentUrls import org.junit.Assert import org.junit.Test diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt index 423a518c2..ad638d67d 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt @@ -4,17 +4,397 @@ */ package ch.srgssr.pillarbox.player +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.PlaybackParameters +import androidx.media3.common.Player +import androidx.media3.common.Timeline.Window +import androidx.media3.common.TrackSelectionParameters +import androidx.media3.common.util.Clock +import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.LoadControl +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector +import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter +import androidx.media3.exoplayer.util.EventLogger +import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed +import ch.srgssr.pillarbox.player.extension.setPreferredAudioRoleFlagsToAccessibilityManagerSettings +import ch.srgssr.pillarbox.player.extension.setSeekIncrements +import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory +import ch.srgssr.pillarbox.player.tracker.CurrentMediaItemTracker +import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider +import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository /** - * Pillarbox [ExoPlayer] interface extension. + * Pillarbox player + * + * @param exoPlayer + * @param mediaItemTrackerProvider + * + * @constructor */ -interface PillarboxExoPlayer : PillarboxPlayer, ExoPlayer { +class PillarboxExoPlayer internal constructor( + private val exoPlayer: ExoPlayer, + mediaItemTrackerProvider: MediaItemTrackerProvider? +) : PillarboxPlayer, ExoPlayer by exoPlayer { + private val listeners = HashSet() + private val itemTracker: CurrentMediaItemTracker? + private val window = Window() + override var smoothSeekingEnabled: Boolean = false + set(value) { + if (value != field) { + field = value + if (!value) { + seekEnd() + } + clearSeeking() + val listeners = HashSet(listeners) + for (listener in listeners) { + listener.onSmoothSeekingEnabledChanged(value) + } + } + } + private var pendingSeek: Long? = null + private var isSeeking: Boolean = false + + /** + * Enable or disable MediaItem tracking + */ + + override var trackingEnabled: Boolean + set(value) = itemTracker?.let { + if (it.enabled != value) { + it.enabled = value + val listeners = HashSet(listeners) + for (listener in listeners) { + listener.onTrackingEnabledChanged(value) + } + } + } ?: Unit + get() = itemTracker?.enabled ?: false + + init { + exoPlayer.addListener(ComponentListener()) + itemTracker = mediaItemTrackerProvider?.let { + CurrentMediaItemTracker(this, it) + } + if (BuildConfig.DEBUG) { + addAnalyticsListener(EventLogger()) + } + } + + constructor( + context: Context, + mediaSourceFactory: PillarboxMediaSourceFactory = PillarboxMediaSourceFactory(context), + loadControl: LoadControl = PillarboxLoadControl(), + mediaItemTrackerProvider: MediaItemTrackerProvider = MediaItemTrackerRepository(), + seekIncrement: SeekIncrement = SeekIncrement() + ) : this( + context = context, + mediaSourceFactory = mediaSourceFactory, + loadControl = loadControl, + mediaItemTrackerProvider = mediaItemTrackerProvider, + seekIncrement = seekIncrement, + clock = Clock.DEFAULT, + ) + + @VisibleForTesting + constructor( + context: Context, + mediaSourceFactory: PillarboxMediaSourceFactory = PillarboxMediaSourceFactory(context), + loadControl: LoadControl = PillarboxLoadControl(), + mediaItemTrackerProvider: MediaItemTrackerProvider = MediaItemTrackerRepository(), + seekIncrement: SeekIncrement = SeekIncrement(), + clock: Clock, + ) : this( + ExoPlayer.Builder(context) + .setClock(clock) + .setUsePlatformDiagnostics(false) + .setSeekIncrements(seekIncrement) + .setRenderersFactory( + DefaultRenderersFactory(context) + .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF) + .setEnableDecoderFallback(true) + ) + .setBandwidthMeter(DefaultBandwidthMeter.getSingletonInstance(context)) + .setLoadControl(loadControl) + .setMediaSourceFactory(mediaSourceFactory) + .setTrackSelector( + DefaultTrackSelector( + context, + TrackSelectionParameters.Builder(context) + .setPreferredAudioRoleFlagsToAccessibilityManagerSettings(context) + .build() + ) + ) + .setDeviceVolumeControlEnabled(true) // allow player to control device volume + .build(), + mediaItemTrackerProvider = mediaItemTrackerProvider + ) + + override fun addListener(listener: Player.Listener) { + exoPlayer.addListener(listener) + if (listener is PillarboxPlayer.Listener) { + listeners.add(listener) + } + } + + override fun removeListener(listener: Player.Listener) { + exoPlayer.removeListener(listener) + if (listener is PillarboxPlayer.Listener) { + listeners.remove(listener) + } + } + + override fun setMediaItem(mediaItem: MediaItem) { + exoPlayer.setMediaItem(mediaItem.clearTag()) + } + + override fun setMediaItem(mediaItem: MediaItem, resetPosition: Boolean) { + exoPlayer.setMediaItem(mediaItem.clearTag(), resetPosition) + } + + override fun setMediaItem(mediaItem: MediaItem, startPositionMs: Long) { + exoPlayer.setMediaItem(mediaItem.clearTag(), startPositionMs) + } + + override fun setMediaItems(mediaItems: List) { + exoPlayer.setMediaItems(mediaItems.map { it.clearTag() }) + } + + override fun setMediaItems(mediaItems: List, resetPosition: Boolean) { + exoPlayer.setMediaItems(mediaItems.map { it.clearTag() }, resetPosition) + } + + override fun setMediaItems(mediaItems: List, startIndex: Int, startPositionMs: Long) { + exoPlayer.setMediaItems(mediaItems.map { it.clearTag() }, startIndex, startPositionMs) + } + + override fun addMediaItem(mediaItem: MediaItem) { + exoPlayer.addMediaItem(mediaItem.clearTag()) + } + + override fun addMediaItem(index: Int, mediaItem: MediaItem) { + exoPlayer.addMediaItem(index, mediaItem.clearTag()) + } + + override fun addMediaItems(mediaItems: List) { + exoPlayer.addMediaItems(mediaItems.map { it.clearTag() }) + } + + override fun addMediaItems(index: Int, mediaItems: List) { + exoPlayer.addMediaItems(index, mediaItems.map { it.clearTag() }) + } + + override fun replaceMediaItem(index: Int, mediaItem: MediaItem) { + exoPlayer.replaceMediaItem(index, mediaItem.clearTag()) + } + + override fun replaceMediaItems(fromIndex: Int, toIndex: Int, mediaItems: List) { + exoPlayer.replaceMediaItems(fromIndex, toIndex, mediaItems.map { it.clearTag() }) + } + + override fun seekTo(positionMs: Long) { + if (!smoothSeekingEnabled) { + exoPlayer.seekTo(positionMs) + return + } + smoothSeekTo(positionMs) + } + + private fun smoothSeekTo(positionMs: Long) { + if (isSeeking) { + pendingSeek = positionMs + return + } + isSeeking = true + exoPlayer.seekTo(positionMs) + } + + override fun seekTo(mediaItemIndex: Int, positionMs: Long) { + if (!smoothSeekingEnabled) { + exoPlayer.seekTo(mediaItemIndex, positionMs) + return + } + smoothSeekTo(mediaItemIndex, positionMs) + } + + private fun smoothSeekTo(mediaItemIndex: Int, positionMs: Long) { + if (mediaItemIndex != currentMediaItemIndex) { + clearSeeking() + exoPlayer.seekTo(mediaItemIndex, positionMs) + return + } + if (isSeeking) { + pendingSeek = positionMs + return + } + exoPlayer.seekTo(mediaItemIndex, positionMs) + } + + override fun seekToDefaultPosition() { + clearSeeking() + exoPlayer.seekToDefaultPosition() + } + + override fun seekToDefaultPosition(mediaItemIndex: Int) { + clearSeeking() + exoPlayer.seekToDefaultPosition(mediaItemIndex) + } + + override fun seekBack() { + clearSeeking() + exoPlayer.seekBack() + } + + override fun seekForward() { + clearSeeking() + exoPlayer.seekForward() + } + + override fun seekToNext() { + clearSeeking() + exoPlayer.seekToNext() + } + + override fun seekToPrevious() { + clearSeeking() + exoPlayer.seekToPrevious() + } + + override fun seekToNextMediaItem() { + clearSeeking() + exoPlayer.seekToNextMediaItem() + } + + override fun seekToPreviousMediaItem() { + clearSeeking() + exoPlayer.seekToPreviousMediaItem() + } + /** - * Handle audio focus with currently set AudioAttributes - * @param handleAudioFocus true if the player should handle audio focus, false otherwise. + * Releases the player. + * This method must be called when the player is no longer required. The player must not be used after calling this method. + * + * Release call automatically [stop] if the player is not in [Player.STATE_IDLE]. */ - fun setHandleAudioFocus(handleAudioFocus: Boolean) { - setAudioAttributes(audioAttributes, handleAudioFocus) + override fun release() { + clearSeeking() + if (playbackState != Player.STATE_IDLE) { + stop() + } + exoPlayer.release() + } + + override fun setPlaybackParameters(playbackParameters: PlaybackParameters) { + if (isPlaybackSpeedPossibleAtPosition(currentPosition, playbackParameters.speed, window)) { + exoPlayer.playbackParameters = playbackParameters + } else { + exoPlayer.playbackParameters = playbackParameters.withSpeed(NormalSpeed) + } } + + override fun setPlaybackSpeed(speed: Float) { + playbackParameters = playbackParameters.withSpeed(speed) + } + + private fun seekEnd() { + isSeeking = false + pendingSeek?.let { pendingPosition -> + pendingSeek = null + seekTo(pendingPosition) + } + } + + private fun clearSeeking() { + isSeeking = false + pendingSeek = null + } + + private inner class ComponentListener : Player.Listener { + private val window = Window() + + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + clearSeeking() + } + + override fun onRenderedFirstFrame() { + seekEnd() + } + + override fun onPlaybackStateChanged(playbackState: Int) { + when (playbackState) { + Player.STATE_READY -> { + if (isSeeking) { + seekEnd() + } + } + + Player.STATE_IDLE, Player.STATE_ENDED -> { + clearSeeking() + } + + Player.STATE_BUFFERING -> { + // Do nothing + } + } + } + + override fun onPlayerError(error: PlaybackException) { + clearSeeking() + if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) { + setPlaybackSpeed(NormalSpeed) + seekToDefaultPosition() + prepare() + } + } + + override fun onEvents(player: Player, events: Player.Events) { + if (!player.isCurrentMediaItemLive || player.getPlaybackSpeed() == NormalSpeed) return + if (!player.isCurrentMediaItemSeekable) { + setPlaybackSpeed(NormalSpeed) + return + } + player.currentTimeline.getWindow(currentMediaItemIndex, window) + if (window.isAtDefaultPosition(currentPosition) && getPlaybackSpeed() > NormalSpeed) { + exoPlayer.setPlaybackSpeed(NormalSpeed) + } + } + } +} + +/** + * Return if the playback [speed] is possible at [position]. + * Always return true for none live content or if [Player.getCurrentTimeline] is empty. + * + * @param position The position to test the playback speed. + * @param speed The playback speed + * @param window optional window for performance purpose + * @return true if the playback [speed] can be set at [position] + */ +fun Player.isPlaybackSpeedPossibleAtPosition(position: Long, speed: Float, window: Window = Window()): Boolean { + if (currentTimeline.isEmpty || speed == NormalSpeed || !isCurrentMediaItemLive) { + return true + } + currentTimeline.getWindow(currentMediaItemIndex, window) + return window.isPlaybackSpeedPossibleAtPosition(position, speed) } + +internal fun Window.isPlaybackSpeedPossibleAtPosition(positionMs: Long, playbackSpeed: Float): Boolean { + return when { + !isLive() || playbackSpeed == NormalSpeed -> true + !isSeekable -> false + isAtDefaultPosition(positionMs) && playbackSpeed > NormalSpeed -> false + else -> true + } +} + +internal fun Window.isAtDefaultPosition(positionMs: Long): Boolean { + return positionMs >= defaultPositionMs +} + +private const val NormalSpeed = 1.0f + +private fun MediaItem.clearTag() = this.buildUpon().setTag(null).build() diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/exoplayer/PillarboxExoPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/exoplayer/PillarboxExoPlayer.kt deleted file mode 100644 index 5d0f92f9b..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/exoplayer/PillarboxExoPlayer.kt +++ /dev/null @@ -1,405 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.exoplayer - -import android.content.Context -import androidx.annotation.VisibleForTesting -import androidx.media3.common.MediaItem -import androidx.media3.common.PlaybackException -import androidx.media3.common.PlaybackParameters -import androidx.media3.common.Player -import androidx.media3.common.Timeline.Window -import androidx.media3.common.TrackSelectionParameters -import androidx.media3.common.util.Clock -import androidx.media3.exoplayer.DefaultRenderersFactory -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.LoadControl -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector -import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter -import androidx.media3.exoplayer.util.EventLogger -import ch.srgssr.pillarbox.player.BuildConfig -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.extension.getPlaybackSpeed -import ch.srgssr.pillarbox.player.extension.setPreferredAudioRoleFlagsToAccessibilityManagerSettings -import ch.srgssr.pillarbox.player.extension.setSeekIncrements -import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory -import ch.srgssr.pillarbox.player.tracker.CurrentMediaItemTracker -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository - -/** - * Pillarbox player - * - * @param exoPlayer - * @param mediaItemTrackerProvider - * - * @constructor - */ -class PillarboxExoPlayer internal constructor( - private val exoPlayer: ExoPlayer, - mediaItemTrackerProvider: MediaItemTrackerProvider? -) : PillarboxExoPlayer, ExoPlayer by exoPlayer { - private val listeners = HashSet() - private val itemTracker: CurrentMediaItemTracker? - private val window = Window() - override var smoothSeekingEnabled: Boolean = false - set(value) { - if (value != field) { - field = value - if (!value) { - seekEnd() - } - clearSeeking() - val listeners = HashSet(listeners) - for (listener in listeners) { - listener.onSmoothSeekingEnabledChanged(value) - } - } - } - private var pendingSeek: Long? = null - private var isSeeking: Boolean = false - - /** - * Enable or disable MediaItem tracking - */ - - override var trackingEnabled: Boolean - set(value) = itemTracker?.let { - if (it.enabled != value) { - it.enabled = value - val listeners = HashSet(listeners) - for (listener in listeners) { - listener.onTrackingEnabledChanged(value) - } - } - } ?: Unit - get() = itemTracker?.enabled ?: false - - init { - exoPlayer.addListener(ComponentListener()) - itemTracker = mediaItemTrackerProvider?.let { - CurrentMediaItemTracker(this, it) - } - if (BuildConfig.DEBUG) { - addAnalyticsListener(EventLogger()) - } - } - - constructor( - context: Context, - mediaSourceFactory: PillarboxMediaSourceFactory = PillarboxMediaSourceFactory(context), - loadControl: LoadControl = PillarboxLoadControl(), - mediaItemTrackerProvider: MediaItemTrackerProvider = MediaItemTrackerRepository(), - seekIncrement: SeekIncrement = SeekIncrement() - ) : this( - context = context, - mediaSourceFactory = mediaSourceFactory, - loadControl = loadControl, - mediaItemTrackerProvider = mediaItemTrackerProvider, - seekIncrement = seekIncrement, - clock = Clock.DEFAULT, - ) - - @VisibleForTesting - constructor( - context: Context, - mediaSourceFactory: PillarboxMediaSourceFactory = PillarboxMediaSourceFactory(context), - loadControl: LoadControl = PillarboxLoadControl(), - mediaItemTrackerProvider: MediaItemTrackerProvider = MediaItemTrackerRepository(), - seekIncrement: SeekIncrement = SeekIncrement(), - clock: Clock, - ) : this( - ExoPlayer.Builder(context) - .setClock(clock) - .setUsePlatformDiagnostics(false) - .setSeekIncrements(seekIncrement) - .setRenderersFactory( - DefaultRenderersFactory(context) - .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF) - .setEnableDecoderFallback(true) - ) - .setBandwidthMeter(DefaultBandwidthMeter.getSingletonInstance(context)) - .setLoadControl(loadControl) - .setMediaSourceFactory(mediaSourceFactory) - .setTrackSelector( - DefaultTrackSelector( - context, - TrackSelectionParameters.Builder(context) - .setPreferredAudioRoleFlagsToAccessibilityManagerSettings(context) - .build() - ) - ) - .setDeviceVolumeControlEnabled(true) // allow player to control device volume - .build(), - mediaItemTrackerProvider = mediaItemTrackerProvider - ) - - override fun addListener(listener: Player.Listener) { - exoPlayer.addListener(listener) - if (listener is PillarboxPlayer.Listener) { - listeners.add(listener) - } - } - - override fun removeListener(listener: Player.Listener) { - exoPlayer.removeListener(listener) - if (listener is PillarboxPlayer.Listener) { - listeners.remove(listener) - } - } - - override fun setMediaItem(mediaItem: MediaItem) { - exoPlayer.setMediaItem(mediaItem.clearTag()) - } - - override fun setMediaItem(mediaItem: MediaItem, resetPosition: Boolean) { - exoPlayer.setMediaItem(mediaItem.clearTag(), resetPosition) - } - - override fun setMediaItem(mediaItem: MediaItem, startPositionMs: Long) { - exoPlayer.setMediaItem(mediaItem.clearTag(), startPositionMs) - } - - override fun setMediaItems(mediaItems: List) { - exoPlayer.setMediaItems(mediaItems.map { it.clearTag() }) - } - - override fun setMediaItems(mediaItems: List, resetPosition: Boolean) { - exoPlayer.setMediaItems(mediaItems.map { it.clearTag() }, resetPosition) - } - - override fun setMediaItems(mediaItems: List, startIndex: Int, startPositionMs: Long) { - exoPlayer.setMediaItems(mediaItems.map { it.clearTag() }, startIndex, startPositionMs) - } - - override fun addMediaItem(mediaItem: MediaItem) { - exoPlayer.addMediaItem(mediaItem.clearTag()) - } - - override fun addMediaItem(index: Int, mediaItem: MediaItem) { - exoPlayer.addMediaItem(index, mediaItem.clearTag()) - } - - override fun addMediaItems(mediaItems: List) { - exoPlayer.addMediaItems(mediaItems.map { it.clearTag() }) - } - - override fun addMediaItems(index: Int, mediaItems: List) { - exoPlayer.addMediaItems(index, mediaItems.map { it.clearTag() }) - } - - override fun replaceMediaItem(index: Int, mediaItem: MediaItem) { - exoPlayer.replaceMediaItem(index, mediaItem.clearTag()) - } - - override fun replaceMediaItems(fromIndex: Int, toIndex: Int, mediaItems: List) { - exoPlayer.replaceMediaItems(fromIndex, toIndex, mediaItems.map { it.clearTag() }) - } - - override fun seekTo(positionMs: Long) { - if (!smoothSeekingEnabled) { - exoPlayer.seekTo(positionMs) - return - } - smoothSeekTo(positionMs) - } - - private fun smoothSeekTo(positionMs: Long) { - if (isSeeking) { - pendingSeek = positionMs - return - } - isSeeking = true - exoPlayer.seekTo(positionMs) - } - - override fun seekTo(mediaItemIndex: Int, positionMs: Long) { - if (!smoothSeekingEnabled) { - exoPlayer.seekTo(mediaItemIndex, positionMs) - return - } - smoothSeekTo(mediaItemIndex, positionMs) - } - - private fun smoothSeekTo(mediaItemIndex: Int, positionMs: Long) { - if (mediaItemIndex != currentMediaItemIndex) { - clearSeeking() - exoPlayer.seekTo(mediaItemIndex, positionMs) - return - } - if (isSeeking) { - pendingSeek = positionMs - return - } - exoPlayer.seekTo(mediaItemIndex, positionMs) - } - - override fun seekToDefaultPosition() { - clearSeeking() - exoPlayer.seekToDefaultPosition() - } - - override fun seekToDefaultPosition(mediaItemIndex: Int) { - clearSeeking() - exoPlayer.seekToDefaultPosition(mediaItemIndex) - } - - override fun seekBack() { - clearSeeking() - exoPlayer.seekBack() - } - - override fun seekForward() { - clearSeeking() - exoPlayer.seekForward() - } - - override fun seekToNext() { - clearSeeking() - exoPlayer.seekToNext() - } - - override fun seekToPrevious() { - clearSeeking() - exoPlayer.seekToPrevious() - } - - override fun seekToNextMediaItem() { - clearSeeking() - exoPlayer.seekToNextMediaItem() - } - - override fun seekToPreviousMediaItem() { - clearSeeking() - exoPlayer.seekToPreviousMediaItem() - } - - /** - * Releases the player. - * This method must be called when the player is no longer required. The player must not be used after calling this method. - * - * Release call automatically [stop] if the player is not in [Player.STATE_IDLE]. - */ - override fun release() { - clearSeeking() - if (playbackState != Player.STATE_IDLE) { - stop() - } - exoPlayer.release() - } - - override fun setPlaybackParameters(playbackParameters: PlaybackParameters) { - if (isPlaybackSpeedPossibleAtPosition(currentPosition, playbackParameters.speed, window)) { - exoPlayer.playbackParameters = playbackParameters - } else { - exoPlayer.playbackParameters = playbackParameters.withSpeed(NormalSpeed) - } - } - - override fun setPlaybackSpeed(speed: Float) { - playbackParameters = playbackParameters.withSpeed(speed) - } - - private fun seekEnd() { - isSeeking = false - pendingSeek?.let { pendingPosition -> - pendingSeek = null - seekTo(pendingPosition) - } - } - - private fun clearSeeking() { - isSeeking = false - pendingSeek = null - } - - private inner class ComponentListener : Player.Listener { - private val window = Window() - - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - clearSeeking() - } - - override fun onRenderedFirstFrame() { - seekEnd() - } - - override fun onPlaybackStateChanged(playbackState: Int) { - when (playbackState) { - Player.STATE_READY -> { - if (isSeeking) { - seekEnd() - } - } - - Player.STATE_IDLE, Player.STATE_ENDED -> { - clearSeeking() - } - - Player.STATE_BUFFERING -> { - // Do nothing - } - } - } - - override fun onPlayerError(error: PlaybackException) { - clearSeeking() - if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) { - setPlaybackSpeed(NormalSpeed) - seekToDefaultPosition() - prepare() - } - } - - override fun onEvents(player: Player, events: Player.Events) { - if (!player.isCurrentMediaItemLive || player.getPlaybackSpeed() == NormalSpeed) return - if (!player.isCurrentMediaItemSeekable) { - setPlaybackSpeed(NormalSpeed) - return - } - player.currentTimeline.getWindow(currentMediaItemIndex, window) - if (window.isAtDefaultPosition(currentPosition) && getPlaybackSpeed() > NormalSpeed) { - exoPlayer.setPlaybackSpeed(NormalSpeed) - } - } - } -} - -/** - * Return if the playback [speed] is possible at [position]. - * Always return true for none live content or if [Player.getCurrentTimeline] is empty. - * - * @param position The position to test the playback speed. - * @param speed The playback speed - * @param window optional window for performance purpose - * @return true if the playback [speed] can be set at [position] - */ -fun Player.isPlaybackSpeedPossibleAtPosition(position: Long, speed: Float, window: Window = Window()): Boolean { - if (currentTimeline.isEmpty || speed == NormalSpeed || !isCurrentMediaItemLive) { - return true - } - currentTimeline.getWindow(currentMediaItemIndex, window) - return window.isPlaybackSpeedPossibleAtPosition(position, speed) -} - -internal fun Window.isPlaybackSpeedPossibleAtPosition(positionMs: Long, playbackSpeed: Float): Boolean { - return when { - !isLive() || playbackSpeed == NormalSpeed -> true - !isSeekable -> false - isAtDefaultPosition(positionMs) && playbackSpeed > NormalSpeed -> false - else -> true - } -} - -internal fun Window.isAtDefaultPosition(positionMs: Long): Boolean { - return positionMs >= defaultPositionMs -} - -private const val NormalSpeed = 1.0f - -private fun MediaItem.clearTag() = this.buildUpon().setTag(null).build() diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt index 12dab3d92..d51d8b290 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt @@ -39,3 +39,11 @@ fun Player.getPlaybackSpeed(): Float { fun Player.currentPositionPercentage(): Float { return currentPosition / duration.coerceAtLeast(1).toFloat() } + +/** + * Handle audio focus with currently set AudioAttributes + * @param handleAudioFocus true if the player should handle audio focus, false otherwise. + */ +fun Player.setHandleAudioFocus(handleAudioFocus: Boolean) { + setAudioAttributes(audioAttributes, handleAudioFocus) +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PlaybackService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PlaybackService.kt index 5c7371a7e..1dd11c9b1 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PlaybackService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PlaybackService.kt @@ -15,7 +15,8 @@ import androidx.media3.common.C import androidx.media3.common.util.NotificationUtil import androidx.media3.session.MediaSession import androidx.media3.ui.PlayerNotificationManager -import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.extension.setHandleAudioFocus import ch.srgssr.pillarbox.player.notification.PillarboxMediaDescriptionAdapter /** diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt index 9b54e9173..6577227c2 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt @@ -11,7 +11,8 @@ import androidx.media3.common.Player import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession.ControllerInfo -import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.extension.setHandleAudioFocus import ch.srgssr.pillarbox.player.utils.PendingIntentUtils /** diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt index 30a584ba2..b981a6497 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt @@ -10,7 +10,8 @@ import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService -import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.extension.setHandleAudioFocus import ch.srgssr.pillarbox.player.utils.PendingIntentUtils /** diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxExoPlayerMediaItemTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxExoPlayerMediaItemTest.kt index d36127c67..14e2d7015 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxExoPlayerMediaItemTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxExoPlayerMediaItemTest.kt @@ -11,7 +11,6 @@ import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.test.utils.FakeClock import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.player.extension.getCurrentMediaItems import org.junit.Before import org.junit.runner.RunWith diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestIsPlaybackSpeedPossibleAtPosition.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestIsPlaybackSpeedPossibleAtPosition.kt index 5f1131a59..4273cc7e0 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestIsPlaybackSpeedPossibleAtPosition.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestIsPlaybackSpeedPossibleAtPosition.kt @@ -8,7 +8,6 @@ import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.Timeline.EMPTY import androidx.media3.common.Timeline.Window -import ch.srgssr.pillarbox.player.exoplayer.isPlaybackSpeedPossibleAtPosition import ch.srgssr.pillarbox.player.test.utils.TestTimeline import io.mockk.clearAllMocks import io.mockk.every diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxExoPlayerPlaybackSpeed.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxExoPlayerPlaybackSpeed.kt index ae8d159f7..9abff8b6c 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxExoPlayerPlaybackSpeed.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxExoPlayerPlaybackSpeed.kt @@ -13,7 +13,6 @@ import androidx.media3.test.utils.FakeClock import androidx.media3.test.utils.robolectric.TestPlayerRunHelper import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed import ch.srgssr.pillarbox.player.test.utils.TestPillarboxRunHelper import org.junit.After diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerTest.kt index 16599fef0..4e6cd4dcf 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerTest.kt @@ -13,8 +13,8 @@ import androidx.media3.test.utils.robolectric.RobolectricUtil import androidx.media3.test.utils.robolectric.TestPlayerRunHelper import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import ch.srgssr.pillarbox.player.PillarboxExoPlayer import ch.srgssr.pillarbox.player.SeekIncrement -import ch.srgssr.pillarbox.player.exoplayer.PillarboxExoPlayer import ch.srgssr.pillarbox.player.extension.getMediaItemTrackerData import ch.srgssr.pillarbox.player.extension.getMediaItemTrackerDataOrNull import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory From 726d18127fdcd1b0ede7596acb33c12f6c76c14a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Thu, 4 Apr 2024 15:47:55 +0200 Subject: [PATCH 14/27] Update README.md --- pillarbox-player/docs/README.md | 41 ++++++++++++--------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/pillarbox-player/docs/README.md b/pillarbox-player/docs/README.md index eca2c2dbd..f2fcb4ab0 100644 --- a/pillarbox-player/docs/README.md +++ b/pillarbox-player/docs/README.md @@ -34,7 +34,7 @@ val mediaItem = MediaItem.fromUri(videoUri) ### Create a `PillarboxPlayer` ```kotlin -val player = PillarboxPlayer(context = context) +val player: PillarboxPlayer = PillarboxExoPlayer(context = context) // Make player ready to play content player.prepare() // Will start playback when a MediaItem is ready to play @@ -91,7 +91,7 @@ player.release() ### Connect the player to the MediaSession ```kotlin -val mediaSession = MediaSession.Builder(application, player).build() +val mediaSession = PillarboxMediaSession.Builder(application, player).build() ``` Don't forget to release the `MediaSession` when you no longer need it or when releasing the player with @@ -134,19 +134,14 @@ And since Android 14 (targetApiVersion = 34) a new permission have to be added: ``` -Then in the code you have to use `MediaController` to handle playback, not `PillarboxPlayer`. Pillarbox provide an easy way to retrieve that -`MediaController` with `MediaControllerConnection`. +Then in the code you have to use `PillarboxMediaController` to handle playback, not `PillarboxExoPlayer`. Pillarbox provide an easy way to retrieve +that`MediaController` with `PillarboxMediaController.Builder`. ```kotlin -val sessionToken = SessionToken(application, ComponentName(application, MyMediaSessionService::class.java)) -val listenableFuture = MediaController.Builder(application, sessionToken).setListener(this).buildAsync() - -listenableFuture.addListener({ setController(listenableFuture.get())}, MoreExecutors.directExecutor()) -// or suspend function -setController(listenableFuture.await()) - -// ... -MediaController.release(listenableFuture) +coroutineScope.launch(){ + val mediaController: PillarboxPlayer = PillarboxMediaController.Builder(application,DemoMediaLibraryService::class.java) + doSomethingWith(mediaController) +} ``` ### PillarboxMediaLibraryService @@ -186,24 +181,18 @@ And enable foreground service in the top of the manifest: ``` -Then in the code you have to use `MediaBrowser` to handle playback, not `PillarboxPlayer`. Pillarbox provide an easy way to retrieve that -`MediaBrowser` with `MediaBrowserConnection`. +Then in the code you have to use `PillarboxMediaBrowser` to handle playback, not `PillarboxExoPlayer`. Pillarbox provide an easy way to retrieve that +`MediaBrowser` with `PillarboxMediaBrowser.Builder`. ```kotlin -val sessionToken = SessionToken(application, ComponentName(application, MyMediaSessionService::class.java)) -val listenableFuture = MediaBrowser.Builder(application, sessionToken).setListener(this).buildAsync() - -listenableFuture.addListener({ setMediaBrowser(listenableFuture.get())}, MoreExecutors.directExecutor()) -// or suspend function -setMediaBrowser(listenableFuture.await()) - -// ... -MediaController.release(listenableFuture) +coroutineScope.launch(){ + val mediaBrowser: PillarboxPlayer = PillarboxMediaBrowser.Builder(application,DemoMediaLibraryService::class.java) + doSomethingWith(mediaBrowser) +} ``` - ## Exoplayer -As `PillarboxPlayer` extending an _Exoplayer_ `Player`, all documentation related to Exoplayer is valid for Pillarbox. +As `PillarboxExoPlayer` extending an _Exoplayer_ `Player`, all documentation related to Exoplayer is valid for Pillarbox. - [HelloWorld](https://developer.android.com/media/media3/exoplayer/hello-world.html) - [Player Events](https://developer.android.com/media/media3/exoplayer/listening-to-player-events) From 6f1a24ddf6ab9802d49ae65676824f69735d9f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Thu, 4 Apr 2024 16:42:03 +0200 Subject: [PATCH 15/27] Improve services --- .../demo/service/DemoMediaLibraryService.kt | 2 +- .../demo/service/DemoMediaSessionService.kt | 2 +- .../player/session/PillarboxMediaLibraryService.kt | 13 +++++++++++-- .../player/session/PillarboxMediaSessionService.kt | 10 +++++++--- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt index f8218e4c5..f352f4276 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt @@ -36,7 +36,7 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() { super.onCreate() demoBrowser = DemoBrowser() val player = PlayerModule.provideDefaultPlayer(this) - setPlayer(player, DemoCallback()) + setPlayer(player = player, callback = DemoCallback(), sessionId = "AndroidAutoSession") } override fun sessionActivity(): PendingIntent { diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt index 160d1d45d..853025b71 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaSessionService.kt @@ -35,7 +35,7 @@ class DemoMediaSessionService : PillarboxMediaSessionService() { player.prepare() player.play() - setPlayer(player) + setPlayer(player = player, sessionId = "DemoMediaSession") } override fun sessionActivity(): PendingIntent { diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt index 6577227c2..f6920e80e 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt @@ -67,17 +67,26 @@ abstract class PillarboxMediaLibraryService : MediaLibraryService() { /** * Set player to use with this Service. + * @param player PillarboxPlayer to link to this service. + * @param callback The [PillarboxMediaLibrarySession.Callback] + * @param sessionId The ID. Must be unique among all sessions per package. */ - fun setPlayer(player: PillarboxExoPlayer, callback: PillarboxMediaLibrarySession.Callback) { + fun setPlayer( + player: PillarboxExoPlayer, + callback: PillarboxMediaLibrarySession.Callback, + sessionId: String? = null, + ) { if (this.player == null) { this.player = player player.setWakeMode(C.WAKE_MODE_NETWORK) player.setHandleAudioFocus(true) mediaSession = PillarboxMediaLibrarySession.Builder(this, player, callback).apply { - setId(packageName) sessionActivity()?.let { setSessionActivity(it) } + sessionId?.let { + setId(it) + } }.build() } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt index b981a6497..c92707f0b 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt @@ -64,11 +64,13 @@ abstract class PillarboxMediaSessionService : MediaSessionService() { /** * Set player to use with this Service. * @param player PillarboxPlayer to link to this service. - * @param mediaSessionCallback The MediaSession.Callback to use [MediaSession.Builder.setCallback]. + * @param mediaSessionCallback The MediaSession.Callback to use [MediaSession.Builder.setCallback] + * @param sessionId The ID. Must be unique among all sessions per package. */ fun setPlayer( player: PillarboxExoPlayer, - mediaSessionCallback: PillarboxMediaSession.Callback = PillarboxMediaSession.Callback.Default + mediaSessionCallback: PillarboxMediaSession.Callback = PillarboxMediaSession.Callback.Default, + sessionId: String? = null, ) { if (this.player == null) { this.player = player @@ -78,8 +80,10 @@ abstract class PillarboxMediaSessionService : MediaSessionService() { sessionActivity()?.let { setSessionActivity(it) } - setId("MediaService/$packageName") setCallback(mediaSessionCallback) + sessionId?.let { + setId(it) + } }.build() } } From e1d6c4cf8596e42406ea5068332073c1c165582e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Fri, 5 Apr 2024 11:24:11 +0200 Subject: [PATCH 16/27] Make the project build again --- .../demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt index 33386a8b9..93702e646 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/leanback/LeanbackPlayerFragment.kt @@ -21,6 +21,7 @@ import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.shared.di.PlayerModule 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 From dda55781d44030c10688de737b3338ea4aaba8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Fri, 5 Apr 2024 14:22:42 +0200 Subject: [PATCH 17/27] Use a var instead of setPlayer to keep same api as MediaSession --- .../player/session/PillarboxMediaSession.kt | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt index d353b2820..2a7ea2f3c 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt @@ -7,6 +7,7 @@ package ch.srgssr.pillarbox.player.session import android.app.PendingIntent import android.content.Context import android.os.Bundle +import android.support.v4.media.session.MediaSessionCompat import androidx.media3.common.MediaItem import androidx.media3.common.util.Util import androidx.media3.session.MediaLibraryService @@ -140,37 +141,40 @@ open class PillarboxMediaSession internal constructor() { return _mediaSession } + /** + * @see MediaSession.getSessionCompatToken + */ + val token: MediaSessionCompat.Token + get() { + return _mediaSession.sessionCompatToken + } + /** * Player */ - val player: PillarboxPlayer + var player: PillarboxPlayer get() { return _mediaSession.player as PillarboxPlayer } + set(value) { + if (value != this.player) { + this.player.removeListener(listener) + _mediaSession.player = value + value.addListener(listener) + for (controllerInfo in _mediaSession.connectedControllers) { + _mediaSession.setSessionExtras( + controllerInfo, + playerSessionState.toBundle(_mediaSession.sessionExtras) + ) + } + } + } private val playerSessionState: PlayerSessionState get() { return PlayerSessionState(player) } - /** - * Sets the underlying Player for this session to dispatch incoming events to. - * @see MediaSession.setPlayer - */ - fun setPlayer(player: PillarboxPlayer) { - if (player != this.player) { - this.player.removeListener(listener) - _mediaSession.player = player - player.addListener(listener) - for (controllerInfo in _mediaSession.connectedControllers) { - _mediaSession.setSessionExtras( - controllerInfo, - playerSessionState.toBundle(_mediaSession.sessionExtras) - ) - } - } - } - internal fun setMediaSession(mediaSession: MediaSession) { this._mediaSession = mediaSession player.addListener(listener) From 1cda4ec7d7f778519906f2b0beb4975c683ec2d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Fri, 5 Apr 2024 14:23:22 +0200 Subject: [PATCH 18/27] Update showcase --- .../demo/ui/showcases/misc/MultiPlayerViewModel.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/MultiPlayerViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/MultiPlayerViewModel.kt index 85f267030..3b33d1218 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/MultiPlayerViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/MultiPlayerViewModel.kt @@ -9,13 +9,15 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import androidx.media3.common.C import androidx.media3.common.Player -import androidx.media3.session.MediaSession import androidx.media3.ui.PlayerNotificationManager import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.shared.di.PlayerModule +import ch.srgssr.pillarbox.player.PillarboxExoPlayer import ch.srgssr.pillarbox.player.PillarboxPlayer import ch.srgssr.pillarbox.player.extension.disableAudioTrack +import ch.srgssr.pillarbox.player.extension.setHandleAudioFocus import ch.srgssr.pillarbox.player.notification.PillarboxMediaDescriptionAdapter +import ch.srgssr.pillarbox.player.session.PillarboxMediaSession import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -33,7 +35,7 @@ class MultiPlayerViewModel(application: Application) : AndroidViewModel(applicat .setChannelNameResourceId(androidx.media3.session.R.string.default_notification_channel_name) .setMediaDescriptionAdapter(PillarboxMediaDescriptionAdapter(null, application)) .build() - private val mediaSession: MediaSession + private val mediaSession: PillarboxMediaSession private val _playerOne = PlayerModule.provideDefaultPlayer(application).apply { repeatMode = Player.REPEAT_MODE_ONE @@ -71,10 +73,10 @@ class MultiPlayerViewModel(application: Application) : AndroidViewModel(applicat }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), _playerTwo) init { - mediaSession = MediaSession.Builder(application, _playerTwo) + mediaSession = PillarboxMediaSession.Builder(application, _playerTwo) .setId("MultiPlayerSession") .build() - notificationManager.setMediaSessionToken(mediaSession.sessionCompatToken) + notificationManager.setMediaSessionToken(mediaSession.token) setActivePlayer(_playerOne) } @@ -83,8 +85,8 @@ class MultiPlayerViewModel(application: Application) : AndroidViewModel(applicat * * @param activePlayer The new active player. */ - fun setActivePlayer(activePlayer: PillarboxPlayer) { - val oldActivePlayer = mediaSession.player as PillarboxPlayer + fun setActivePlayer(activePlayer: PillarboxExoPlayer) { + val oldActivePlayer = mediaSession.player as PillarboxExoPlayer _activePlayer.update { activePlayer } mediaSession.player = activePlayer notificationManager.setPlayer(activePlayer) From 30a5189d50c4dd323cdb5656072d56a85233c53c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Fri, 5 Apr 2024 14:37:03 +0200 Subject: [PATCH 19/27] Fix buildHealth --- pillarbox-demo-tv/build.gradle.kts | 1 - pillarbox-player/build.gradle.kts | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pillarbox-demo-tv/build.gradle.kts b/pillarbox-demo-tv/build.gradle.kts index 07f91286d..e0793358e 100644 --- a/pillarbox-demo-tv/build.gradle.kts +++ b/pillarbox-demo-tv/build.gradle.kts @@ -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) diff --git a/pillarbox-player/build.gradle.kts b/pillarbox-player/build.gradle.kts index 4fede1f6d..cb4e93843 100644 --- a/pillarbox-player/build.gradle.kts +++ b/pillarbox-player/build.gradle.kts @@ -1,3 +1,4 @@ + /* * Copyright (c) SRG SSR. All rights reserved. * License information is available from the LICENSE file. @@ -28,7 +29,7 @@ android { dependencies { implementation(libs.androidx.annotation) implementation(libs.androidx.core) - implementation(libs.androidx.media) + api(libs.androidx.media) api(libs.androidx.media3.common) implementation(libs.androidx.media3.dash) implementation(libs.androidx.media3.datasource) @@ -37,7 +38,7 @@ dependencies { api(libs.androidx.media3.session) api(libs.androidx.media3.ui) api(libs.guava) - api(libs.kotlinx.coroutines.guava) + implementation(libs.kotlinx.coroutines.guava) runtimeOnly(libs.kotlinx.coroutines.android) api(libs.kotlinx.coroutines.core) From aa9eb7586989d06084351ac1c91606187d000561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Fri, 5 Apr 2024 14:56:26 +0200 Subject: [PATCH 20/27] Rebase on main --- .../pillarbox/player/PillarboxExoPlayer.kt | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt index ad638d67d..defb06d6f 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt @@ -23,7 +23,8 @@ import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed import ch.srgssr.pillarbox.player.extension.setPreferredAudioRoleFlagsToAccessibilityManagerSettings import ch.srgssr.pillarbox.player.extension.setSeekIncrements import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory -import ch.srgssr.pillarbox.player.tracker.CurrentMediaItemTracker +import ch.srgssr.pillarbox.player.tracker.AnalyticsMediaItemTracker +import ch.srgssr.pillarbox.player.tracker.CurrentMediaItemTagTracker import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository @@ -37,10 +38,11 @@ import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository */ class PillarboxExoPlayer internal constructor( private val exoPlayer: ExoPlayer, - mediaItemTrackerProvider: MediaItemTrackerProvider? + mediaItemTrackerProvider: MediaItemTrackerProvider ) : PillarboxPlayer, ExoPlayer by exoPlayer { private val listeners = HashSet() - private val itemTracker: CurrentMediaItemTracker? + private val itemTagTracker = CurrentMediaItemTagTracker(this) + private val analyticsTracker = AnalyticsMediaItemTracker(this, mediaItemTrackerProvider) private val window = Window() override var smoothSeekingEnabled: Boolean = false set(value) { @@ -51,8 +53,8 @@ class PillarboxExoPlayer internal constructor( } clearSeeking() val listeners = HashSet(listeners) - for (listener in listeners) { - listener.onSmoothSeekingEnabledChanged(value) + listeners.forEach { + it.onSmoothSeekingEnabledChanged(value) } } } @@ -60,26 +62,24 @@ class PillarboxExoPlayer internal constructor( private var isSeeking: Boolean = false /** - * Enable or disable MediaItem tracking + * Enable or disable analytics tracking for the current [MediaItem]. */ - override var trackingEnabled: Boolean - set(value) = itemTracker?.let { - if (it.enabled != value) { - it.enabled = value + set(value) { + if (analyticsTracker.enabled != value) { + analyticsTracker.enabled = value val listeners = HashSet(listeners) - for (listener in listeners) { - listener.onTrackingEnabledChanged(value) + listeners.forEach { + it.onTrackingEnabledChanged(value) } } - } ?: Unit - get() = itemTracker?.enabled ?: false + } + get() = analyticsTracker.enabled init { exoPlayer.addListener(ComponentListener()) - itemTracker = mediaItemTrackerProvider?.let { - CurrentMediaItemTracker(this, it) - } + itemTagTracker.addCallback(analyticsTracker) + if (BuildConfig.DEBUG) { addAnalyticsListener(EventLogger()) } From 6024ff998d76c7ddb9dbaf3d86bbad4fd98c8b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Fri, 5 Apr 2024 15:13:00 +0200 Subject: [PATCH 21/27] fix test after rebase --- .../player/tracker/CurrentMediaItemTagTrackerTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTagTrackerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTagTrackerTest.kt index bcf4c6a50..491a3c083 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTagTrackerTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTagTrackerTest.kt @@ -11,7 +11,7 @@ import androidx.media3.test.utils.FakeClock import androidx.media3.test.utils.robolectric.TestPlayerRunHelper import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import ch.srgssr.pillarbox.player.PillarboxPlayer +import ch.srgssr.pillarbox.player.PillarboxExoPlayer import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import io.mockk.confirmVerified import io.mockk.mockk @@ -33,7 +33,7 @@ class CurrentMediaItemTagTrackerTest { clock = FakeClock(true) context = ApplicationProvider.getApplicationContext() - player = PillarboxPlayer( + player = PillarboxExoPlayer( context = context, mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { addAssetLoader(FakeAssetLoader(context)) From 89833a222620ce5fc6eb83d773558405f8683bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Mon, 8 Apr 2024 09:33:35 +0200 Subject: [PATCH 22/27] Update some documentation --- .../core/business/DefaultPillarbox.kt | 2 +- .../pillarbox/demo/shared/data/DemoBrowser.kt | 2 +- .../demo/service/DemoMediaLibraryService.kt | 4 +- .../demo/ui/showcases/layouts/SimpleStory.kt | 2 +- pillarbox-player/build.gradle.kts | 1 - pillarbox-player/docs/README.md | 20 ++++----- .../pillarbox/player/extension/Player.kt | 4 +- .../player/session/PillarboxMediaBrowser.kt | 10 ++--- .../session/PillarboxMediaController.kt | 42 +++++++++---------- .../session/PillarboxMediaLibraryService.kt | 10 ++--- .../session/PillarboxMediaLibrarySession.kt | 25 ++++++----- .../player/session/PillarboxMediaSession.kt | 4 +- .../session/PillarboxMediaSessionService.kt | 12 +++--- 13 files changed, 69 insertions(+), 69 deletions(-) diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/DefaultPillarbox.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/DefaultPillarbox.kt index a66a0d5fc..263e4b02c 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/DefaultPillarbox.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/DefaultPillarbox.kt @@ -21,7 +21,7 @@ import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider import kotlin.time.Duration.Companion.seconds /** - * DefaultPillarbox convenient class to create [PillarboxExoPlayer] 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) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt index 6cdb89e7a..d62276ee7 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoBrowser.kt @@ -21,7 +21,7 @@ import androidx.media3.common.MediaMetadata class DemoBrowser { /** - * Every android auto navigable MediaItem accessed by id. + * Every Android Auto navigable [MediaItem] accessed by id. */ private val mapMediaIdMediaItem = mutableMapOf() private val mapMediaIdToChildren = mutableMapOf>() diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt index f352f4276..54936f256 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt @@ -24,7 +24,7 @@ import com.google.common.util.concurrent.ListenableFuture import okhttp3.internal.toImmutableList /** - * The only way to handle Android auto application. + * The only way to handle an Android Auto application. * * Hints for testing : https://developer.android.com/training/cars/testing */ @@ -52,7 +52,7 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() { /** * Demo callback is used by Android Auto to create the navigation. - * */ + */ private inner class DemoCallback : PillarboxMediaLibrarySession.Callback { override fun onGetLibraryRoot( session: PillarboxMediaLibrarySession, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/SimpleStory.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/SimpleStory.kt index ec2c47c0b..29b65cb21 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/SimpleStory.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/SimpleStory.kt @@ -57,7 +57,7 @@ fun SimpleStory() { /** * Simple story player - * Each DemoItem have a [PillarboxExoPlayer], the player is released onDispose + * Each [DemoItem] has a [PillarboxExoPlayer], the player is released onDispose * * @param demoItem The DemoItem to play * @param isPlaying to pause or play the player diff --git a/pillarbox-player/build.gradle.kts b/pillarbox-player/build.gradle.kts index cb4e93843..ec29366e5 100644 --- a/pillarbox-player/build.gradle.kts +++ b/pillarbox-player/build.gradle.kts @@ -1,4 +1,3 @@ - /* * Copyright (c) SRG SSR. All rights reserved. * License information is available from the LICENSE file. diff --git a/pillarbox-player/docs/README.md b/pillarbox-player/docs/README.md index f2fcb4ab0..a6e6b3784 100644 --- a/pillarbox-player/docs/README.md +++ b/pillarbox-player/docs/README.md @@ -113,7 +113,6 @@ integrator to handle them. In order to use that service you need to declare it inside the application manifest as follow : ```xml - @@ -121,32 +120,31 @@ In order to use that service you need to declare it inside the application manif ``` -And enable foreground service in the top of the manifest: +And enable foreground service at the top of the manifest: ```xml - ``` -And since Android 14 (targetApiVersion = 34) a new permission have to be added: +And since Android 14 (targetApiVersion >= 34) a new permission has to be added: ```xml ``` -Then in the code you have to use `PillarboxMediaController` to handle playback, not `PillarboxExoPlayer`. Pillarbox provide an easy way to retrieve -that`MediaController` with `PillarboxMediaController.Builder`. +Then in the code you have to use `PillarboxMediaController` to handle playback, not `PillarboxExoPlayer`. Pillarbox provides an easy way to retrieve +that `MediaController` with `PillarboxMediaController.Builder`. ```kotlin -coroutineScope.launch(){ - val mediaController: PillarboxPlayer = PillarboxMediaController.Builder(application,DemoMediaLibraryService::class.java) +coroutineScope.launch() { + val mediaController: PillarboxPlayer = PillarboxMediaController.Builder(application, DemoMediaLibraryService::class.java) doSomethingWith(mediaController) } ``` ### PillarboxMediaLibraryService -`PillarboxMediaLibraryService` have the same feature than `PillarboxMediaSessionService` but it allow the application to provider content with +`PillarboxMediaLibraryService` has the same feature as `PillarboxMediaSessionService` but it allows the application to provide content with _MediaBrowser_. More information about [Android auto](https://developer.android.com/training/auto/audio/). In order to use that service you need to declare it inside the application manifest as follow : @@ -181,11 +179,11 @@ And enable foreground service in the top of the manifest: ``` -Then in the code you have to use `PillarboxMediaBrowser` to handle playback, not `PillarboxExoPlayer`. Pillarbox provide an easy way to retrieve that +Then in the code you have to use `PillarboxMediaBrowser` to handle playback, not `PillarboxExoPlayer`. Pillarbox provides an easy way to retrieve that `MediaBrowser` with `PillarboxMediaBrowser.Builder`. ```kotlin -coroutineScope.launch(){ +coroutineScope.launch() { val mediaBrowser: PillarboxPlayer = PillarboxMediaBrowser.Builder(application,DemoMediaLibraryService::class.java) doSomethingWith(mediaBrowser) } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt index d51d8b290..1930300c7 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt @@ -41,8 +41,8 @@ fun Player.currentPositionPercentage(): Float { } /** - * Handle audio focus with currently set AudioAttributes - * @param handleAudioFocus true if the player should handle audio focus, false otherwise. + * Handle audio focus with the currently set [AudioAttributes][androidx.media3.common.AudioAttributes]. + * @param handleAudioFocus `true` if the player should handle audio focus, `false` otherwise. */ fun Player.setHandleAudioFocus(handleAudioFocus: Boolean) { setAudioAttributes(audioAttributes, handleAudioFocus) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt index dc035d2e3..04de20f16 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaBrowser.kt @@ -14,7 +14,7 @@ import androidx.media3.session.SessionToken import kotlinx.coroutines.guava.await /** - * PillarboxMediaBrowser extends [PillarboxMediaController] but connect to a [PillarboxMediaLibrarySession] from a [MediaLibraryService]. + * [PillarboxMediaBrowser] extends [PillarboxMediaController] but connects to a [PillarboxMediaLibrarySession] from a [MediaLibraryService]. * @see MediaBrowser * @see MediaLibraryService */ @@ -92,7 +92,7 @@ class PillarboxMediaBrowser private constructor() : PillarboxMediaController() { } /** - * Called when there's change in the search result requested by the previous search(String, MediaLibraryService.LibraryParams). + * Called when there's change in the search result requested by the previous [PillarboxMediaBrowser.search]. * * @see MediaBrowser.Listener.onSearchResultChanged */ @@ -121,7 +121,7 @@ class PillarboxMediaBrowser private constructor() : PillarboxMediaController() { /** * Subscribes to a parent id for changes to its children. - * When there's a change, [PillarboxMediaBrowser.Listener.onChildrenChanged] will be called with the MediaLibraryService.LibraryParams. + * When there's a change, [PillarboxMediaBrowser.Listener.onChildrenChanged] will be called with the [MediaLibraryService.LibraryParams]. * You may call [PillarboxMediaBrowser.getChildren] to get the children. * * @param parentId A non-empty parent id to subscribe to. @@ -135,7 +135,7 @@ class PillarboxMediaBrowser private constructor() : PillarboxMediaController() { ) = mediaBrowser.subscribe(parentId, params).await() /** - * Unsubscribes from a parent id for changes to its children, which was previously subscribed by subscribe. + * Unsubscribes from a parent id for changes to its children, which was previously subscribed by [subscribe]. * * @param parentId A non-empty parent id to unsubscribe from. * @see MediaBrowser.unsubscribe @@ -143,7 +143,7 @@ class PillarboxMediaBrowser private constructor() : PillarboxMediaController() { suspend fun unsubscribe(parentId: String) = mediaBrowser.unsubscribe(parentId).await() /** - * Get children for the parentId + * Get children for the [parentId] * * @param parentId A non-empty parent id for getting the children. * @param page A page number to get the paginated result starting from 0. diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt index b731ec08e..7f09a8611 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt @@ -167,7 +167,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { private val listeners = HashSet() /** - * The SessionToken of the connected session, or null if it is not connected. + * The [SessionToken] of the connected session, or `null` if it is not connected. * @see MediaController.getConnectedToken */ val connectedToken: SessionToken? @@ -252,14 +252,14 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } /** - * @See [MediaController.setRating] + * @see [MediaController.setRating] */ fun setRating(rating: Rating): ListenableFuture { return mediaController.setRating(rating) } /** - * @See [MediaController.sendCustomCommand] + * @see [MediaController.sendCustomCommand] */ @JvmOverloads fun sendCustomCommand(command: SessionCommand, args: Bundle = Bundle.EMPTY): ListenableFuture { @@ -476,13 +476,13 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } @UnstableApi - @Deprecated("") + @Deprecated("Use #hasPreviousMediaItem() instead.", ReplaceWith("hasPreviousMediaItem()")) override fun hasPrevious(): Boolean { return mediaController.hasPrevious() } @UnstableApi - @Deprecated("") + @Deprecated("Use #hasPreviousMediaItem() instead.", ReplaceWith("hasPreviousMediaItem()")) override fun hasPreviousWindow(): Boolean { return mediaController.hasPreviousWindow() } @@ -492,13 +492,13 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } @UnstableApi - @Deprecated("") + @Deprecated("Use #seekToPreviousMediaItem() instead.", ReplaceWith("seekToPreviousMediaItem()")) override fun previous() { mediaController.previous() } @UnstableApi - @Deprecated("") + @Deprecated("Use #seekToPreviousMediaItem() instead.", ReplaceWith("seekToPreviousMediaItem()")) override fun seekToPreviousWindow() { mediaController.seekToPreviousWindow() } @@ -516,13 +516,13 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } @UnstableApi - @Deprecated("") + @Deprecated("Use #hasNextMediaItem() instead.", ReplaceWith("hasNextMediaItem()")) override fun hasNext(): Boolean { return mediaController.hasNext() } @UnstableApi - @Deprecated("") + @Deprecated("Use #hasNextMediaItem() instead.", ReplaceWith("hasNextMediaItem()")) override fun hasNextWindow(): Boolean { return mediaController.hasNextWindow() } @@ -532,13 +532,13 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } @UnstableApi - @Deprecated("") + @Deprecated("Use #seekToNextMediaItem() instead.", ReplaceWith("seekToNextMediaItem()")) override fun next() { mediaController.next() } @UnstableApi - @Deprecated("") + @Deprecated("Use #seekToNextMediaItem() instead.", ReplaceWith("seekToNextMediaItem()")) override fun seekToNextWindow() { mediaController.seekToNextWindow() } @@ -609,7 +609,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } @UnstableApi - @Deprecated("") + @Deprecated("Use getCurrentMediaItemIndex() instead.", ReplaceWith("getCurrentMediaItemIndex()")) override fun getCurrentWindowIndex(): Int { return mediaController.currentWindowIndex } @@ -619,7 +619,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } @UnstableApi - @Deprecated("") + @Deprecated("Use getNextMediaItemIndex() instead.", ReplaceWith("getNextMediaItemIndex()")) override fun getNextWindowIndex(): Int { return mediaController.nextWindowIndex } @@ -629,7 +629,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } @UnstableApi - @Deprecated("") + @Deprecated("Use getPreviousMediaItemIndex() instead.", ReplaceWith("getPreviousMediaItemIndex()")) override fun getPreviousWindowIndex(): Int { return mediaController.previousWindowIndex } @@ -672,7 +672,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } @UnstableApi - @Deprecated("") + @Deprecated("Use isCurrentMediaItemDynamic() instead.", ReplaceWith("isCurrentMediaItemDynamic()")) override fun isCurrentWindowDynamic(): Boolean { return mediaController.isCurrentWindowDynamic } @@ -682,7 +682,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } @UnstableApi - @Deprecated("") + @Deprecated("Use isCurrentMediaItemLive() instead.", ReplaceWith("isCurrentMediaItemLive()")) override fun isCurrentWindowLive(): Boolean { return mediaController.isCurrentWindowLive } @@ -696,7 +696,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } @UnstableApi - @Deprecated("") + @Deprecated("Use isCurrentMediaItemSeekable() instead.", ReplaceWith("isCurrentMediaItemSeekable()")) override fun isCurrentWindowSeekable(): Boolean { return mediaController.isCurrentWindowSeekable } @@ -804,7 +804,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { return mediaController.isDeviceMuted() } - @Deprecated("") + @Deprecated("Use setDeviceVolume(Int, Int) instead.", ReplaceWith("setDeviceVolume(volume, 0)")) override fun setDeviceVolume(volume: Int) { mediaController.setDeviceVolume(volume) } @@ -813,7 +813,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { mediaController.setDeviceVolume(volume, flags) } - @Deprecated("") + @Deprecated("Use increaseDeviceVolume(Int) instead.", ReplaceWith("increaseDeviceVolume(0)")) override fun increaseDeviceVolume() { mediaController.increaseDeviceVolume() } @@ -822,7 +822,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { mediaController.increaseDeviceVolume(flags) } - @Deprecated("") + @Deprecated("Use decreaseDeviceVolume(Int) instead.", ReplaceWith("decreaseDeviceVolume(0)")) override fun decreaseDeviceVolume() { mediaController.decreaseDeviceVolume() } @@ -831,7 +831,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { mediaController.decreaseDeviceVolume(flags) } - @Deprecated("") + @Deprecated("Use setDeviceMuted(Boolean, Int) instead.", ReplaceWith("setDeviceMuted(muted, 0)")) override fun setDeviceMuted(muted: Boolean) { mediaController.setDeviceMuted(muted) } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt index f6920e80e..6cbbd557d 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt @@ -46,14 +46,14 @@ import ch.srgssr.pillarbox.player.utils.PendingIntentUtils * * ``` * - * Use [PillarboxMediaBrowser.Builder] to connect this Service to a `PillarboxMediaBrowser`: + * Use [PillarboxMediaBrowser.Builder] to connect this Service to a [PillarboxMediaBrowser]: * ```kotlin - * coroutineScope.launch(){ - * val mediaBrowser = PillarboxMediaBrowser.Builder(application,DemoMediaLibraryService::class.java) + * coroutineScope.launch() { + * val mediaBrowser = PillarboxMediaBrowser.Builder(application, DemoMediaLibraryService::class.java) * doSomethingWith(mediaBrowser) * } * ... - * mediaBrowser.release() // when MediaBrowser no more needed. + * mediaBrowser.release() // when the MediaBrowser is no longer needed. * ``` */ abstract class PillarboxMediaLibraryService : MediaLibraryService() { @@ -67,7 +67,7 @@ abstract class PillarboxMediaLibraryService : MediaLibraryService() { /** * Set player to use with this Service. - * @param player PillarboxPlayer to link to this service. + * @param player [PillarboxExoPlayer] to link to this service. * @param callback The [PillarboxMediaLibrarySession.Callback] * @param sessionId The ID. Must be unique among all sessions per package. */ diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt index a1255c6cb..b0fe4a1d8 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt @@ -28,18 +28,19 @@ import com.google.common.util.concurrent.ListenableFuture * @see PillarboxMediaLibraryService * @see PillarboxMediaBrowser */ -open class PillarboxMediaLibrarySession internal constructor() : - PillarboxMediaSession() { +open class PillarboxMediaLibrarySession internal constructor() : PillarboxMediaSession() { /** * An extended [PillarboxMediaSession.Callback] for the [PillarboxMediaLibrarySession]. - *

When you return [LibraryResult] with [MediaItem] media items, each item must - * have valid [MediaItem.mediaId ]and specify [MediaMetadata.isBrowsable] and [MediaMetadata.isPlayable] in its [MediaItem.mediaMetadata]. + * + * When you return [LibraryResult] with [MediaItem] media items, each item must + * have valid [mediaId][MediaItem.mediaId] and specify [isBrowsable][MediaMetadata.isBrowsable] and [isPlayable][MediaMetadata.isPlayable] in its + * [mediaMetadata][MediaItem.mediaMetadata]. * @see MediaLibrarySession.Callback */ interface Callback : PillarboxMediaSession.Callback { /** - * Called when a [PillarboxMediaBrowser] requests the root [MediaItem]. + * Called when a [PillarboxMediaBrowser] requests the root [MediaItem]. * @see MediaLibrarySession.Callback.onGetLibraryRoot */ fun onGetLibraryRoot( @@ -66,7 +67,7 @@ open class PillarboxMediaLibrarySession internal constructor() : } /** - * Called when a [PillarboxMediaBrowser] requests a [MediaItem] from mediaId. + * Called when a [PillarboxMediaBrowser] requests a [MediaItem] from [mediaId]. * @see MediaLibrarySession.Callback.onGetItem */ fun onGetItem( @@ -78,7 +79,7 @@ open class PillarboxMediaLibrarySession internal constructor() : } /** - * Called when a [androidx.media3.session.MediaBrowser] requests a search. + * Called when a [MediaBrowser][androidx.media3.session.MediaBrowser] requests a search. * @see MediaLibrarySession.Callback.onSearch */ fun onSearch( @@ -126,9 +127,9 @@ open class PillarboxMediaLibrarySession internal constructor() : /** * Set session activity - * @see MediaLibrarySession.Builder.setSessionActivity * @param pendingIntent The [PendingIntent]. * @return the builder for convenience. + * @see MediaLibrarySession.Builder.setSessionActivity */ fun setSessionActivity(pendingIntent: PendingIntent): Builder { this.pendingIntent = pendingIntent @@ -137,9 +138,9 @@ open class PillarboxMediaLibrarySession internal constructor() : /** * Set id - * @see MediaLibrarySession.Builder.setId * @param id The ID. Must be unique among all sessions per package. * @return the builder for convenience. + * @see MediaLibrarySession.Builder.setId */ fun setId(id: String): Builder { this.id = id @@ -167,8 +168,10 @@ open class PillarboxMediaLibrarySession internal constructor() : override val mediaSession: MediaLibrarySession get() = super.mediaSession as MediaLibrarySession - internal class MediaLibraryCallbackImpl(callback: Callback, mediaSession: PillarboxMediaLibrarySession) : - MediaSessionCallbackImpl(callback, mediaSession), MediaLibrarySession.Callback { + internal class MediaLibraryCallbackImpl( + callback: Callback, + mediaSession: PillarboxMediaLibrarySession + ) : MediaSessionCallbackImpl(callback, mediaSession), MediaLibrarySession.Callback { override fun onGetLibraryRoot( session: MediaLibrarySession, browser: MediaSession.ControllerInfo, diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt index 2a7ea2f3c..21beb3de7 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt @@ -22,7 +22,7 @@ import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture /** - * PillarboxMediaSession link together a [MediaSession] to a [PillarboxPlayer]. + * [PillarboxMediaSession] link together a [MediaSession] to a [PillarboxPlayer]. */ open class PillarboxMediaSession internal constructor() { @@ -79,7 +79,7 @@ open class PillarboxMediaSession internal constructor() { private var callback: Callback = object : Callback {} /** - * Sets a PendingIntent to launch an android.app.Activity for the MediaSession. + * Sets a [PendingIntent] to launch an [Activity][android.app.Activity] for the [MediaSession]. * This can be used as a quick link to an ongoing media screen. * * @param pendingIntent The [PendingIntent]. diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt index c92707f0b..2df071101 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSessionService.kt @@ -41,14 +41,14 @@ import ch.srgssr.pillarbox.player.utils.PendingIntentUtils * * ``` * - * Use [PillarboxMediaController.Builder] to connect this Service to a `PillarboxMediaController`: + * Use [PillarboxMediaController.Builder] to connect this Service to a [PillarboxMediaController]: * ```kotlin - * coroutineScope.launch(){ - * val mediaController: PillarboxPlayer = PillarboxMediaController.Builder(application,DemoMediaLibraryService::class.java) + * coroutineScope.launch() { + * val mediaController: PillarboxPlayer = PillarboxMediaController.Builder(application, DemoMediaLibraryService::class.java) * doSomethingWith(mediaController) * } * ... - * mediaController.release() // when mediaController no more needed. + * mediaController.release() // when the MediaController is no longer needed. * ``` */ @Suppress("MemberVisibilityCanBePrivate") @@ -63,8 +63,8 @@ abstract class PillarboxMediaSessionService : MediaSessionService() { /** * Set player to use with this Service. - * @param player PillarboxPlayer to link to this service. - * @param mediaSessionCallback The MediaSession.Callback to use [MediaSession.Builder.setCallback] + * @param player [PillarboxExoPlayer] to link to this service. + * @param mediaSessionCallback The [PillarboxMediaSession.Callback] * @param sessionId The ID. Must be unique among all sessions per package. */ fun setPlayer( From a879368995b769d3d1dcc886ee2af8f625c32ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Mon, 8 Apr 2024 09:34:50 +0200 Subject: [PATCH 23/27] Ignore warnings --- .../player/session/PillarboxMediaController.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt index 7f09a8611..cd25d93df 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt @@ -478,12 +478,14 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @UnstableApi @Deprecated("Use #hasPreviousMediaItem() instead.", ReplaceWith("hasPreviousMediaItem()")) override fun hasPrevious(): Boolean { + @Suppress("DEPRECATION") return mediaController.hasPrevious() } @UnstableApi @Deprecated("Use #hasPreviousMediaItem() instead.", ReplaceWith("hasPreviousMediaItem()")) override fun hasPreviousWindow(): Boolean { + @Suppress("DEPRECATION") return mediaController.hasPreviousWindow() } @@ -494,12 +496,14 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @UnstableApi @Deprecated("Use #seekToPreviousMediaItem() instead.", ReplaceWith("seekToPreviousMediaItem()")) override fun previous() { + @Suppress("DEPRECATION") mediaController.previous() } @UnstableApi @Deprecated("Use #seekToPreviousMediaItem() instead.", ReplaceWith("seekToPreviousMediaItem()")) override fun seekToPreviousWindow() { + @Suppress("DEPRECATION") mediaController.seekToPreviousWindow() } @@ -518,12 +522,14 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @UnstableApi @Deprecated("Use #hasNextMediaItem() instead.", ReplaceWith("hasNextMediaItem()")) override fun hasNext(): Boolean { + @Suppress("DEPRECATION") return mediaController.hasNext() } @UnstableApi @Deprecated("Use #hasNextMediaItem() instead.", ReplaceWith("hasNextMediaItem()")) override fun hasNextWindow(): Boolean { + @Suppress("DEPRECATION") return mediaController.hasNextWindow() } @@ -534,12 +540,14 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @UnstableApi @Deprecated("Use #seekToNextMediaItem() instead.", ReplaceWith("seekToNextMediaItem()")) override fun next() { + @Suppress("DEPRECATION") mediaController.next() } @UnstableApi @Deprecated("Use #seekToNextMediaItem() instead.", ReplaceWith("seekToNextMediaItem()")) override fun seekToNextWindow() { + @Suppress("DEPRECATION") mediaController.seekToNextWindow() } @@ -611,6 +619,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @UnstableApi @Deprecated("Use getCurrentMediaItemIndex() instead.", ReplaceWith("getCurrentMediaItemIndex()")) override fun getCurrentWindowIndex(): Int { + @Suppress("DEPRECATION") return mediaController.currentWindowIndex } @@ -621,6 +630,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @UnstableApi @Deprecated("Use getNextMediaItemIndex() instead.", ReplaceWith("getNextMediaItemIndex()")) override fun getNextWindowIndex(): Int { + @Suppress("DEPRECATION") return mediaController.nextWindowIndex } @@ -631,6 +641,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @UnstableApi @Deprecated("Use getPreviousMediaItemIndex() instead.", ReplaceWith("getPreviousMediaItemIndex()")) override fun getPreviousWindowIndex(): Int { + @Suppress("DEPRECATION") return mediaController.previousWindowIndex } @@ -674,6 +685,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @UnstableApi @Deprecated("Use isCurrentMediaItemDynamic() instead.", ReplaceWith("isCurrentMediaItemDynamic()")) override fun isCurrentWindowDynamic(): Boolean { + @Suppress("DEPRECATION") return mediaController.isCurrentWindowDynamic } @@ -684,6 +696,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @UnstableApi @Deprecated("Use isCurrentMediaItemLive() instead.", ReplaceWith("isCurrentMediaItemLive()")) override fun isCurrentWindowLive(): Boolean { + @Suppress("DEPRECATION") return mediaController.isCurrentWindowLive } @@ -698,6 +711,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @UnstableApi @Deprecated("Use isCurrentMediaItemSeekable() instead.", ReplaceWith("isCurrentMediaItemSeekable()")) override fun isCurrentWindowSeekable(): Boolean { + @Suppress("DEPRECATION") return mediaController.isCurrentWindowSeekable } @@ -806,6 +820,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @Deprecated("Use setDeviceVolume(Int, Int) instead.", ReplaceWith("setDeviceVolume(volume, 0)")) override fun setDeviceVolume(volume: Int) { + @Suppress("DEPRECATION") mediaController.setDeviceVolume(volume) } @@ -815,6 +830,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @Deprecated("Use increaseDeviceVolume(Int) instead.", ReplaceWith("increaseDeviceVolume(0)")) override fun increaseDeviceVolume() { + @Suppress("DEPRECATION") mediaController.increaseDeviceVolume() } @@ -824,6 +840,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @Deprecated("Use decreaseDeviceVolume(Int) instead.", ReplaceWith("decreaseDeviceVolume(0)")) override fun decreaseDeviceVolume() { + @Suppress("DEPRECATION") mediaController.decreaseDeviceVolume() } @@ -833,6 +850,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { @Deprecated("Use setDeviceMuted(Boolean, Int) instead.", ReplaceWith("setDeviceMuted(muted, 0)")) override fun setDeviceMuted(muted: Boolean) { + @Suppress("DEPRECATION") mediaController.setDeviceMuted(muted) } From 3c5476c0278601514ccaaba02ce655eb0a2c6783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Mon, 8 Apr 2024 09:35:29 +0200 Subject: [PATCH 24/27] Remove code duplication --- .../player/session/PillarboxMediaController.kt | 4 ++-- .../pillarbox/player/session/PillarboxMediaSession.kt | 11 +++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt index cd25d93df..a889624b1 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt @@ -164,7 +164,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { private lateinit var mediaController: MediaController private lateinit var playerSessionState: PlayerSessionState - private val listeners = HashSet() + private val listeners = mutableSetOf() /** * The [SessionToken] of the connected session, or `null` if it is not connected. @@ -272,7 +272,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { } }, MoreExecutors.directExecutor() ) - return mediaController.sendCustomCommand(command, args) + return result } /** diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt index 21beb3de7..8400f956d 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt @@ -76,7 +76,7 @@ open class PillarboxMediaSession internal constructor() { */ class Builder(context: Context, player: PillarboxPlayer) { private val mediaSessionBuilder = MediaSession.Builder(context, player) - private var callback: Callback = object : Callback {} + private var callback: Callback = Callback.Default /** * Sets a [PendingIntent] to launch an [Activity][android.app.Activity] for the [MediaSession]. @@ -161,12 +161,7 @@ open class PillarboxMediaSession internal constructor() { this.player.removeListener(listener) _mediaSession.player = value value.addListener(listener) - for (controllerInfo in _mediaSession.connectedControllers) { - _mediaSession.setSessionExtras( - controllerInfo, - playerSessionState.toBundle(_mediaSession.sessionExtras) - ) - } + listener.updateMediaSessionExtras() } } @@ -190,7 +185,7 @@ open class PillarboxMediaSession internal constructor() { private inner class ComponentListener : PillarboxPlayer.Listener { - private fun updateMediaSessionExtras() { + internal fun updateMediaSessionExtras() { for (controllerInfo in _mediaSession.connectedControllers) { _mediaSession.setSessionExtras( controllerInfo, From 676a76cc95c28b0e089b14120cec52bb633cf5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Mon, 8 Apr 2024 09:36:23 +0200 Subject: [PATCH 25/27] Fix `TRACKER_ENABLED` command --- .../session/PillarboxMediaController.kt | 2 +- .../player/session/PillarboxMediaSession.kt | 2 +- .../session/PillarboxSessionCommands.kt | 12 ++-- .../session/PillarboxSessionCommandsTest.kt | 67 +++++++++++++++++++ 4 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommandsTest.kt diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt index a889624b1..4f11dfa7a 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt @@ -212,7 +212,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { override var smoothSeekingEnabled: Boolean set(value) { - sendCustomCommand(PillarboxSessionCommands.setSmoothSeekingCommand(value)) + sendCustomCommand(PillarboxSessionCommands.setSmoothSeekingEnabled(value)) } get() { return playerSessionState.smoothSeekingEnabled diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt index 8400f956d..52c00dd1d 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt @@ -216,7 +216,7 @@ open class PillarboxMediaSession internal constructor() { addSessionCommands(MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.commands) } // TODO maybe add a way integrators can add custom commands - add(PillarboxSessionCommands.COMMAND_SEEK_ENABLED) + add(PillarboxSessionCommands.COMMAND_SMOOTH_SEEKING_ENABLED) add(PillarboxSessionCommands.COMMAND_TRACKER_ENABLED) }.build() val pillarboxPlayer = session.player as PillarboxPlayer diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt index eba61e32a..b30c00e8f 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommands.kt @@ -17,16 +17,18 @@ internal object PillarboxSessionCommands { /** * Place holder command */ - val COMMAND_SEEK_ENABLED = SessionCommand(SMOOTH_SEEKING_ENABLED, Bundle.EMPTY) + val COMMAND_SMOOTH_SEEKING_ENABLED = SessionCommand(SMOOTH_SEEKING_ENABLED, Bundle.EMPTY) /** * Place holder command */ val COMMAND_TRACKER_ENABLED = SessionCommand(TRACKER_ENABLED, Bundle.EMPTY) - fun setSmoothSeekingCommand(smoothSeekingEnabled: Boolean) = - SessionCommand(SMOOTH_SEEKING_ENABLED, Bundle().apply { putBoolean(SMOOTH_SEEKING_ARG, smoothSeekingEnabled) }) + fun setSmoothSeekingEnabled(smoothSeekingEnabled: Boolean): SessionCommand { + return SessionCommand(SMOOTH_SEEKING_ENABLED, Bundle().apply { putBoolean(SMOOTH_SEEKING_ARG, smoothSeekingEnabled) }) + } - fun setTrackerEnabled(enabled: Boolean) = - SessionCommand(SMOOTH_SEEKING_ENABLED, Bundle().apply { putBoolean(TRACKER_ENABLED_ARG, enabled) }) + fun setTrackerEnabled(trackerEnabled: Boolean): SessionCommand { + return SessionCommand(TRACKER_ENABLED, Bundle().apply { putBoolean(TRACKER_ENABLED_ARG, trackerEnabled) }) + } } diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommandsTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommandsTest.kt new file mode 100644 index 000000000..e466f5250 --- /dev/null +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/session/PillarboxSessionCommandsTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.session + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class PillarboxSessionCommandsTest { + @Test + fun `empty smooth seeking enabled command`() { + val command = PillarboxSessionCommands.COMMAND_SMOOTH_SEEKING_ENABLED + + assertEquals(PillarboxSessionCommands.SMOOTH_SEEKING_ENABLED, command.customAction) + assertTrue(command.customExtras.isEmpty) + } + + @Test + fun `empty tracker enabled command`() { + val command = PillarboxSessionCommands.COMMAND_TRACKER_ENABLED + + assertEquals(PillarboxSessionCommands.TRACKER_ENABLED, command.customAction) + assertTrue(command.customExtras.isEmpty) + } + + @Test + fun `set smooth seeking enabled`() { + val command = PillarboxSessionCommands.setSmoothSeekingEnabled(true) + + assertEquals(PillarboxSessionCommands.SMOOTH_SEEKING_ENABLED, command.customAction) + assertEquals(1, command.customExtras.size()) + assertTrue(command.customExtras.getBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG)) + } + + @Test + fun `set smooth seeking disabled`() { + val command = PillarboxSessionCommands.setSmoothSeekingEnabled(false) + + assertEquals(PillarboxSessionCommands.SMOOTH_SEEKING_ENABLED, command.customAction) + assertEquals(1, command.customExtras.size()) + assertFalse(command.customExtras.getBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG)) + } + + @Test + fun `set tracker enabled`() { + val command = PillarboxSessionCommands.setTrackerEnabled(true) + + assertEquals(PillarboxSessionCommands.TRACKER_ENABLED, command.customAction) + assertEquals(1, command.customExtras.size()) + assertTrue(command.customExtras.getBoolean(PillarboxSessionCommands.TRACKER_ENABLED_ARG)) + } + + @Test + fun `set tracker disabled`() { + val command = PillarboxSessionCommands.setTrackerEnabled(false) + + assertEquals(PillarboxSessionCommands.TRACKER_ENABLED, command.customAction) + assertEquals(1, command.customExtras.size()) + assertFalse(command.customExtras.getBoolean(PillarboxSessionCommands.TRACKER_ENABLED_ARG)) + } +} From fd5ccc443758e39360a0ca74d5d93b23af38e333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Mon, 8 Apr 2024 09:36:41 +0200 Subject: [PATCH 26/27] Add missing handling of the `TRACKER_ENABLED` command --- .../player/session/PillarboxMediaSession.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt index 52c00dd1d..44e52237b 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt @@ -229,6 +229,7 @@ open class PillarboxMediaSession internal constructor() { .build() } + @Suppress("ReturnCount") override fun onCustomCommand( session: MediaSession, controller: MediaSession.ControllerInfo, @@ -237,11 +238,18 @@ open class PillarboxMediaSession internal constructor() { ): ListenableFuture { // TODO maybe add a way integrators can add custom commands DebugLogger.debug(TAG, "onCustomCommand ${customCommand.customAction} ${customCommand.customExtras}") + val player = session.player when (customCommand.customAction) { PillarboxSessionCommands.SMOOTH_SEEKING_ENABLED -> { - if (session.player is PillarboxPlayer) { - (session.player as PillarboxPlayer).smoothSeekingEnabled = - customCommand.customExtras.getBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG) + if (player is PillarboxPlayer) { + player.smoothSeekingEnabled = customCommand.customExtras.getBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG) + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + } + + PillarboxSessionCommands.TRACKER_ENABLED -> { + if (player is PillarboxPlayer) { + player.trackingEnabled = customCommand.customExtras.getBoolean(PillarboxSessionCommands.TRACKER_ENABLED_ARG) return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } } From 7b5262bb3129e7568f060fae3a3baac8ca031583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Mon, 8 Apr 2024 09:36:46 +0200 Subject: [PATCH 27/27] Add test --- .../player/session/PlayerSessionStateTest.kt | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/session/PlayerSessionStateTest.kt diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/session/PlayerSessionStateTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/session/PlayerSessionStateTest.kt new file mode 100644 index 000000000..376a31923 --- /dev/null +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/session/PlayerSessionStateTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.session + +import android.content.Context +import android.os.Bundle +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import ch.srgssr.pillarbox.player.PillarboxExoPlayer +import org.junit.runner.RunWith +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class PlayerSessionStateTest { + @Test + fun `create with bundle`() { + val bundle = Bundle().apply { + putBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG, true) + putBoolean(PillarboxSessionCommands.TRACKER_ENABLED_ARG, false) + } + val sessionState = PlayerSessionState(bundle) + + assertTrue(sessionState.smoothSeekingEnabled) + assertFalse(sessionState.trackingEnabled) + } + + @Test + fun `create with empty bundle`() { + val bundle = Bundle.EMPTY + val sessionState = PlayerSessionState(bundle) + + assertFalse(sessionState.smoothSeekingEnabled) + assertFalse(sessionState.trackingEnabled) + } + + @Test + fun `create with Pillarbox player`() { + val context = ApplicationProvider.getApplicationContext() + val player = PillarboxExoPlayer(context).apply { + smoothSeekingEnabled = false + trackingEnabled = true + } + val sessionState = PlayerSessionState(player) + + assertFalse(sessionState.smoothSeekingEnabled) + assertTrue(sessionState.trackingEnabled) + } + + @Test + fun `to bundle`() { + val context = ApplicationProvider.getApplicationContext() + val player = PillarboxExoPlayer(context).apply { + smoothSeekingEnabled = false + trackingEnabled = true + } + val sessionState = PlayerSessionState(player) + + val bundle = sessionState.toBundle() + + assertEquals(2, bundle.size()) + assertFalse(bundle.getBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG)) + assertTrue(bundle.getBoolean(PillarboxSessionCommands.TRACKER_ENABLED_ARG)) + } + + @Test + fun `to bundle with extra data`() { + val context = ApplicationProvider.getApplicationContext() + val player = PillarboxExoPlayer(context).apply { + smoothSeekingEnabled = false + trackingEnabled = true + } + val sessionState = PlayerSessionState(player) + + val extraData = Bundle().apply { + putString("foo", "bar") + } + val bundle = sessionState.toBundle(extraData) + + assertEquals(extraData.size() + 2, bundle.size()) + assertEquals(extraData.getString("foo"), bundle.getString("foo")) + assertFalse(bundle.getBoolean(PillarboxSessionCommands.SMOOTH_SEEKING_ARG)) + assertTrue(bundle.getBoolean(PillarboxSessionCommands.TRACKER_ENABLED_ARG)) + } +}