From 1dc653015e32a8f618b7381ab216c7c4b4c1fc7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Fri, 27 Sep 2024 14:20:33 +0200 Subject: [PATCH] Revisit media item tracker (#717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gaëtan Muller Co-authored-by: Gaëtan Muller --- .../pillarbox/analytics/SRGAnalytics.kt | 10 +- .../analytics/commandersact/CommandersAct.kt | 17 + .../pillarbox/analytics/comscore/ComScore.kt | 13 + .../core/business/DefaultPillarbox.kt | 8 - .../core/business/source/SRGAssetLoader.kt | 46 ++- .../DefaultMediaItemTrackerRepository.kt | 81 ----- .../business/tracker/SRGEventLoggerTracker.kt | 14 +- .../commandersact/CommandersActStreaming.kt | 140 +++++--- .../commandersact/CommandersActTracker.kt | 28 +- .../tracker/comscore/ComScoreTracker.kt | 15 +- .../core/business/SRGAssetLoaderTest.kt | 82 +++++ .../DefaultMediaItemTrackerRepositoryTest.kt | 51 --- .../tracker/SRGEventLoggerTrackerTest.kt | 5 +- .../CommandersActStreamingTest.kt | 16 + .../CommandersActTrackerIntegrationTest.kt | 109 +++++- .../commandersact/CommandersActTrackerTest.kt | 101 ------ .../ComScoreTrackerIntegrationTest.kt | 78 +++-- .../tracker/comscore/ComScoreTrackerTest.kt | 29 +- .../pillarbox/demo/shared/di/PlayerModule.kt | 2 - .../pillarbox/player/PillarboxExoPlayer.kt | 37 +- .../ch/srgssr/pillarbox/player/asset/Asset.kt | 5 +- .../pillarbox/player/asset/PillarboxData.kt | 27 -- .../pillarbox/player/asset/UrlAssetLoader.kt | 31 +- .../pillarbox/player/extension/Tracks.kt | 21 +- .../player/source/PillarboxMediaPeriod.kt | 55 +-- .../player/source/PillarboxMediaSource.kt | 36 +- .../tracker/AnalyticsMediaItemTracker.kt | 169 ++++------ .../player/tracker/BlockedTimeRangeTracker.kt | 32 +- .../CurrentMediaItemPillarboxDataTracker.kt | 75 ----- .../player/tracker/MediaItemTracker.kt | 46 +-- .../player/tracker/MediaItemTrackerData.kt | 116 ++----- .../player/tracker/MediaItemTrackerList.kt | 72 ---- .../tracker/MediaItemTrackerProvider.kt | 20 -- .../tracker/MediaItemTrackerRepository.kt | 30 -- .../player/PillarboxMediaPeriodTest.kt | 144 ++++++++ .../pillarbox/player/extension/PlayerTest.kt | 34 +- ...urrentMediaItemPillarboxDataTrackerTest.kt | 318 ------------------ .../player/tracker/FakeAssetLoader.kt | 16 +- .../player/tracker/FakeMediaItemTracker.kt | 25 +- .../tracker/MediaItemTrackerDataTest.kt | 88 ----- .../tracker/MediaItemTrackerListTest.kt | 107 ------ .../tracker/MediaItemTrackerRepositoryTest.kt | 49 --- .../player/tracker/MediaItemTrackerTest.kt | 126 +------ 43 files changed, 857 insertions(+), 1667 deletions(-) delete mode 100644 pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepository.kt delete mode 100644 pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepositoryTest.kt delete mode 100644 pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.kt delete mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/PillarboxData.kt delete mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemPillarboxDataTracker.kt delete mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerList.kt delete mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerProvider.kt delete mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerRepository.kt create mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt delete mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemPillarboxDataTrackerTest.kt delete mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerDataTest.kt delete mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerListTest.kt delete mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerRepositoryTest.kt diff --git a/pillarbox-analytics/src/main/java/ch/srgssr/pillarbox/analytics/SRGAnalytics.kt b/pillarbox-analytics/src/main/java/ch/srgssr/pillarbox/analytics/SRGAnalytics.kt index 2b80bcfdd..a2805240c 100644 --- a/pillarbox-analytics/src/main/java/ch/srgssr/pillarbox/analytics/SRGAnalytics.kt +++ b/pillarbox-analytics/src/main/java/ch/srgssr/pillarbox/analytics/SRGAnalytics.kt @@ -12,9 +12,11 @@ import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct import ch.srgssr.pillarbox.analytics.commandersact.CommandersActEvent import ch.srgssr.pillarbox.analytics.commandersact.CommandersActPageView import ch.srgssr.pillarbox.analytics.commandersact.CommandersActSrg +import ch.srgssr.pillarbox.analytics.commandersact.NoOpCommandersAct import ch.srgssr.pillarbox.analytics.comscore.ComScore import ch.srgssr.pillarbox.analytics.comscore.ComScorePageView import ch.srgssr.pillarbox.analytics.comscore.ComScoreSrg +import ch.srgssr.pillarbox.analytics.comscore.NoOpComScore /** * Analytics for SRG SSR @@ -44,18 +46,18 @@ object SRGAnalytics { * SRG CommandersAct analytics, do not use it unless you don't have any other choice! * Meant to be used internally inside Pillarbox */ - val commandersAct: CommandersAct? + val commandersAct: CommandersAct get() { - return instance?.commandersAct + return instance?.commandersAct ?: NoOpCommandersAct } /** * SRG ComScore analytics, do not use it unless you don't have any other choice! * Meant to be used internally inside Pillarbox */ - val comScore: ComScore? + val comScore: ComScore get() { - return instance?.comScore + return instance?.comScore ?: NoOpComScore } /** diff --git a/pillarbox-analytics/src/main/java/ch/srgssr/pillarbox/analytics/commandersact/CommandersAct.kt b/pillarbox-analytics/src/main/java/ch/srgssr/pillarbox/analytics/commandersact/CommandersAct.kt index dac25cdfd..e295ce8ab 100644 --- a/pillarbox-analytics/src/main/java/ch/srgssr/pillarbox/analytics/commandersact/CommandersAct.kt +++ b/pillarbox-analytics/src/main/java/ch/srgssr/pillarbox/analytics/commandersact/CommandersAct.kt @@ -63,3 +63,20 @@ interface CommandersAct { */ fun setConsentServices(consentServices: List) } + +internal object NoOpCommandersAct : CommandersAct { + + override fun sendPageView(pageView: CommandersActPageView) = Unit + + override fun sendEvent(event: CommandersActEvent) = Unit + + override fun sendTcMediaEvent(event: TCMediaEvent) = Unit + + override fun putPermanentData(labels: Map) = Unit + + override fun removePermanentData(label: String) = Unit + + override fun getPermanentDataLabel(label: String): String? = null + + override fun setConsentServices(consentServices: List) = Unit +} diff --git a/pillarbox-analytics/src/main/java/ch/srgssr/pillarbox/analytics/comscore/ComScore.kt b/pillarbox-analytics/src/main/java/ch/srgssr/pillarbox/analytics/comscore/ComScore.kt index 1c8078b21..b978e3f7b 100644 --- a/pillarbox-analytics/src/main/java/ch/srgssr/pillarbox/analytics/comscore/ComScore.kt +++ b/pillarbox-analytics/src/main/java/ch/srgssr/pillarbox/analytics/comscore/ComScore.kt @@ -45,3 +45,16 @@ interface ComScore { */ fun setUserConsent(userConsent: ComScoreUserConsent) } + +internal object NoOpComScore : ComScore { + + override fun sendPageView(pageView: ComScorePageView) = Unit + + override fun putPersistentLabels(labels: Map) = Unit + + override fun removePersistentLabel(label: String) = Unit + + override fun getPersistentLabel(label: String): String? = null + + override fun setUserConsent(userConsent: ComScoreUserConsent) = Unit +} 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 e0cd501a9..029ffc488 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,13 +13,11 @@ import androidx.media3.exoplayer.LoadControl import ch.srgssr.pillarbox.core.business.integrationlayer.service.HttpMediaCompositionService 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.PillarboxExoPlayer.Companion.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION import ch.srgssr.pillarbox.player.PillarboxLoadControl import ch.srgssr.pillarbox.player.SeekIncrement import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext import kotlin.time.Duration @@ -37,7 +35,6 @@ object DefaultPillarbox { * @param context The context. * @param seekIncrement The seek increment. * @param maxSeekToPreviousPosition The [Player.getMaxSeekToPreviousPosition] value. - * @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 [PillarboxExoPlayer] suited for SRG. @@ -46,7 +43,6 @@ object DefaultPillarbox { context: Context, seekIncrement: SeekIncrement = defaultSeekIncrement, maxSeekToPreviousPosition: Duration = DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION, - mediaItemTrackerRepository: MediaItemTrackerProvider = DefaultMediaItemTrackerRepository(), mediaCompositionService: MediaCompositionService = HttpMediaCompositionService(), loadControl: LoadControl = PillarboxLoadControl(), ): PillarboxExoPlayer { @@ -54,7 +50,6 @@ object DefaultPillarbox { context = context, seekIncrement = seekIncrement, maxSeekToPreviousPosition = maxSeekToPreviousPosition, - mediaItemTrackerRepository = mediaItemTrackerRepository, mediaCompositionService = mediaCompositionService, loadControl = loadControl, clock = Clock.DEFAULT, @@ -68,7 +63,6 @@ object DefaultPillarbox { * @param context The context. * @param seekIncrement The seek increment. * @param maxSeekToPreviousPosition The [Player.getMaxSeekToPreviousPosition] value. - * @param mediaItemTrackerRepository The provider of MediaItemTracker, by default [DefaultMediaItemTrackerRepository]. * @param loadControl The load control, by default [DefaultLoadControl]. * @param mediaCompositionService The [MediaCompositionService] to use, by default [HttpMediaCompositionService]. * @param clock The internal clock used by the player. @@ -80,7 +74,6 @@ object DefaultPillarbox { context: Context, seekIncrement: SeekIncrement = defaultSeekIncrement, maxSeekToPreviousPosition: Duration = DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION, - mediaItemTrackerRepository: MediaItemTrackerProvider = DefaultMediaItemTrackerRepository(), loadControl: LoadControl = DefaultLoadControl(), mediaCompositionService: MediaCompositionService = HttpMediaCompositionService(), clock: Clock, @@ -93,7 +86,6 @@ object DefaultPillarbox { mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { addAssetLoader(SRGAssetLoader(context, mediaCompositionService)) }, - mediaItemTrackerProvider = mediaItemTrackerRepository, loadControl = loadControl, clock = clock, coroutineContext = coroutineContext, diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt index 4a2f4d5b1..55f189e54 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt @@ -12,6 +12,8 @@ import androidx.media3.common.MediaMetadata import androidx.media3.common.MimeTypes import androidx.media3.datasource.DefaultDataSource import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import ch.srgssr.pillarbox.analytics.SRGAnalytics +import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct import ch.srgssr.pillarbox.core.business.HttpResultException import ch.srgssr.pillarbox.core.business.akamai.AkamaiTokenDataSource import ch.srgssr.pillarbox.core.business.akamai.AkamaiTokenProvider @@ -31,10 +33,13 @@ import ch.srgssr.pillarbox.core.business.tracker.commandersact.CommandersActTrac import ch.srgssr.pillarbox.core.business.tracker.comscore.ComScoreTracker import ch.srgssr.pillarbox.player.asset.Asset import ch.srgssr.pillarbox.player.asset.AssetLoader -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData +import ch.srgssr.pillarbox.player.tracker.FactoryData +import ch.srgssr.pillarbox.player.tracker.MutableMediaItemTrackerData import io.ktor.client.plugins.ClientRequestException +import kotlinx.coroutines.Dispatchers import kotlinx.serialization.SerializationException import java.io.IOException +import kotlin.coroutines.CoroutineContext /** * Mime Type for representing SRG SSR content @@ -46,13 +51,18 @@ const val MimeTypeSrg = "${MimeTypes.BASE_TYPE_APPLICATION}/srg-ssr" * * @param context The context. * @param mediaCompositionService The service to load a [MediaComposition]. + * @param commandersAct The CommandersAct implementation to use with [CommandersActTracker]. + * @param coroutineContext The [CoroutineContext] to use with [CommandersActTracker] */ -class SRGAssetLoader( +class SRGAssetLoader internal constructor( context: Context, - private val mediaCompositionService: MediaCompositionService = HttpMediaCompositionService() + private val mediaCompositionService: MediaCompositionService, + private val commandersAct: CommandersAct, + private val coroutineContext: CoroutineContext, ) : AssetLoader( mediaSourceFactory = DefaultMediaSourceFactory(AkamaiTokenDataSource.Factory(AkamaiTokenProvider(), DefaultDataSource.Factory(context))) ) { + /** * An interface to customize how [SRGAssetLoader] should fill [MediaMetadata]. */ @@ -80,13 +90,13 @@ class SRGAssetLoader( /** * Provide Tracker Data to the [Asset]. The official SRG trackers are always setup by [SRGAssetLoader]. * - * @param trackerDataBuilder The [MediaItemTrackerData.Builder] to add trackers data. + * @param trackerDataBuilder The [MutableMediaItemTrackerData] to add tracker data. * @param resource The [Resource] the player will play. * @param chapter The main [Chapter] from the mediaComposition. * @param mediaComposition The [MediaComposition] loaded from [MediaCompositionService]. */ fun provide( - trackerDataBuilder: MediaItemTrackerData.Builder, + trackerDataBuilder: MutableMediaItemTrackerData, resource: Resource, chapter: Chapter, mediaComposition: MediaComposition @@ -105,6 +115,11 @@ class SRGAssetLoader( */ var trackerDataProvider: TrackerDataProvider? = null + constructor( + context: Context, + mediaCompositionService: MediaCompositionService = HttpMediaCompositionService(), + ) : this(context, mediaCompositionService, SRGAnalytics.commandersAct, Dispatchers.Default) + override fun canLoadAsset(mediaItem: MediaItem): Boolean { val localConfiguration = mediaItem.localConfiguration ?: return false @@ -139,16 +154,15 @@ class SRGAssetLoader( if (resource.tokenType == Resource.TokenType.AKAMAI) { uri = AkamaiTokenDataSource.appendTokenQueryToUri(uri) } - val trackerData = MediaItemTrackerData.Builder().apply { - trackerDataProvider?.provide(this, resource, chapter, result) - putData(SRGEventLoggerTracker::class.java) - getComScoreData(result, chapter, resource)?.let { - putData(ComScoreTracker::class.java, it) - } - getCommandersActData(result, chapter, resource)?.let { - putData(CommandersActTracker::class.java, it) - } - }.build() + val trackerData = MutableMediaItemTrackerData() + trackerDataProvider?.provide(trackerData, resource, chapter, result) + trackerData[SRGEventLoggerTracker::class.java] = FactoryData(SRGEventLoggerTracker.Factory(), Unit) + getComScoreData(result, chapter, resource)?.let { + trackerData[ComScoreTracker::class.java] = FactoryData(ComScoreTracker.Factory(), it) + } + getCommandersActData(result, chapter, resource)?.let { + trackerData[CommandersActTracker::class.java] = FactoryData(CommandersActTracker.Factory(commandersAct, coroutineContext), it) + } val loadingMediaItem = MediaItem.Builder() .setDrmConfiguration(fillDrmConfiguration(resource)) @@ -156,7 +170,7 @@ class SRGAssetLoader( .build() return Asset( mediaSource = mediaSourceFactory.createMediaSource(loadingMediaItem), - trackersData = trackerData, + trackersData = trackerData.toMediaItemTrackerData(), mediaMetadata = mediaItem.mediaMetadata.buildUpon().apply { mediaMetadataProvider.provide( this, diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepository.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepository.kt deleted file mode 100644 index 8aed06e4a..000000000 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepository.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.core.business.tracker - -import ch.srgssr.pillarbox.analytics.SRGAnalytics -import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct -import ch.srgssr.pillarbox.analytics.commandersact.CommandersActEvent -import ch.srgssr.pillarbox.analytics.commandersact.CommandersActPageView -import ch.srgssr.pillarbox.analytics.commandersact.TCMediaEvent -import ch.srgssr.pillarbox.core.business.tracker.commandersact.CommandersActTracker -import ch.srgssr.pillarbox.core.business.tracker.comscore.ComScoreTracker -import ch.srgssr.pillarbox.player.tracker.MediaItemTracker -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository -import kotlinx.coroutines.Dispatchers -import kotlin.coroutines.CoroutineContext - -/** - * Default media item tracker repository for SRG. - * - * @param trackerRepository The MediaItemTrackerRepository to use to store Tracker.Factory. - * @param commandersAct CommanderAct instance to use for tracking. If set to null no tracking is made. - * @param coroutineContext The coroutine context in which to track the events. - */ -class DefaultMediaItemTrackerRepository internal constructor( - private val trackerRepository: MediaItemTrackerRepository, - commandersAct: CommandersAct?, - coroutineContext: CoroutineContext, -) : MediaItemTrackerProvider by trackerRepository { - init { - registerFactory(SRGEventLoggerTracker::class.java, SRGEventLoggerTracker.Factory()) - registerFactory(ComScoreTracker::class.java, ComScoreTracker.Factory()) - val commanderActOrEmpty = commandersAct ?: EmptyCommandersAct - registerFactory(CommandersActTracker::class.java, CommandersActTracker.Factory(commanderActOrEmpty, coroutineContext)) - } - - constructor() : this(trackerRepository = MediaItemTrackerRepository(), SRGAnalytics.commandersAct, Dispatchers.Default) - - /** - * Register factory - * @see MediaItemTrackerRepository.registerFactory - * @param T Class type extends [MediaItemTracker] - * @param trackerClass The class the trackerFactory create. Clazz must extends MediaItemTracker. - * @param trackerFactory The tracker factory associated with clazz. - */ - fun registerFactory(trackerClass: Class, trackerFactory: MediaItemTracker.Factory) { - trackerRepository.registerFactory(trackerClass, trackerFactory) - } - - private object EmptyCommandersAct : CommandersAct { - override fun sendPageView(pageView: CommandersActPageView) { - // Nothing - } - - override fun sendEvent(event: CommandersActEvent) { - // Nothing - } - - override fun sendTcMediaEvent(event: TCMediaEvent) { - // Nothing - } - - override fun putPermanentData(labels: Map) { - // Nothing - } - - override fun removePermanentData(label: String) { - // Nothing - } - - override fun getPermanentDataLabel(label: String): String? { - return null - } - - override fun setConsentServices(consentServices: List) { - // Nothing - } - } -} diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/SRGEventLoggerTracker.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/SRGEventLoggerTracker.kt index e98631d62..e05eb64a0 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/SRGEventLoggerTracker.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/SRGEventLoggerTracker.kt @@ -4,34 +4,30 @@ */ package ch.srgssr.pillarbox.core.business.tracker -import android.util.Log import androidx.media3.exoplayer.ExoPlayer import ch.srgssr.pillarbox.player.tracker.MediaItemTracker import ch.srgssr.pillarbox.player.utils.PillarboxEventLogger -import kotlin.time.Duration.Companion.milliseconds /** * Enable/Disable EventLogger when item is currently active. */ -class SRGEventLoggerTracker : MediaItemTracker { +class SRGEventLoggerTracker : MediaItemTracker { private val eventLogger = PillarboxEventLogger(TAG) - override fun start(player: ExoPlayer, initialData: Any?) { - Log.w(TAG, "---- Start") + override fun start(player: ExoPlayer, data: Unit) { player.addAnalyticsListener(eventLogger) } - override fun stop(player: ExoPlayer, reason: MediaItemTracker.StopReason, positionMs: Long) { - Log.w(TAG, "---- Stop because $reason at ${positionMs.milliseconds}") + override fun stop(player: ExoPlayer) { player.removeAnalyticsListener(eventLogger) } /** * Factory for a [SRGEventLoggerTracker] */ - class Factory : MediaItemTracker.Factory { + class Factory : MediaItemTracker.Factory { - override fun create(): MediaItemTracker { + override fun create(): MediaItemTracker { return SRGEventLoggerTracker() } } diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreaming.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreaming.kt index 40b62ebb7..19f6bbbd0 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreaming.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreaming.kt @@ -6,9 +6,14 @@ package ch.srgssr.pillarbox.core.business.tracker.commandersact import androidx.media3.common.C import androidx.media3.common.Format +import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.Player.PositionInfo +import androidx.media3.common.Timeline.Window +import androidx.media3.common.Tracks import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.analytics.AnalyticsListener +import androidx.media3.exoplayer.analytics.AnalyticsListener.EventTime import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType import ch.srgssr.pillarbox.analytics.commandersact.TCMediaEvent @@ -65,8 +70,13 @@ internal class CommandersActStreaming( private var state: State = State.Idle private val playtimeTracker = TotalPlaytimeCounter() + private var oldPosition: PositionInfo? = null + private var reachEoF = false + private var currentTracks = player.currentTracks + private val window = Window() init { + player.currentTimeline.getWindow(player.currentMediaItemIndex, window) if (player.isPlaying) { playtimeTracker.play() notifyPlaying() @@ -85,49 +95,14 @@ internal class CommandersActStreaming( uptimeHeartbeat.stop() } - override fun onIsPlayingChanged(eventTime: AnalyticsListener.EventTime, isPlaying: Boolean) { - if (isPlaying) { - playtimeTracker.play() - } else { - playtimeTracker.pause() - } - } - - override fun onEvents(player: Player, events: AnalyticsListener.Events) { - if (events.containsAny(AnalyticsListener.EVENT_PLAYBACK_STATE_CHANGED, AnalyticsListener.EVENT_PLAY_WHEN_READY_CHANGED)) { - if (player.playbackState == Player.STATE_IDLE || player.playbackState == Player.STATE_ENDED) return - if (player.playWhenReady) { - notifyPlaying() - } else { - notifyPause() - } - } - } - - override fun onPositionDiscontinuity( - eventTime: AnalyticsListener.EventTime, - oldPosition: Player.PositionInfo, - newPosition: Player.PositionInfo, - reason: Int - ) { - if (!isPlaying()) return - when (reason) { - Player.DISCONTINUITY_REASON_SEEK, Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT -> { - if (abs(oldPosition.positionMs - newPosition.positionMs) > VALID_SEEK_THRESHOLD) { - notifySeek(oldPosition.positionMs.milliseconds) - } - } - } - } - private fun notifyEvent(type: MediaEventType, position: Duration) { val totalPlayTime = playtimeTracker.getTotalPlayTime() - DebugLogger.debug(TAG, "send : $type position = $position totalPlayTime = $totalPlayTime") + DebugLogger.debug(TAG, "send : $type position = $position totalPlayTime = $totalPlayTime ${window.isLive}") val event = TCMediaEvent(eventType = type, assets = currentData.assets, sourceId = currentData.sourceId) handleTextTrackData(event) handleAudioTrack(event) - if (player.isCurrentMediaItemLive) { + if (window.isLive) { event.timeShift = getTimeshift(position) } val maxVolume = player.deviceInfo.maxVolume @@ -140,7 +115,7 @@ internal class CommandersActStreaming( event.deviceVolume = deviceVolume / volumeRange.toFloat() } - event.mediaPosition = if (player.isCurrentMediaItemLive) totalPlayTime else position + event.mediaPosition = if (window.isLive) totalPlayTime else position commandersAct.sendTcMediaEvent(event) } @@ -158,6 +133,13 @@ internal class CommandersActStreaming( stopHeartBeat() } + fun stop() { + val position = oldPosition?.positionMs ?: player.currentPosition + notifyStop(position.milliseconds, reachEoF) + oldPosition = null + reachEoF = false + } + fun notifyStop(position: Duration, isEoF: Boolean = false) { stopHeartBeat() if (state == State.Idle) return @@ -180,7 +162,7 @@ internal class CommandersActStreaming( } private fun getTimeshift(position: Duration): Duration { - return if (position == ZERO) ZERO else player.duration.milliseconds - position + return if (position == ZERO) ZERO else window.durationMs.milliseconds - position } private fun isPlaying(): Boolean { @@ -195,7 +177,7 @@ internal class CommandersActStreaming( @Suppress("SwallowedException") private fun handleTextTrackData(event: TCMediaEvent) { try { - val selectedTextGroup = player.currentTracks.groups.first { + val selectedTextGroup = currentTracks.groups.first { it.type == C.TRACK_TYPE_TEXT && it.isSelected } val selectedFormat: Format = selectedTextGroup.getTrackFormat(0) @@ -213,7 +195,7 @@ internal class CommandersActStreaming( } private fun handleAudioTrack(event: TCMediaEvent) { - val currentAudioTrack = player.currentTracks.audioTracks.find { it.isSelected } + val currentAudioTrack = currentTracks.audioTracks.find { it.isSelected } val audioTrackLanguage = currentAudioTrack ?.format ?.language @@ -224,6 +206,82 @@ internal class CommandersActStreaming( event.audioTrackHasAudioDescription = currentAudioTrack?.format?.hasAccessibilityRoles() ?: false } + override fun onIsPlayingChanged(eventTime: EventTime, isPlaying: Boolean) { + if (isPlaying) { + playtimeTracker.play() + } else { + playtimeTracker.pause() + } + } + + override fun onEvents(player: Player, events: AnalyticsListener.Events) { + if (events.containsAny(AnalyticsListener.EVENT_PLAYBACK_STATE_CHANGED, AnalyticsListener.EVENT_PLAY_WHEN_READY_CHANGED)) { + if (player.playbackState == Player.STATE_IDLE || player.playbackState == Player.STATE_ENDED) return + if (player.playWhenReady) { + notifyPlaying() + } else { + notifyPause() + } + } + } + + override fun onPlaybackStateChanged( + eventTime: EventTime, + @Player.State playbackState: Int, + ) { + when (playbackState) { + Player.STATE_ENDED -> { + reachEoF = true + oldPosition = null + stop() + } + + else -> Unit + } + } + + override fun onPositionDiscontinuity( + eventTime: EventTime, + oldPosition: PositionInfo, + newPosition: PositionInfo, + reason: Int + ) { + when (reason) { + Player.DISCONTINUITY_REASON_SEEK, Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT -> { + if (oldPosition.mediaItemIndex != newPosition.mediaItemIndex) { + this.oldPosition = oldPosition + } else if (isPlaying() && abs(oldPosition.positionMs - newPosition.positionMs) > VALID_SEEK_THRESHOLD) { + this.oldPosition = null + notifySeek(oldPosition.positionMs.milliseconds) + } + } + + Player.DISCONTINUITY_REASON_AUTO_TRANSITION, Player.DISCONTINUITY_REASON_REMOVE -> { + this.oldPosition = oldPosition + } + } + } + + override fun onMediaItemTransition(eventTime: EventTime, mediaItem: MediaItem?, reason: Int) { + reachEoF = reason <= Player.MEDIA_ITEM_TRANSITION_REASON_AUTO + when (reason) { + Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT -> { + stop() + notifyPlaying() + } + } + } + + override fun onTimelineChanged(eventTime: EventTime, reason: Int) { + if (reason == Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) { + player.currentTimeline.getWindow(player.currentMediaItemIndex, window) + } + } + + override fun onTracksChanged(eventTime: EventTime, tracks: Tracks) { + currentTracks = tracks + } + companion object { private const val TAG = "CommandersActTracker" diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTracker.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTracker.kt index 7f9242d2d..3d82a6f72 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTracker.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTracker.kt @@ -8,7 +8,6 @@ import androidx.media3.exoplayer.ExoPlayer import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct import ch.srgssr.pillarbox.player.tracker.MediaItemTracker import kotlin.coroutines.CoroutineContext -import kotlin.time.Duration.Companion.milliseconds /** * Commanders act tracker @@ -21,7 +20,8 @@ import kotlin.time.Duration.Companion.milliseconds class CommandersActTracker( private val commandersAct: CommandersAct, private val coroutineContext: CoroutineContext, -) : MediaItemTracker { +) : MediaItemTracker { + /** * Data for CommandersAct * @@ -31,31 +31,25 @@ class CommandersActTracker( data class Data(val assets: Map, val sourceId: String? = null) private var analyticsStreaming: CommandersActStreaming? = null - private var currentData: Data? = null - override fun start(player: ExoPlayer, initialData: Any?) { - requireNotNull(initialData) - require(initialData is Data) + override fun start(player: ExoPlayer, data: Data) { commandersAct.enableRunningInBackground() - currentData = initialData analyticsStreaming = CommandersActStreaming( - commandersAct = commandersAct, player = player, - currentData = initialData, - coroutineContext = coroutineContext, - ) - analyticsStreaming?.let { + commandersAct = commandersAct, + currentData = data, + coroutineContext = coroutineContext + ).also { player.addAnalyticsListener(it) } } - override fun stop(player: ExoPlayer, reason: MediaItemTracker.StopReason, positionMs: Long) { + override fun stop(player: ExoPlayer) { analyticsStreaming?.let { player.removeAnalyticsListener(it) - it.notifyStop(position = positionMs.milliseconds, reason == MediaItemTracker.StopReason.EoF) + it.stop() } analyticsStreaming = null - currentData = null } /** @@ -64,8 +58,8 @@ class CommandersActTracker( class Factory( private val commandersAct: CommandersAct, private val coroutineContext: CoroutineContext, - ) : MediaItemTracker.Factory { - override fun create(): MediaItemTracker { + ) : MediaItemTracker.Factory { + override fun create(): CommandersActTracker { return CommandersActTracker(commandersAct, coroutineContext) } } diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTracker.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTracker.kt index b4607902a..27270944c 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTracker.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTracker.kt @@ -25,7 +25,7 @@ import com.comscore.streaming.StreamingAnalytics */ class ComScoreTracker internal constructor( private val streamingAnalytics: StreamingAnalytics = StreamingAnalytics() -) : MediaItemTracker { +) : MediaItemTracker { /** * Data for ComScore * @@ -49,17 +49,15 @@ class ComScoreTracker internal constructor( streamingAnalytics.setMediaPlayerVersion(BuildConfig.VERSION_NAME) } - override fun start(player: ExoPlayer, initialData: Any?) { - requireNotNull(initialData) - require(initialData is Data) + override fun start(player: ExoPlayer, data: Data) { isSurfaceConnected = player.surfaceSize != Size.ZERO streamingAnalytics.createPlaybackSession() - setMetadata(initialData) + setMetadata(data) handleStart(player) player.addAnalyticsListener(component) } - override fun stop(player: ExoPlayer, reason: MediaItemTracker.StopReason, positionMs: Long) { + override fun stop(player: ExoPlayer) { player.removeAnalyticsListener(component) notifyEnd() } @@ -176,6 +174,7 @@ class ComScoreTracker internal constructor( ) { when (reason) { Player.DISCONTINUITY_REASON_SEEK, Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT -> { + if (oldPosition.mediaItemIndex != newPosition.mediaItemIndex) return notifySeek() eventTime.timeline.getWindow(eventTime.windowIndex, window) notifyPosition(newPosition.positionMs, window) @@ -223,8 +222,8 @@ class ComScoreTracker internal constructor( /** * Factory */ - class Factory : MediaItemTracker.Factory { - override fun create(): MediaItemTracker { + class Factory : MediaItemTracker.Factory { + override fun create(): ComScoreTracker { return ComScoreTracker() } } diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGAssetLoaderTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGAssetLoaderTest.kt index d27fd4311..7a507bab2 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGAssetLoaderTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGAssetLoaderTest.kt @@ -26,12 +26,16 @@ import ch.srgssr.pillarbox.core.business.integrationlayer.service.MediaCompositi import ch.srgssr.pillarbox.core.business.source.SRGAssetLoader import ch.srgssr.pillarbox.core.business.source.SegmentAdapter import ch.srgssr.pillarbox.core.business.source.TimeIntervalAdapter +import ch.srgssr.pillarbox.core.business.tracker.commandersact.CommandersActTracker +import ch.srgssr.pillarbox.core.business.tracker.comscore.ComScoreTracker import ch.srgssr.pillarbox.player.extension.credits import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.runner.RunWith import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class SRGAssetLoaderTest { @@ -164,6 +168,39 @@ class SRGAssetLoaderTest { assertEquals(expectedCredits, asset.mediaMetadata.credits) } + @Test + fun `MediaComposition with both analytics`() = runTest { + val asset = assetLoader.loadAsset( + SRGMediaItemBuilder(DummyMediaCompositionProvider.URN_WITH_ANALYTICS).build() + ) + val trackerData = asset.trackersData + assertTrue { trackerData.isNotEmpty() } + assertTrue { trackerData.contains(ComScoreTracker::class.java) } + assertTrue { trackerData.contains(CommandersActTracker::class.java) } + } + + @Test + fun `MediaComposition with comscore only`() = runTest { + val asset = assetLoader.loadAsset( + SRGMediaItemBuilder(DummyMediaCompositionProvider.URN_WITH_COMSCORE).build() + ) + val trackerData = asset.trackersData + assertTrue { trackerData.isNotEmpty() } + assertTrue { trackerData.contains(ComScoreTracker::class.java) } + assertFalse { trackerData.contains(CommandersActTracker::class.java) } + } + + @Test + fun `MediaComposition with commanders act only`() = runTest { + val asset = assetLoader.loadAsset( + SRGMediaItemBuilder(DummyMediaCompositionProvider.URN_WITH_COMMANDERS_ACT).build() + ) + val trackerData = asset.trackersData + assertTrue { trackerData.isNotEmpty() } + assertFalse { trackerData.contains(ComScoreTracker::class.java) } + assertTrue { trackerData.contains(CommandersActTracker::class.java) } + } + internal class DummyMediaCompositionProvider : MediaCompositionService { override suspend fun fetchMediaComposition(uri: Uri): Result { @@ -227,6 +264,48 @@ class SRGAssetLoaderTest { Result.success(MediaComposition(chapterUrn = urn, listChapter = listOf(mainChapter))) } + URN_WITH_ANALYTICS -> { + val mainChapter = Chapter( + urn = urn, + title = "Audio with analytics", + listResource = listOf(createResource(Resource.Type.HLS)), + imageUrl = DUMMY_IMAGE_URL, + listSegment = null, + mediaType = MediaType.AUDIO, + // None empty labels + comScoreAnalyticsLabels = mutableMapOf("key1" to "data"), + analyticsLabels = mutableMapOf("key1" to "data"), + ) + Result.success(MediaComposition(chapterUrn = urn, listChapter = listOf(mainChapter))) + } + + URN_WITH_COMSCORE -> { + val mainChapter = Chapter( + urn = urn, + title = "Content with Comscore analytics", + listResource = listOf(createResource(Resource.Type.HLS)), + imageUrl = DUMMY_IMAGE_URL, + listSegment = null, + mediaType = MediaType.VIDEO, + // None empty labels + comScoreAnalyticsLabels = mutableMapOf("key1" to "data"), + ) + Result.success(MediaComposition(chapterUrn = urn, listChapter = listOf(mainChapter))) + } + + URN_WITH_COMMANDERS_ACT -> { + val mainChapter = Chapter( + urn = urn, + title = "Content with CommandersAct analytics", + listResource = listOf(createResource(Resource.Type.HLS)), + imageUrl = DUMMY_IMAGE_URL, + listSegment = null, + mediaType = MediaType.AUDIO, + analyticsLabels = mutableMapOf("key1" to "data"), + ) + Result.success(MediaComposition(chapterUrn = urn, listChapter = listOf(mainChapter))) + } + else -> Result.failure(IllegalArgumentException("No resource found")) } } @@ -241,6 +320,9 @@ class SRGAssetLoaderTest { const val URN_SEGMENT_BLOCK_REASON = "urn:rts:video:segment_block_reason" const val URN_TIME_INTERVALS = "urn:rts:video:time_intervals" const val DUMMY_IMAGE_URL = "https://image.png" + const val URN_WITH_ANALYTICS = "urn:rts:video:analytics" + const val URN_WITH_COMSCORE = "urn:rts:video:comscore" + const val URN_WITH_COMMANDERS_ACT = "urn:rts:audio:commandersact" val SEGMENT_1 = Segment( urn = "s1", title = "title", diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepositoryTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepositoryTest.kt deleted file mode 100644 index b539cd19e..000000000 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepositoryTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.core.business.tracker - -import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct -import ch.srgssr.pillarbox.core.business.tracker.commandersact.CommandersActTracker -import ch.srgssr.pillarbox.core.business.tracker.comscore.ComScoreTracker -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository -import io.mockk.mockk -import io.mockk.verifySequence -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.test.Test - -class DefaultMediaItemTrackerRepositoryTest { - @Test - fun `DefaultMediaItemTrackerRepository registers some default factories`() { - val trackerRepository = mockk(relaxed = true) - val commandersAct = mockk() - - DefaultMediaItemTrackerRepository( - trackerRepository = trackerRepository, - commandersAct = commandersAct, - coroutineContext = EmptyCoroutineContext, - ) - - verifySequence { - trackerRepository.registerFactory(SRGEventLoggerTracker::class.java, any(SRGEventLoggerTracker.Factory::class)) - trackerRepository.registerFactory(ComScoreTracker::class.java, any(ComScoreTracker.Factory::class)) - trackerRepository.registerFactory(CommandersActTracker::class.java, any(CommandersActTracker.Factory::class)) - } - } - - @Test - fun `DefaultMediaItemTrackerRepository registers some default factories without CommandersAct`() { - val trackerRepository = mockk(relaxed = true) - - DefaultMediaItemTrackerRepository( - trackerRepository = trackerRepository, - commandersAct = null, - coroutineContext = EmptyCoroutineContext, - ) - - verifySequence { - trackerRepository.registerFactory(SRGEventLoggerTracker::class.java, any(SRGEventLoggerTracker.Factory::class)) - trackerRepository.registerFactory(ComScoreTracker::class.java, any(ComScoreTracker.Factory::class)) - trackerRepository.registerFactory(CommandersActTracker::class.java, any(CommandersActTracker.Factory::class)) - } - } -} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/SRGEventLoggerTrackerTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/SRGEventLoggerTrackerTest.kt index e4fc0cf37..e67803a7e 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/SRGEventLoggerTrackerTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/SRGEventLoggerTrackerTest.kt @@ -6,7 +6,6 @@ package ch.srgssr.pillarbox.core.business.tracker import androidx.media3.exoplayer.ExoPlayer import androidx.test.ext.junit.runners.AndroidJUnit4 -import ch.srgssr.pillarbox.player.tracker.MediaItemTracker import io.mockk.mockk import io.mockk.verifySequence import org.junit.runner.RunWith @@ -19,8 +18,8 @@ class SRGEventLoggerTrackerTest { val player = mockk(relaxed = true) val eventLogger = SRGEventLoggerTracker.Factory().create() - eventLogger.start(player, initialData = null) - eventLogger.stop(player, MediaItemTracker.StopReason.EoF, positionMs = 0L) + eventLogger.start(player, Unit) + eventLogger.stop(player) verifySequence { player.addAnalyticsListener(any()) diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreamingTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreamingTest.kt index 26c36812c..df552c5cd 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreamingTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreamingTest.kt @@ -7,6 +7,7 @@ package ch.srgssr.pillarbox.core.business.tracker.commandersact import android.content.Context import androidx.annotation.FloatRange import androidx.annotation.IntRange +import androidx.media3.common.AdPlaybackState import androidx.media3.common.C import androidx.media3.common.DeviceInfo import androidx.media3.common.Format @@ -14,6 +15,7 @@ import androidx.media3.common.MimeTypes import androidx.media3.common.TrackGroup import androidx.media3.common.Tracks import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.test.utils.FakeTimeline import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct @@ -265,6 +267,18 @@ class CommandersActStreamingTest { return mockk { val player = this val looper = ApplicationProvider.getApplicationContext().mainLooper + val timelineWindowDefinition = FakeTimeline.TimelineWindowDefinition( + 1, + Any(), + true, + isCurrentMediaItemLive, + isCurrentMediaItemLive, + false, + duration * 1000L, + C.TIME_UNSET, + 0, + AdPlaybackState.NONE + ) every { player.playWhenReady } returns true every { player.isPlaying } returns isPlaying @@ -276,6 +290,8 @@ class CommandersActStreamingTest { every { player.duration } returns duration every { player.currentTracks } returns currentTracks every { player.applicationLooper } returns looper + every { player.currentTimeline } returns FakeTimeline(timelineWindowDefinition) + every { player.currentMediaItemIndex } returns 0 } } diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt index e54e8e2da..553a0c896 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt @@ -22,19 +22,19 @@ import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType.Seek import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType.Stop import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType.Uptime import ch.srgssr.pillarbox.analytics.commandersact.TCMediaEvent -import ch.srgssr.pillarbox.core.business.DefaultPillarbox import ch.srgssr.pillarbox.core.business.SRGMediaItemBuilder -import ch.srgssr.pillarbox.core.business.tracker.DefaultMediaItemTrackerRepository -import ch.srgssr.pillarbox.core.business.tracker.comscore.ComScoreTracker +import ch.srgssr.pillarbox.core.business.source.SRGAssetLoader import ch.srgssr.pillarbox.core.business.utils.LocalMediaCompositionWithFallbackService +import ch.srgssr.pillarbox.player.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import ch.srgssr.pillarbox.player.test.utils.TestPillarboxRunHelper -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository import io.mockk.Called import io.mockk.clearAllMocks import io.mockk.confirmVerified import io.mockk.mockk import io.mockk.slot import io.mockk.verify +import io.mockk.verifyAll import io.mockk.verifyOrder import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestDispatcher @@ -71,20 +71,18 @@ class CommandersActTrackerIntegrationTest { testDispatcher = UnconfinedTestDispatcher() val context = ApplicationProvider.getApplicationContext() - val mediaItemTrackerRepository = DefaultMediaItemTrackerRepository( - trackerRepository = MediaItemTrackerRepository(), + val mediaCompositionWithFallbackService = LocalMediaCompositionWithFallbackService(context) + val assetLoader = SRGAssetLoader( + context, + mediaCompositionService = mediaCompositionWithFallbackService, commandersAct = commandersAct, - coroutineContext = testDispatcher, + coroutineContext = testDispatcher ) - mediaItemTrackerRepository.registerFactory(ComScoreTracker::class.java) { - mockk(relaxed = true) - } - - val mediaCompositionWithFallbackService = LocalMediaCompositionWithFallbackService(context) - player = DefaultPillarbox( + player = PillarboxExoPlayer( context = context, - mediaItemTrackerRepository = mediaItemTrackerRepository, - mediaCompositionService = mediaCompositionWithFallbackService, + mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { + addAssetLoader(assetLoader) + }, clock = clock, // Use other CoroutineContext to avoid infinite loop because Heartbeat is also running in Pillarbox. coroutineContext = EmptyCoroutineContext, @@ -849,6 +847,8 @@ class CommandersActTrackerIntegrationTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) + verifyOrder { commandersAct.enableRunningInBackground() commandersAct.sendTcMediaEvent(capture(tcMediaEvents)) @@ -863,6 +863,85 @@ class CommandersActTrackerIntegrationTest { assertTrue(tcMediaEvents.all { it.sourceId == null }) } + @Test + fun `repeat current item reset the session`() { + val tcMediaEvents = mutableListOf() + val firstMediaId = URN_VOD_SHORT + player.apply { + setMediaItem(SRGMediaItemBuilder(firstMediaId).build()) + player.repeatMode = Player.REPEAT_MODE_ONE + prepare() + play() + } + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + TestPlayerRunHelper.runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION) + player.stop() // Stop player to stop the auto-repeat mode + + // Wait on item transition. + // Stop otherwise goes crazy. + verifyAll { + commandersAct.enableRunningInBackground() + commandersAct.sendTcMediaEvent(capture(tcMediaEvents)) + } + confirmVerified(commandersAct) + + assertEquals(listOf(Play, Eof, Play, Stop), tcMediaEvents.map { it.eventType }) + } + + @Test + fun `auto transition to next item EoF between items`() { + val tcMediaEvents = mutableListOf() + val firstMediaId = URN_VOD_SHORT + val secondMediaId = URN_VOD_SHORT + player.apply { + addMediaItem(SRGMediaItemBuilder(firstMediaId).build()) + addMediaItem(SRGMediaItemBuilder(secondMediaId).build()) + prepare() + play() + } + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) + + verifyAll { + commandersAct.enableRunningInBackground() + commandersAct.sendTcMediaEvent(capture(tcMediaEvents)) + } + confirmVerified(commandersAct) + + assertEquals(listOf(Play, Eof, Play, Eof), tcMediaEvents.map { it.eventType }) + } + + @Test + fun `one MediaItem reach eof then seek back start a new session`() { + val tcMediaEvents = mutableListOf() + val mediaItem = SRGMediaItemBuilder(URN_VOD_SHORT).build() + player.apply { + setMediaItem(mediaItem) + prepare() + play() + } + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + player.seekBack() + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) + + verifyAll { + commandersAct.enableRunningInBackground() + commandersAct.sendTcMediaEvent(capture(tcMediaEvents)) + } + confirmVerified(commandersAct) + + assertEquals(listOf(Play, Eof, Play), tcMediaEvents.map { it.eventType }) + } + private companion object { private const val URL = "https://rts-vod-amd.akamaized.net/ww/14970442/7510ee63-05a4-3d48-8d26-1f1b3a82f6be/master.m3u8" private const val URN_AUDIO = "urn:rts:audio:13598743" diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.kt deleted file mode 100644 index 657c7adff..000000000 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.core.business.tracker.commandersact - -import androidx.media3.common.C -import androidx.media3.exoplayer.ExoPlayer -import androidx.test.ext.junit.runners.AndroidJUnit4 -import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct -import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType -import ch.srgssr.pillarbox.analytics.commandersact.TCMediaEvent -import ch.srgssr.pillarbox.player.tracker.MediaItemTracker -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify -import org.junit.runner.RunWith -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNull -import kotlin.test.assertTrue -import kotlin.time.Duration.Companion.seconds - -@RunWith(AndroidJUnit4::class) -class CommandersActTrackerTest { - @Test(expected = IllegalArgumentException::class) - fun `start() requires a non-null initial data`() { - val player = mockk(relaxed = true) - val commandersActs = mockk(relaxed = true) - val commandersActTracker = CommandersActTracker(commandersActs, EmptyCoroutineContext) - - commandersActTracker.start( - player = player, - initialData = null, - ) - } - - @Test(expected = IllegalArgumentException::class) - fun `start() requires an instance of CommandersActTracker#Data instance for the initial data`() { - val player = mockk(relaxed = true) - val commandersActs = mockk(relaxed = true) - val commandersActTracker = CommandersActTracker(commandersActs, EmptyCoroutineContext) - - commandersActTracker.start( - player = player, - initialData = "My data", - ) - } - - @Test - fun `commanders act tracker`() { - val player = mockk(relaxed = true) { - every { isPlaying } returns true - } - val commandersAct = mockk(relaxed = true) - val commandersActTracker = CommandersActTracker(commandersAct, EmptyCoroutineContext) - val commandersActStreamingSlot = slot() - val tcMediaEventSlots = mutableListOf() - - commandersActTracker.start( - player = player, - initialData = CommandersActTracker.Data(emptyMap()), - ) - - verify { - commandersAct.enableRunningInBackground() - commandersAct.sendTcMediaEvent(any()) - - player.isPlaying - player.addAnalyticsListener(capture(commandersActStreamingSlot)) - } - - assertTrue(commandersActStreamingSlot.isCaptured) - - val commandersActStreaming = commandersActStreamingSlot.captured - commandersActTracker.stop( - player = player, - reason = MediaItemTracker.StopReason.EoF, - positionMs = 30.seconds.inWholeMilliseconds, - ) - - verify { - player.removeAnalyticsListener(commandersActStreaming) - commandersAct.sendTcMediaEvent(capture(tcMediaEventSlots)) - } - - val tcMediaEvent = tcMediaEventSlots.last() - assertEquals(MediaEventType.Eof, tcMediaEvent.eventType) - assertEquals(commandersActStreaming.currentData.assets, tcMediaEvent.assets) - assertEquals(commandersActStreaming.currentData.sourceId, tcMediaEvent.sourceId) - assertFalse(tcMediaEvent.isSubtitlesOn) - assertNull(tcMediaEvent.subtitleSelectionLanguage) - assertEquals(C.LANGUAGE_UNDETERMINED, tcMediaEvent.audioTrackLanguage) - assertNull(tcMediaEvent.timeShift) - assertEquals(0f, tcMediaEvent.deviceVolume) - assertEquals(30.seconds, tcMediaEvent.mediaPosition) - } -} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerIntegrationTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerIntegrationTest.kt index 245e6cd72..629c4b432 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerIntegrationTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerIntegrationTest.kt @@ -17,11 +17,16 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import ch.srgssr.pillarbox.analytics.BuildConfig -import ch.srgssr.pillarbox.core.business.DefaultPillarbox import ch.srgssr.pillarbox.core.business.SRGMediaItemBuilder -import ch.srgssr.pillarbox.core.business.tracker.DefaultMediaItemTrackerRepository +import ch.srgssr.pillarbox.core.business.source.SRGAssetLoader import ch.srgssr.pillarbox.core.business.utils.LocalMediaCompositionWithFallbackService -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository +import ch.srgssr.pillarbox.player.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.asset.Asset +import ch.srgssr.pillarbox.player.asset.AssetLoader +import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory +import ch.srgssr.pillarbox.player.tracker.FactoryData +import ch.srgssr.pillarbox.player.tracker.MediaItemTracker +import ch.srgssr.pillarbox.player.tracker.MutableMediaItemTrackerData import com.comscore.streaming.AssetMetadata import com.comscore.streaming.StreamingAnalytics import io.mockk.Called @@ -53,21 +58,32 @@ class ComScoreTrackerIntegrationTest { clock = FakeClock(true) streamingAnalytics = mockk(relaxed = true) - val mediaItemTrackerRepository = DefaultMediaItemTrackerRepository( - trackerRepository = MediaItemTrackerRepository(), - commandersAct = null, - coroutineContext = EmptyCoroutineContext, - ) - mediaItemTrackerRepository.registerFactory(ComScoreTracker::class.java) { + val comScoreFactory = MediaItemTracker.Factory { ComScoreTracker(streamingAnalytics) } val context = ApplicationProvider.getApplicationContext() val mediaCompositionWithFallbackService = LocalMediaCompositionWithFallbackService(context) - - player = DefaultPillarbox( + val mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { + val srgAssetLoader = SRGAssetLoader( + context = context, + mediaCompositionService = mediaCompositionWithFallbackService + ) + addAssetLoader(object : AssetLoader(srgAssetLoader.mediaSourceFactory) { + override fun canLoadAsset(mediaItem: MediaItem): Boolean { + return srgAssetLoader.canLoadAsset(mediaItem) + } + + override suspend fun loadAsset(mediaItem: MediaItem): Asset { + val asset = srgAssetLoader.loadAsset(mediaItem) + val mediaItemTracker = MutableMediaItemTrackerData() + mediaItemTracker["FakeComScore"] = FactoryData(comScoreFactory, ComScoreTracker.Data(emptyMap())) + return asset.copy(trackersData = mediaItemTracker.toMediaItemTrackerData()) + } + }) + } + player = PillarboxExoPlayer( context = ApplicationProvider.getApplicationContext(), - mediaItemTrackerRepository = mediaItemTrackerRepository, - mediaCompositionService = mediaCompositionWithFallbackService, + mediaSourceFactory = mediaSourceFactory, clock = clock, coroutineContext = EmptyCoroutineContext, ) @@ -120,17 +136,6 @@ class ComScoreTrackerIntegrationTest { confirmVerified(streamingAnalytics) } - @Test - fun `audio URN don't send any analytics`() { - player.setMediaItem(SRGMediaItemBuilder(URN_AUDIO).build()) - player.prepare() - player.playWhenReady = true - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - - verify { streamingAnalytics wasNot Called } - } - @Test fun `URL don't send any analytics`() { player.setMediaItem(MediaItem.fromUri(URL)) @@ -545,6 +550,30 @@ class ComScoreTrackerIntegrationTest { confirmVerified(streamingAnalytics) } + @Test + fun `player prepared, playing and released`() { + player.setMediaItem(SRGMediaItemBuilder(URN_NOT_LIVE_VIDEO).build()) + player.prepare() + player.playWhenReady = true + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + clock.advanceTime(2.minutes.inWholeMilliseconds) + player.release() + + verifyOrder { + verifyPlayerInformation() + verifyCreatePlaybackSession() + verifyMetadata() + verifyPlaybackRate(playbackRate = 1f) + verifyBufferEvents() + verifySeekEvent(0L) + verifyPlayEvent() + verifyEndEvent() + } + confirmVerified(streamingAnalytics) + } + @Test fun `not live - player prepared, playing and seeking`() { player.setMediaItem(SRGMediaItemBuilder(URN_NOT_LIVE_VIDEO).build()) @@ -669,7 +698,6 @@ class ComScoreTrackerIntegrationTest { private companion object { private const val URL = "https://rts-vod-amd.akamaized.net/ww/14970442/7510ee63-05a4-3d48-8d26-1f1b3a82f6be/master.m3u8" - private const val URN_AUDIO = "urn:rts:audio:13598743" private const val URN_LIVE_DVR_VIDEO = LocalMediaCompositionWithFallbackService.URN_LIVE_DVR_VIDEO private const val URN_NOT_LIVE_VIDEO = "urn:rsi:video:15916771" } diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerTest.kt index 46b5a1106..b6f503f16 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerTest.kt @@ -9,7 +9,6 @@ import androidx.media3.common.util.Size import androidx.media3.exoplayer.ExoPlayer import androidx.test.ext.junit.runners.AndroidJUnit4 import ch.srgssr.pillarbox.analytics.BuildConfig -import ch.srgssr.pillarbox.player.tracker.MediaItemTracker import com.comscore.Analytics import com.comscore.streaming.StreamingAnalytics import io.mockk.confirmVerified @@ -48,22 +47,6 @@ class ComScoreTrackerTest { confirmVerified(streamingAnalytics) } - @Test(expected = IllegalArgumentException::class) - fun `start() require non null initial data`() { - val streamingAnalytics: StreamingAnalytics = mockk(relaxed = true) - val tracker = ComScoreTracker(streamingAnalytics = streamingAnalytics) - val player = mockk(relaxed = true) - tracker.start(player = player, null) - } - - @Test(expected = IllegalArgumentException::class) - fun `start() require an instance of ComScoreTracker#Data for initial data`() { - val streamingAnalytics: StreamingAnalytics = mockk(relaxed = true) - val tracker = ComScoreTracker(streamingAnalytics = streamingAnalytics) - val player = mockk(relaxed = true) - tracker.start(player = player, initialData = "data") - } - @Test fun `start() does not call notify play or buffer start when player can't play`() { val streamingAnalytics: StreamingAnalytics = mockk(relaxed = true) @@ -73,7 +56,7 @@ class ComScoreTrackerTest { every { player.surfaceSize } returns Size(100, 200) every { player.playbackState } returns Player.STATE_IDLE val assets = mapOf("value1" to "key1") - tracker.start(player = player, initialData = ComScoreTracker.Data(assets = assets)) + tracker.start(player = player, data = ComScoreTracker.Data(assets = assets)) verify(exactly = 1) { streamingAnalytics.setMetadata(any()) @@ -93,7 +76,7 @@ class ComScoreTrackerTest { every { player.isPlaying } returns false every { player.surfaceSize } returns Size(130, 200) every { player.playbackState } returns Player.STATE_BUFFERING - tracker.start(player = player, initialData = ComScoreTracker.Data(assets = mapOf("value1" to "key1"))) + tracker.start(player = player, data = ComScoreTracker.Data(assets = mapOf("value1" to "key1"))) verify(exactly = 1) { streamingAnalytics.notifyBufferStart() @@ -108,7 +91,7 @@ class ComScoreTrackerTest { every { player.isPlaying } returns true every { player.surfaceSize } returns Size(300, 200) every { player.playbackState } returns Player.STATE_READY - tracker.start(player = player, initialData = ComScoreTracker.Data(assets = mapOf("value1" to "key1"))) + tracker.start(player = player, data = ComScoreTracker.Data(assets = mapOf("value1" to "key1"))) verify(exactly = 1) { streamingAnalytics.notifyPlay() @@ -123,7 +106,7 @@ class ComScoreTrackerTest { every { player.isPlaying } returns true every { player.surfaceSize } returns Size.ZERO every { player.playbackState } returns Player.STATE_READY - tracker.start(player = player, initialData = ComScoreTracker.Data(assets = mapOf("value1" to "key1"))) + tracker.start(player = player, data = ComScoreTracker.Data(assets = mapOf("value1" to "key1"))) verify(exactly = 0) { streamingAnalytics.notifyPlay() @@ -138,8 +121,8 @@ class ComScoreTrackerTest { every { player.isPlaying } returns true every { player.surfaceSize } returns Size.ZERO every { player.playbackState } returns Player.STATE_READY - tracker.stop(player = player, MediaItemTracker.StopReason.EoF, 500) - tracker.stop(player = player, MediaItemTracker.StopReason.Stop, 500) + tracker.stop(player = player) + tracker.stop(player = player) verify(exactly = 2) { streamingAnalytics.notifyEnd() 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 607e1aa61..d2b8b7434 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 @@ -10,7 +10,6 @@ import ch.srg.dataProvider.integrationlayer.dependencies.modules.OkHttpModule import ch.srgssr.dataprovider.paging.DataProviderPaging import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost import ch.srgssr.pillarbox.core.business.source.SRGAssetLoader -import ch.srgssr.pillarbox.core.business.tracker.DefaultMediaItemTrackerRepository import ch.srgssr.pillarbox.demo.shared.source.BlockedTimeRangeAssetLoader import ch.srgssr.pillarbox.demo.shared.source.CustomAssetLoader import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.ILRepository @@ -34,7 +33,6 @@ object PlayerModule { addAssetLoader(CustomAssetLoader(context)) addAssetLoader(BlockedTimeRangeAssetLoader(context)) }, - mediaItemTrackerProvider = DefaultMediaItemTrackerRepository() ) } 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 ad026bff4..5dc42bffd 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 @@ -29,6 +29,8 @@ import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange import ch.srgssr.pillarbox.player.asset.timeRange.Chapter import ch.srgssr.pillarbox.player.asset.timeRange.Credit import ch.srgssr.pillarbox.player.asset.timeRange.TimeRange +import ch.srgssr.pillarbox.player.extension.getBlockedTimeRangeOrNull +import ch.srgssr.pillarbox.player.extension.getMediaItemTrackerDataOrNull import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed import ch.srgssr.pillarbox.player.extension.setPreferredAudioRoleFlagsToAccessibilityManagerSettings import ch.srgssr.pillarbox.player.extension.setSeekIncrements @@ -41,9 +43,7 @@ import ch.srgssr.pillarbox.player.network.PillarboxHttpClient import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import ch.srgssr.pillarbox.player.tracker.AnalyticsMediaItemTracker import ch.srgssr.pillarbox.player.tracker.BlockedTimeRangeTracker -import ch.srgssr.pillarbox.player.tracker.CurrentMediaItemPillarboxDataTracker -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository +import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData import ch.srgssr.pillarbox.player.tracker.PillarboxMediaMetaDataTracker import ch.srgssr.pillarbox.player.utils.PillarboxEventLogger import kotlinx.coroutines.CoroutineScope @@ -61,7 +61,6 @@ import kotlin.time.Duration.Companion.milliseconds * @param context The context. * @param coroutineContext The [CoroutineContext]. * @param exoPlayer The underlying player. - * @param mediaItemTrackerProvider The [MediaItemTrackerProvider]. * @param analyticsCollector The [PillarboxAnalyticsCollector]. * @param metricsCollector The [MetricsCollector]. * @param monitoringMessageHandler The class to handle each Monitoring message. @@ -70,7 +69,6 @@ class PillarboxExoPlayer internal constructor( context: Context, coroutineContext: CoroutineContext, private val exoPlayer: ExoPlayer, - mediaItemTrackerProvider: MediaItemTrackerProvider, analyticsCollector: PillarboxAnalyticsCollector, private val metricsCollector: MetricsCollector = MetricsCollector(), monitoringMessageHandler: MonitoringMessageHandler, @@ -78,8 +76,7 @@ class PillarboxExoPlayer internal constructor( private val listeners = ListenerSet(applicationLooper, clock) { listener, flags -> listener.onEvents(this, Player.Events(flags)) } - private val itemPillarboxDataTracker = CurrentMediaItemPillarboxDataTracker(this) - private val analyticsTracker = AnalyticsMediaItemTracker(this, mediaItemTrackerProvider) + private val analyticsTracker = AnalyticsMediaItemTracker(this) internal val sessionManager = PlaybackSessionManager() private val window = Window() @@ -133,8 +130,6 @@ class PillarboxExoPlayer internal constructor( blockedTimeRangeTracker.setPlayer(this) addListener(analyticsCollector) exoPlayer.addListener(ComponentListener()) - itemPillarboxDataTracker.addCallback(blockedTimeRangeTracker) - itemPillarboxDataTracker.addCallback(analyticsTracker) if (BuildConfig.DEBUG) { addAnalyticsListener(PillarboxEventLogger()) } @@ -144,7 +139,6 @@ class PillarboxExoPlayer internal constructor( context: Context, mediaSourceFactory: PillarboxMediaSourceFactory = PillarboxMediaSourceFactory(context), loadControl: LoadControl = PillarboxLoadControl(), - mediaItemTrackerProvider: MediaItemTrackerProvider = MediaItemTrackerRepository(), seekIncrement: SeekIncrement = SeekIncrement(), maxSeekToPreviousPosition: Duration = DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION, coroutineContext: CoroutineContext = Dispatchers.Default, @@ -161,7 +155,6 @@ class PillarboxExoPlayer internal constructor( context = context, mediaSourceFactory = mediaSourceFactory, loadControl = loadControl, - mediaItemTrackerProvider = mediaItemTrackerProvider, seekIncrement = seekIncrement, maxSeekToPreviousPosition = maxSeekToPreviousPosition, clock = Clock.DEFAULT, @@ -174,7 +167,6 @@ class PillarboxExoPlayer internal constructor( context: Context, mediaSourceFactory: PillarboxMediaSourceFactory = PillarboxMediaSourceFactory(context), loadControl: LoadControl = PillarboxLoadControl(), - mediaItemTrackerProvider: MediaItemTrackerProvider = MediaItemTrackerRepository(), seekIncrement: SeekIncrement = SeekIncrement(), maxSeekToPreviousPosition: Duration = DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION, clock: Clock, @@ -209,7 +201,6 @@ class PillarboxExoPlayer internal constructor( .setAnalyticsCollector(analyticsCollector) .setDeviceVolumeControlEnabled(true) // allow player to control device volume .build(), - mediaItemTrackerProvider = mediaItemTrackerProvider, analyticsCollector = analyticsCollector, metricsCollector = metricsCollector, monitoringMessageHandler = monitoringMessageHandler, @@ -341,11 +332,25 @@ class PillarboxExoPlayer internal constructor( */ override fun release() { clearSeeking() - mediaMetadataTracker.release() - blockedTimeRangeTracker.release() exoPlayer.release() listeners.release() - itemPillarboxDataTracker.release() + mediaMetadataTracker.release() + blockedTimeRangeTracker.release() + analyticsTracker.release() + } + + /** + * @return [MediaItemTrackerData] if it exists, `null` otherwise + */ + fun getMediaItemTrackerDataOrNull(): MediaItemTrackerData? { + return currentTracks.getMediaItemTrackerDataOrNull() + } + + /** + * @return a list of [BlockedTimeRange] if it exists, `null` otherwise + */ + fun getBlockedTimeRangeOrNull(): List? { + return currentTracks.getBlockedTimeRangeOrNull() } private fun notifyTimeRangeChanged(timeRange: TimeRange?) { diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/Asset.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/Asset.kt index c66fec305..c4adb07ae 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/Asset.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/Asset.kt @@ -8,18 +8,19 @@ import androidx.media3.common.MediaMetadata import androidx.media3.exoplayer.source.MediaSource import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData +import ch.srgssr.pillarbox.player.tracker.MutableMediaItemTrackerData /** * Assets * * @property mediaSource The [MediaSource] used by the player to play something. - * @property trackersData The [MediaItemTrackerData] to set to the [PillarboxData]. + * @property trackersData The [MediaItemTrackerData]. * @property mediaMetadata The [MediaMetadata] to set to the player media item. * @property blockedTimeRanges The [BlockedTimeRange] list. */ data class Asset( val mediaSource: MediaSource, - val trackersData: MediaItemTrackerData = MediaItemTrackerData.EMPTY, + val trackersData: MediaItemTrackerData = MutableMediaItemTrackerData.EMPTY.toMediaItemTrackerData(), val mediaMetadata: MediaMetadata = MediaMetadata.EMPTY, val blockedTimeRanges: List = emptyList(), ) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/PillarboxData.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/PillarboxData.kt deleted file mode 100644 index 6070250ed..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/PillarboxData.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.asset - -import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData - -/** - * Pillarbox data - * - * @property trackersData The [MediaItemTrackerData]. - * @property blockedTimeRanges The [BlockedTimeRange] list. - */ -data class PillarboxData( - val trackersData: MediaItemTrackerData = MediaItemTrackerData.EMPTY, - val blockedTimeRanges: List = emptyList() -) { - @Suppress("UndocumentedPublicClass") - companion object { - /** - * Empty [PillarboxData]. - */ - val EMPTY = PillarboxData() - } -} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/UrlAssetLoader.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/UrlAssetLoader.kt index de73392f6..abfc19ad3 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/UrlAssetLoader.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/UrlAssetLoader.kt @@ -6,36 +6,15 @@ package ch.srgssr.pillarbox.player.asset import androidx.media3.common.MediaItem import androidx.media3.exoplayer.source.DefaultMediaSourceFactory -import ch.srgssr.pillarbox.player.asset.UrlAssetLoader.TrackerDataProvider -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData /** - * AssetLoader to load Asset from an stream url. + * [AssetLoader] to load an [Asset] from a stream url. * * @param defaultMediaSourceFactory The [DefaultMediaSourceFactory] to create a MediaSource for the player. */ class UrlAssetLoader( defaultMediaSourceFactory: DefaultMediaSourceFactory, ) : AssetLoader(defaultMediaSourceFactory) { - /** - * The [TrackerDataProvider] to customize tracker data. - */ - var trackerDataProvider: TrackerDataProvider = DEFAULT_TRACKER_DATA_LOADER - - /** - * Tracker data loader - * - * @constructor Create empty Tracker data loader - */ - fun interface TrackerDataProvider { - /** - * Provide Tracker Data to the [MediaItem]. - * - * @param mediaItem The input [MediaItem] of the [UrlAssetLoader.loadAsset]. - * @param trackerDataBuilder The [MediaItemTrackerData.Builder] to add tracker data. - */ - suspend fun provide(mediaItem: MediaItem, trackerDataBuilder: MediaItemTrackerData.Builder) - } override fun canLoadAsset(mediaItem: MediaItem): Boolean { return mediaItem.localConfiguration != null @@ -43,17 +22,9 @@ class UrlAssetLoader( override suspend fun loadAsset(mediaItem: MediaItem): Asset { val mediaSource = mediaSourceFactory.createMediaSource(mediaItem) - val trackerData = MediaItemTrackerData.Builder().apply { - trackerDataProvider.provide(mediaItem, this) - }.build() return Asset( mediaSource = mediaSource, mediaMetadata = mediaItem.mediaMetadata, - trackersData = trackerData, ) } - - private companion object { - private val DEFAULT_TRACKER_DATA_LOADER = TrackerDataProvider { _, _ -> } - } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Tracks.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Tracks.kt index 3f5d73c66..3f112ea37 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Tracks.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Tracks.kt @@ -10,8 +10,9 @@ import androidx.media3.common.C.TrackType import androidx.media3.common.Format import androidx.media3.common.TrackGroup import androidx.media3.common.Tracks -import ch.srgssr.pillarbox.player.asset.PillarboxData +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange import ch.srgssr.pillarbox.player.source.PillarboxMediaSource +import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData /** * Text tracks. @@ -98,10 +99,20 @@ internal fun Tracks.Group.filterBy(predicate: (Tracks.Group, Int) -> Boolean): T } /** - * @return [PillarboxData] if it exists, `null` otherwise + * @return [MediaItemTrackerData] if it exists, `null` otherwise */ -fun Tracks.getPillarboxDataOrNull(): PillarboxData? { +fun Tracks.getMediaItemTrackerDataOrNull(): MediaItemTrackerData? { return groups.firstOrNull { - it.type == PillarboxMediaSource.PILLARBOX_TRACK_TYPE - }?.getTrackFormat(0)?.customData as PillarboxData? + it.type == PillarboxMediaSource.TRACK_TYPE_PILLARBOX_TRACKERS + }?.getTrackFormat(0)?.customData as? MediaItemTrackerData +} + +/** + * @return a list of [BlockedTimeRange] if it exists, `null` otherwise + */ +@Suppress("UNCHECKED_CAST") +fun Tracks.getBlockedTimeRangeOrNull(): List? { + return groups.firstOrNull { + it.type == PillarboxMediaSource.TRACK_TYPE_PILLARBOX_BLOCKED + }?.getTrackFormat(0)?.customData as? List } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaPeriod.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaPeriod.kt index d50796dac..e4bed7ede 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaPeriod.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaPeriod.kt @@ -9,34 +9,51 @@ import androidx.media3.common.TrackGroup import androidx.media3.exoplayer.source.MediaPeriod import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.TrackGroupArray -import ch.srgssr.pillarbox.player.asset.PillarboxData -import ch.srgssr.pillarbox.player.source.PillarboxMediaSource.Companion.PILLARBOX_TRACK_MIME_TYPE +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange +import ch.srgssr.pillarbox.player.source.PillarboxMediaSource.Companion.PILLARBOX_BLOCKED_MIME_TYPE +import ch.srgssr.pillarbox.player.source.PillarboxMediaSource.Companion.PILLARBOX_TRACKERS_MIME_TYPE +import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData internal class PillarboxMediaPeriod( private val mediaPeriod: MediaPeriod, - pillarboxData: PillarboxData, + mediaItemTrackerData: MediaItemTrackerData, + blockedTimeRanges: List, ) : MediaPeriod by mediaPeriod { - private val pillarboxGroup: TrackGroup = TrackGroup( - "Pillarbox", - Format.Builder() - .setId("PillarboxData") - .setSampleMimeType(PILLARBOX_TRACK_MIME_TYPE) - .setCustomData(pillarboxData) - .build(), - ) + private val pillarboxTracks = buildList { + if (mediaItemTrackerData.isNotEmpty()) { + add( + TrackGroup( + "Pillarbox-Trackers", + Format.Builder() + .setId("TrackerData:0") + .setSampleMimeType(PILLARBOX_TRACKERS_MIME_TYPE) + .setCustomData(mediaItemTrackerData) + .build(), + ) + ) + } + if (blockedTimeRanges.isNotEmpty()) { + add( + TrackGroup( + "Pillarbox-BlockedTimeRanges", + Format.Builder() + .setSampleMimeType(PILLARBOX_BLOCKED_MIME_TYPE) + .setId("BlockedTimeRanges") + .setCustomData(blockedTimeRanges) + .build(), + ) + ) + } + }.toTypedArray() @Suppress("SpreadOperator") override fun getTrackGroups(): TrackGroupArray { - val trackGroup = mediaPeriod.trackGroups - val trackGroups = Array(trackGroup.length + 1) { - if (it < trackGroup.length) { - trackGroup.get(it) - } else { - pillarboxGroup - } + val trackGroups = mediaPeriod.trackGroups + val trackGroupArray = Array(trackGroups.length) { + trackGroups.get(it) } // Don't know how to do it, without SpreadOperator! - return TrackGroupArray(*trackGroups) + return TrackGroupArray(*trackGroupArray, *pillarboxTracks) } fun release(mediaSource: MediaSource) { diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaSource.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaSource.kt index a21dcda39..bced36b6b 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaSource.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaSource.kt @@ -21,7 +21,9 @@ import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.TimelineWithUpdatedMediaItem import androidx.media3.exoplayer.upstream.Allocator import ch.srgssr.pillarbox.player.asset.AssetLoader -import ch.srgssr.pillarbox.player.asset.PillarboxData +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange +import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData +import ch.srgssr.pillarbox.player.tracker.MutableMediaItemTrackerData import ch.srgssr.pillarbox.player.utils.DebugLogger import kotlinx.coroutines.runBlocking import java.io.IOException @@ -48,7 +50,8 @@ class PillarboxMediaSource internal constructor( private val eventDispatcher by lazy { createEventDispatcher(null) } private var loadTaskId = 0L private var timeMarkLoadStart: TimeMark? = null - private var pillarboxData: PillarboxData = PillarboxData.EMPTY + private var mediaItemTrackerData: MediaItemTrackerData = MutableMediaItemTrackerData.EMPTY.toMediaItemTrackerData() + private var blockedTimeRanges: List = emptyList() @Suppress("TooGenericExceptionCaught") override fun prepareSourceInternal(mediaTransferListener: TransferListener?) { @@ -63,10 +66,8 @@ class PillarboxMediaSource internal constructor( dispatchLoadCompleted() DebugLogger.debug(TAG, "Asset(${mediaItem.localConfiguration?.uri}) : ${asset.trackersData}") mediaSource = asset.mediaSource - pillarboxData = PillarboxData( - trackersData = asset.trackersData, - blockedTimeRanges = asset.blockedTimeRanges, - ) + mediaItemTrackerData = asset.trackersData + blockedTimeRanges = asset.blockedTimeRanges mediaItem = mediaItem.buildUpon() .setMediaMetadata(asset.mediaMetadata) .build() @@ -134,7 +135,7 @@ class PillarboxMediaSource internal constructor( startPositionUs: Long ): MediaPeriod { DebugLogger.debug(TAG, "createPeriod: $id") - return PillarboxMediaPeriod(mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs), pillarboxData = pillarboxData) + return PillarboxMediaPeriod(mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs), mediaItemTrackerData, blockedTimeRanges) } override fun releasePeriod(mediaPeriod: MediaPeriod) { @@ -216,17 +217,28 @@ class PillarboxMediaSource internal constructor( private const val TAG = "PillarboxMediaSource" /** - * [Format.sampleMimeType] used to define Pillarbox custom tracks. + * [Format.sampleMimeType] used to define Pillarbox trackers data custom tracks. */ - const val PILLARBOX_TRACK_MIME_TYPE = "${MimeTypes.BASE_TYPE_APPLICATION}/pillarbox" + internal const val PILLARBOX_TRACKERS_MIME_TYPE = "${MimeTypes.BASE_TYPE_APPLICATION}/pillarbox-trackers" /** - * [TrackGroup.type] for [Format]s with mime type [PILLARBOX_TRACK_MIME_TYPE]. + * [Format.sampleMimeType] used to define Pillarbox blocked time interval custom tracks. */ - const val PILLARBOX_TRACK_TYPE = C.DATA_TYPE_CUSTOM_BASE + 1 + internal const val PILLARBOX_BLOCKED_MIME_TYPE = "${MimeTypes.BASE_TYPE_APPLICATION}/pillarbox-blocked" + + /** + * [TrackGroup.type] for [Format]s with mime type [PILLARBOX_TRACKERS_MIME_TYPE]. + */ + const val TRACK_TYPE_PILLARBOX_TRACKERS = C.DATA_TYPE_CUSTOM_BASE + 1 + + /** + * [TrackGroup.type] for [Format]s with mime type [PILLARBOX_BLOCKED_MIME_TYPE]. + */ + const val TRACK_TYPE_PILLARBOX_BLOCKED = TRACK_TYPE_PILLARBOX_TRACKERS + 1 init { - MimeTypes.registerCustomMimeType(PILLARBOX_TRACK_MIME_TYPE, "pillarbox", PILLARBOX_TRACK_TYPE) + MimeTypes.registerCustomMimeType(PILLARBOX_TRACKERS_MIME_TYPE, "pillarbox", TRACK_TYPE_PILLARBOX_TRACKERS) + MimeTypes.registerCustomMimeType(PILLARBOX_BLOCKED_MIME_TYPE, "pillarbox", TRACK_TYPE_PILLARBOX_BLOCKED) } } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/AnalyticsMediaItemTracker.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/AnalyticsMediaItemTracker.kt index a726b7528..36c8511a8 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/AnalyticsMediaItemTracker.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/AnalyticsMediaItemTracker.kt @@ -6,150 +6,95 @@ package ch.srgssr.pillarbox.player.tracker import androidx.media3.common.MediaItem import androidx.media3.common.Player -import androidx.media3.common.Player.PositionInfo +import androidx.media3.common.Tracks import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.analytics.AnalyticsListener -import ch.srgssr.pillarbox.player.asset.PillarboxData -import ch.srgssr.pillarbox.player.extension.getPillarboxDataOrNull -import ch.srgssr.pillarbox.player.tracker.MediaItemTracker.StopReason +import ch.srgssr.pillarbox.player.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.extension.getMediaItemTrackerDataOrNull import ch.srgssr.pillarbox.player.utils.DebugLogger -import ch.srgssr.pillarbox.player.utils.StringUtil -import kotlin.time.Duration.Companion.milliseconds /** - * Custom [CurrentMediaItemPillarboxDataTracker.Callback] to manage analytics. + * Tracks [Player.getCurrentTracks] to handle [MediaItemTrackerData] changes. + * When the player is stopped (player state is `IDLE`), the `MediaPeriod` is destroyed and then `prepare()` is called, it will create a session + * by calling `start()`. * - * @param player The [Player] whose current [MediaItem] is tracked for analytics. - * @param mediaItemTrackerProvider The [MediaItemTrackerProvider] that provide new instance of [MediaItemTracker]. + * @param player The [Player] whose current [Tracks] is tracked for analytics. */ internal class AnalyticsMediaItemTracker( - private val player: ExoPlayer, - private val mediaItemTrackerProvider: MediaItemTrackerProvider, -) : CurrentMediaItemPillarboxDataTracker.Callback { - private val listener = CurrentMediaItemListener() + private val player: PillarboxExoPlayer, +) : AnalyticsListener { /** * Trackers are empty if the tracking session is stopped. */ - private var trackers = MediaItemTrackerList() - private var currentPillarboxData: PillarboxData? = null + private val trackers = mutableListOf>() + private var currentMediaItemTrackerData: MediaItemTrackerData? = null + set(value) { + if (field !== value) { + DebugLogger.info(TAG, "currentMediaItemTrackerData $field -> $value") + stopSession() + field = value + field?.let { + if (it.isNotEmpty()) { + startNewSession(it) + } + } + } + } var enabled: Boolean = true set(value) { if (field == value) { return } - field = value - if (field) { - currentPillarboxData = player.currentTracks.getPillarboxDataOrNull()?.let { - startNewSession(data = it) - it - } + currentMediaItemTrackerData = if (field) { + player.getMediaItemTrackerDataOrNull() } else { - stopSession(StopReason.Stop) + null } } - override fun onPillarboxDataChanged( - data: PillarboxData?, - ) { - DebugLogger.info(TAG, "onPillarboxDataChanged $data") - stopSession(StopReason.Stop) - player.removeAnalyticsListener(listener) - currentPillarboxData = data - data?.let { - if (it.trackersData.isNotEmpty) { - player.addAnalyticsListener(listener) - startNewSession(it) - } + init { + player.addAnalyticsListener(this) + currentMediaItemTrackerData = player.getMediaItemTrackerDataOrNull() + } + + override fun onTracksChanged(eventTime: AnalyticsListener.EventTime, tracks: Tracks) { + currentMediaItemTrackerData = tracks.getMediaItemTrackerDataOrNull() + } + + override fun onPlaybackStateChanged(eventTime: AnalyticsListener.EventTime, state: Int) { + if (state == Player.STATE_IDLE) { + release() } } - private fun stopSession( - stopReason: StopReason, - positionMs: Long = player.currentPosition, - ) { + fun release() { + currentMediaItemTrackerData = null + } + + private fun stopSession() { if (trackers.isEmpty()) return - DebugLogger.info(TAG, "Stop trackers $stopReason @${positionMs.milliseconds}") + DebugLogger.info(TAG, "Stop session") for (tracker in trackers) { - tracker.stop(player, stopReason, positionMs) + tracker.stop(player) } trackers.clear() } - private fun startNewSession(data: PillarboxData) { - if (!enabled || data.trackersData.trackers.isEmpty()) { + private fun startNewSession(data: MediaItemTrackerData) { + if (!enabled || data.isEmpty()) { return } require(trackers.isEmpty()) - DebugLogger.info(TAG, "Start new session for ${player.currentMediaItem?.prettyString()}") - - // Create each tracker for this new MediaItem - val trackers = data.trackersData.trackers - .map { trackerType -> - mediaItemTrackerProvider.getMediaItemTrackerFactory(trackerType).create() - .also { it.start(player, data.trackersData.getData(it)) } - } - - this.trackers.addAll(trackers) - } - - private inner class CurrentMediaItemListener : AnalyticsListener { - override fun onPlaybackStateChanged( - eventTime: AnalyticsListener.EventTime, - @Player.State playbackState: Int, - ) { - DebugLogger.debug( - TAG, - "onPlaybackStateChanged ${StringUtil.playerStateString(playbackState)} ${player.currentMediaItem?.prettyString()}" - ) - - when (playbackState) { - Player.STATE_ENDED -> stopSession(StopReason.EoF) - Player.STATE_IDLE -> stopSession(StopReason.Stop) - Player.STATE_READY -> { - if (trackers.isEmpty() && currentPillarboxData != null) { - startNewSession(data = currentPillarboxData!!) - } - } - - else -> Unit - } - } - - /* - * On position discontinuity handle stop session if required - */ - override fun onPositionDiscontinuity( - eventTime: AnalyticsListener.EventTime, - oldPosition: PositionInfo, - newPosition: PositionInfo, - @Player.DiscontinuityReason reason: Int, - ) { - DebugLogger.debug( - TAG, - "onPositionDiscontinuity ${StringUtil.discontinuityReasonString(reason)} ${oldPosition.mediaItem?.prettyString()}" - ) - - val oldPositionMs = oldPosition.positionMs - when (reason) { - Player.DISCONTINUITY_REASON_REMOVE -> stopSession(StopReason.Stop, oldPositionMs) - Player.DISCONTINUITY_REASON_AUTO_TRANSITION -> { - stopSession(StopReason.EoF, oldPositionMs) - if (oldPosition.mediaItemIndex == newPosition.mediaItemIndex) { - currentPillarboxData?.let { startNewSession(it) } - } - } - - else -> { - if (oldPosition.mediaItemIndex != newPosition.mediaItemIndex) { - stopSession(StopReason.Stop, oldPositionMs) - } - } + val delegates = data.map { + DelegateMediaItemTracker(it.value).apply { + this.start(player, Unit) } } + trackers.addAll(delegates) } private companion object { @@ -159,3 +104,15 @@ internal class AnalyticsMediaItemTracker( } } } + +internal class DelegateMediaItemTracker(private val factoryData: FactoryData) : MediaItemTracker { + val tracker: MediaItemTracker = factoryData.factory.create() + + override fun start(player: ExoPlayer, data: Unit) { + tracker.start(player, factoryData.data) + } + + override fun stop(player: ExoPlayer) { + tracker.stop(player) + } +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/BlockedTimeRangeTracker.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/BlockedTimeRangeTracker.kt index d9c609950..15a8c3c84 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/BlockedTimeRangeTracker.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/BlockedTimeRangeTracker.kt @@ -5,37 +5,38 @@ package ch.srgssr.pillarbox.player.tracker import androidx.media3.common.Player +import androidx.media3.common.Tracks import androidx.media3.exoplayer.PlayerMessage import ch.srgssr.pillarbox.player.PillarboxExoPlayer -import ch.srgssr.pillarbox.player.asset.PillarboxData import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange import ch.srgssr.pillarbox.player.asset.timeRange.TimeRange import ch.srgssr.pillarbox.player.asset.timeRange.firstOrNullAtPosition +import ch.srgssr.pillarbox.player.extension.getBlockedTimeRangeOrNull internal class BlockedTimeRangeTracker( private val callback: (TimeRange?) -> Unit -) : CurrentMediaItemPillarboxDataTracker.Callback, Player.Listener { +) : Player.Listener { private val playerMessages = mutableListOf() private var timeRanges: List? = null + set(value) { + if (field != value) { + clear() + field = value + field?.let { + createMessages(it) + } + } + } private lateinit var player: PillarboxExoPlayer fun setPlayer(player: PillarboxExoPlayer) { this.player = player + timeRanges = player.currentTracks.getBlockedTimeRangeOrNull() player.addListener(this) } - /* - * Called when the callback is added, and we already have a [PillarboxData]. - */ - override fun onPillarboxDataChanged(data: PillarboxData?) { - clear() - data?.let { - timeRanges = it.blockedTimeRanges - it.blockedTimeRanges.firstOrNullAtPosition(player.currentPosition)?.let { timeRange -> - callback(timeRange) - } - createMessages(it.blockedTimeRanges) - } + override fun onTracksChanged(tracks: Tracks) { + timeRanges = tracks.getBlockedTimeRangeOrNull() } override fun onEvents(player: Player, events: Player.Events) { @@ -69,10 +70,9 @@ internal class BlockedTimeRangeTracker( playerMessage.cancel() } playerMessages.clear() - timeRanges = null } fun release() { - clear() + timeRanges = null } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemPillarboxDataTracker.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemPillarboxDataTracker.kt deleted file mode 100644 index a2a4f9916..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemPillarboxDataTracker.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.tracker - -import androidx.media3.common.MediaItem -import androidx.media3.common.Player -import androidx.media3.common.Tracks -import androidx.media3.exoplayer.ExoPlayer -import ch.srgssr.pillarbox.player.asset.PillarboxData -import ch.srgssr.pillarbox.player.extension.getPillarboxDataOrNull - -/** - * - * @param player The [Player] for which the current media item's tag must be tracked. - */ -internal class CurrentMediaItemPillarboxDataTracker(private val player: ExoPlayer) { - interface Callback { - /** - * Called when the [PillarboxData] of the current media item changes. - * - * @param data The [PillarboxData] of the current [MediaItem]. Might be `null` if no [PillarboxData] is set. - */ - fun onPillarboxDataChanged( - data: PillarboxData?, - ) - } - - /** - * The callbacks managed by this tracker. - */ - private val callbacks = mutableSetOf() - private var currentPillarboxData: PillarboxData? = player.currentTracks.getPillarboxDataOrNull() - set(value) { - // Check instance instead of content, because multiple items could have the same data. - if (field !== value) { - notifyPillarboxDataChange(value) - field = value - } - } - - init { - player.addListener(CurrentMediaItemListener()) - } - - /** - * To be called when [Player.release]. - */ - fun release() { - currentPillarboxData = null - } - - /** - * Add callback will call [Callback.onPillarboxDataChanged] with the current [PillarboxData] if not `null`. - */ - fun addCallback(callback: Callback) { - callbacks.add(callback) - currentPillarboxData?.let { - callback.onPillarboxDataChanged(it) - } - } - - private fun notifyPillarboxDataChange(pillarboxData: PillarboxData?) { - callbacks.forEach { callback -> - callback.onPillarboxDataChanged(pillarboxData) - } - } - - private inner class CurrentMediaItemListener : Player.Listener { - override fun onTracksChanged(tracks: Tracks) { - currentPillarboxData = tracks.getPillarboxDataOrNull() - } - } -} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTracker.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTracker.kt index 21c34522a..16f17b47c 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTracker.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTracker.kt @@ -9,59 +9,31 @@ import androidx.media3.exoplayer.ExoPlayer /** * Media item tracker */ -interface MediaItemTracker { - - /** - * Stop reason - */ - enum class StopReason { - - /** - * When the player has been stopped, released or its current media item changes. - */ - Stop, - - /** - * When the player reaches the end of the media. - */ - EoF - } +interface MediaItemTracker { /** * Start Media tracking. * * @param player The player to track. - * @param initialData The data associated if any. + * @param data The data associated. */ - fun start(player: ExoPlayer, initialData: Any?) + fun start(player: ExoPlayer, data: T) /** * Stop Media tracking. * - * @param player The player tracked. - * @param reason To tell how the track is stopped. - * @param positionMs The player position when the tracker is stopped. - */ - fun stop(player: ExoPlayer, reason: StopReason, positionMs: Long) - - /** - * Update with data. - * - * Data may not have change. - * - * @param data The data to use with this Tracker. + * @param player The player tracked. The current player state may reflect the next item. */ - // fun update(data: Any) {} + fun stop(player: ExoPlayer) /** * Factory */ - fun interface Factory { + fun interface Factory { + /** - * Create a new instance of a [MediaItemTracker] - * - * @return a new instance. + * @return a new instance of a [MediaItemTracker] */ - fun create(): MediaItemTracker + fun create(): MediaItemTracker } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerData.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerData.kt index 1fa3231d1..0e8187f0f 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerData.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerData.kt @@ -5,107 +5,37 @@ package ch.srgssr.pillarbox.player.tracker /** - * Immutable MediaItem tracker data. + * Link between [data] and it's [factory]. + * + * @param T The factory data type. + * @property factory The [MediaItemTracker.Factory]. + * @property data The data of type T to use in [MediaItemTracker.start]. */ -class MediaItemTrackerData private constructor(private val map: Map, Any?>) { - - /** - * List of tracker class that have data. - */ - val trackers: Collection> - get() { - return map.keys - } - - /** - * Is empty - */ - val isEmpty: Boolean = map.isEmpty() - - /** - * Is not empty - */ - val isNotEmpty: Boolean = !isEmpty - - /** - * Get data for a Tracker - * - * @param T The Data class. - * @param tracker The tracker to retrieve the data. - * @return data for tracker as T if it exist. - */ - @Suppress("UNCHECKED_CAST") - fun getDataAs(tracker: MediaItemTracker): T? { - return map[tracker::class.java] as T? - } - - /** - * Get data for a tracker - * - * @param tracker The tracker to get data of. - * @return generic data if any. - */ - fun getData(tracker: MediaItemTracker): Any? { - return map[tracker::class.java] - } +class FactoryData(val factory: MediaItemTracker.Factory, val data: T) +/** + * Mutable MediaItem tracker data. + * + * @constructor Create empty Mutable media item tracker data + */ +class MutableMediaItemTrackerData : MutableMap> by mutableMapOf() { /** - * Build upon - * - * @return A builder filled with current data. + * To media item tracker data */ - fun buildUpon(): Builder = Builder(this) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as MediaItemTrackerData - - return map == other.map - } - - override fun hashCode(): Int { - return map.hashCode() - } - - override fun toString(): String { - return "MediaItemTrackerData(map=$map)" - } + fun toMediaItemTrackerData() = MediaItemTrackerData(this) @Suppress("UndocumentedPublicClass") companion object { /** - * Empty [MediaItemTrackerData]. + * Empty mutable media item tracker data. */ - val EMPTY = MediaItemTrackerData(emptyMap()) - } - - /** - * Builder - *y - * @param source set this builder with source value. - */ - class Builder(source: MediaItemTrackerData = EMPTY) { - private val map = HashMap, Any?>(source.map) - - /** - * Put data for trackerClass - * - * @param T extends [MediaItemTracker]. - * @param trackerClass The class of the [MediaItemTracker]. - * @param data The data to associated with any instance of trackerClass. - */ - fun putData(trackerClass: Class, data: Any? = null): Builder { - map[trackerClass] = data - return this - } - - /** - * Build - * - * @return a new instance of [MediaItemTrackerData] - */ - fun build(): MediaItemTrackerData = MediaItemTrackerData(map.toMap()) + val EMPTY = MutableMediaItemTrackerData() } } + +/** + * Immutable MediaItem tracker data. + */ +class MediaItemTrackerData internal constructor( + mutableMediaItemTrackerData: MutableMediaItemTrackerData +) : Map> by mutableMediaItemTrackerData diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerList.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerList.kt deleted file mode 100644 index 4d2ea8d3c..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerList.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.tracker - -/** - * This class holds a list of [MediaItemTracker]. - * - * ```kotlin - * val trackers = MediaItemTrackerList() - * trackers.add(tracker) - * trackers.addAll(tracker1, tracker2) - * ``` - * - * @constructor Create an empty `MediaItemTrackerList`. - */ -class MediaItemTrackerList internal constructor() : Iterable { - private val trackers = mutableListOf() - - internal val trackerList: List = trackers - - /** - * Add a tracker to the list. Each [tracker] type can only be added once to this [MediaItemTracker]. - * - * @param tracker The tracker to add. - * @return `true` if the tracker was successfully added, `false` otherwise. - */ - fun add(tracker: MediaItemTracker): Boolean { - return if (trackers.none { it::class.java == tracker::class.java }) { - trackers.add(tracker) - } else { - false - } - } - - /** - * Add multiple trackers at once to the list. Each [tracker] type can only be added once to this [MediaItemTracker]. - * - * @param trackers The trackers to add. - * @return `false` if one of the trackers was already added, `true` otherwise. - */ - fun addAll(trackers: List): Boolean { - var added = true - for (tracker in trackers) { - if (!add(tracker)) { - added = false - } - } - return added - } - - /** - * Clear the list of trackers. - */ - fun clear() { - trackers.clear() - } - - /** - * Check if the list of trackers is empty of not. - * - * @return `true` if the list is empty, `false` otherwise. - */ - fun isEmpty(): Boolean { - return trackers.isEmpty() - } - - override fun iterator(): Iterator { - return trackers.iterator() - } -} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerProvider.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerProvider.kt deleted file mode 100644 index 4fd3b61de..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerProvider.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.tracker - -/** - * Tracker factory - * - * @constructor Create empty Tracker factory - */ -interface MediaItemTrackerProvider { - /** - * Get media item tracker factory - * - * @param trackerClass - * @return - */ - fun getMediaItemTrackerFactory(trackerClass: Class<*>): MediaItemTracker.Factory -} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerRepository.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerRepository.kt deleted file mode 100644 index 812395489..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerRepository.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.tracker - -/** - * Media item media item tracker repository - * - * @constructor Create empty Media item media item tracker repository - */ -class MediaItemTrackerRepository : MediaItemTrackerProvider { - private val map = mutableMapOf, MediaItemTracker.Factory>() - - /** - * Register factory - * - * @param T Class type extends [MediaItemTracker] - * @param trackerClass The class the trackerFactory create. Clazz must extends MediaItemTracker. - * @param trackerFactory The tracker factory associated with clazz. - */ - fun registerFactory(trackerClass: Class, trackerFactory: MediaItemTracker.Factory) { - map[trackerClass] = trackerFactory - } - - override fun getMediaItemTrackerFactory(trackerClass: Class<*>): MediaItemTracker.Factory { - assert(map.contains(trackerClass)) { "No MediaItemTracker.Factory found for $trackerClass" } - return map[trackerClass]!! - } -} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt new file mode 100644 index 000000000..76fbf4120 --- /dev/null +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player + +import androidx.media3.common.Format +import androidx.media3.common.TrackGroup +import androidx.media3.exoplayer.source.TrackGroupArray +import androidx.media3.test.utils.FakeMediaPeriod +import androidx.test.ext.junit.runners.AndroidJUnit4 +import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange +import ch.srgssr.pillarbox.player.source.PillarboxMediaPeriod +import ch.srgssr.pillarbox.player.source.PillarboxMediaSource.Companion.PILLARBOX_BLOCKED_MIME_TYPE +import ch.srgssr.pillarbox.player.source.PillarboxMediaSource.Companion.PILLARBOX_TRACKERS_MIME_TYPE +import ch.srgssr.pillarbox.player.tracker.FactoryData +import ch.srgssr.pillarbox.player.tracker.FakeMediaItemTracker +import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData +import ch.srgssr.pillarbox.player.tracker.MutableMediaItemTrackerData +import io.mockk.mockk +import org.junit.runner.RunWith +import kotlin.test.Test +import kotlin.test.assertEquals + +@RunWith(AndroidJUnit4::class) +class PillarboxMediaPeriodTest { + + @Test + fun `test track group with no tracker data and no blocked time range`() { + val mediaItemTrackData = MediaItemTrackerData(MutableMediaItemTrackerData.EMPTY) + val blockedTimeRangeList = emptyList() + val mediaPeriod = PillarboxMediaPeriod( + mediaPeriod = createFakeChildMediaPeriod(), + mediaItemTrackerData = mediaItemTrackData, + blockedTimeRanges = blockedTimeRangeList + ) + val expectedTrackGroup = TrackGroupArray( + TrackGroup(createDummyFormat("DummyId")) + ) + mediaPeriod.prepare(mockk(relaxed = true), 0) + assertEquals(expectedTrackGroup, mediaPeriod.trackGroups) + } + + @Test + fun `test track group with tracker data and blocked time range`() { + val mutableMediaItemTrackerData = MutableMediaItemTrackerData() + mutableMediaItemTrackerData[Any()] = FactoryData(FakeMediaItemTracker.Factory(FakeMediaItemTracker()), FakeMediaItemTracker.Data("Test01")) + val mediaItemTrackerData = mutableMediaItemTrackerData.toMediaItemTrackerData() + val blockedTimeRangeList = listOf(BlockedTimeRange(0L, 100L), BlockedTimeRange(200L, 300L)) + val mediaPeriod = PillarboxMediaPeriod( + mediaPeriod = createFakeChildMediaPeriod(), + mediaItemTrackerData = mediaItemTrackerData, + blockedTimeRanges = blockedTimeRangeList + ) + val expectedTrackGroup = TrackGroupArray( + TrackGroup(createDummyFormat("DummyId")), + TrackGroup( + "Pillarbox-Trackers", + Format.Builder() + .setId("TrackerData:0") + .setSampleMimeType(PILLARBOX_TRACKERS_MIME_TYPE) + .setCustomData(mediaItemTrackerData) + .build() + ), + TrackGroup( + "Pillarbox-BlockedTimeRanges", + Format.Builder() + .setSampleMimeType(PILLARBOX_BLOCKED_MIME_TYPE) + .setId("BlockedTimeRanges") + .setCustomData(blockedTimeRangeList) + .build(), + ) + ) + mediaPeriod.prepare(mockk(relaxed = true), 0) + assertEquals(expectedTrackGroup, mediaPeriod.trackGroups) + } + + @Test + fun `test track group with tracker data only`() { + val mutableMediaItemTrackerData = MutableMediaItemTrackerData() + mutableMediaItemTrackerData[Any()] = FactoryData(FakeMediaItemTracker.Factory(FakeMediaItemTracker()), FakeMediaItemTracker.Data("Test01")) + val mediaItemTrackerData = mutableMediaItemTrackerData.toMediaItemTrackerData() + val blockedTimeRangeList = emptyList() + val mediaPeriod = PillarboxMediaPeriod( + mediaPeriod = createFakeChildMediaPeriod(), + mediaItemTrackerData = mediaItemTrackerData, + blockedTimeRanges = blockedTimeRangeList + ) + val expectedTrackGroup = TrackGroupArray( + TrackGroup(createDummyFormat("DummyId")), + TrackGroup( + "Pillarbox-Trackers", + Format.Builder() + .setId("TrackerData:0") + .setSampleMimeType(PILLARBOX_TRACKERS_MIME_TYPE) + .setCustomData(mediaItemTrackerData) + .build() + ) + ) + mediaPeriod.prepare(mockk(relaxed = true), 0) + assertEquals(expectedTrackGroup, mediaPeriod.trackGroups) + } + + @Test + fun `test track group with blocked time range only`() { + val mediaItemTrackData = MediaItemTrackerData(MutableMediaItemTrackerData.EMPTY) + val blockedTimeRangeList = listOf(BlockedTimeRange(0L, 100L), BlockedTimeRange(200L, 300L)) + val mediaPeriod = PillarboxMediaPeriod( + mediaPeriod = createFakeChildMediaPeriod(), + mediaItemTrackerData = mediaItemTrackData, + blockedTimeRanges = blockedTimeRangeList + ) + val expectedTrackGroup = TrackGroupArray( + TrackGroup(createDummyFormat("DummyId")), + TrackGroup( + "Pillarbox-BlockedTimeRanges", + Format.Builder() + .setSampleMimeType(PILLARBOX_BLOCKED_MIME_TYPE) + .setId("BlockedTimeRanges") + .setCustomData(blockedTimeRangeList) + .build(), + ) + ) + mediaPeriod.prepare(mockk(relaxed = true), 0) + assertEquals(expectedTrackGroup, mediaPeriod.trackGroups) + } + + companion object { + fun createFakeChildMediaPeriod(trackGroupArray: TrackGroupArray = createFakeTracks()) = + FakeMediaPeriod(trackGroupArray, mockk(relaxed = true), 0L, mockk(relaxed = true)) + + fun createDummyFormat(id: String) = Format.Builder() + .setId(id) + .build() + + private fun createFakeTracks(): TrackGroupArray { + return TrackGroupArray( + TrackGroup( + createDummyFormat("DummyId") + ) + ) + } + } +} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/PlayerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/PlayerTest.kt index 20dae1dd2..00a469e0f 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/PlayerTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/PlayerTest.kt @@ -9,7 +9,7 @@ import androidx.media3.common.MediaMetadata import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.test.ext.junit.runners.AndroidJUnit4 -import ch.srgssr.pillarbox.player.asset.PillarboxData +import ch.srgssr.pillarbox.player.asset.timeRange.Chapter import ch.srgssr.pillarbox.player.asset.timeRange.Credit import io.mockk.every import io.mockk.mockk @@ -90,16 +90,17 @@ class PlayerTest { } @Test - fun `getCurrentCredits, without MediaItem`() { + fun `getCurrentCredits and getCurrentChapters empty without MediaItem`() { val player = mockk { every { currentMediaItem } returns null } assertEquals(emptyList(), player.getCurrentCredits()) + assertEquals(emptyList(), player.getCurrentChapters()) } @Test - fun `getCurrentCredits, with MediaItem, without PillarboxData`() { + fun `getCurrentCredits, with empty MediaMetadata`() { val player = mockk { every { currentMediaItem } returns MediaItem.Builder().build() } @@ -108,31 +109,44 @@ class PlayerTest { } @Test - fun `getCurrentCredits, with MediaItem, with PillarboxData, without credits`() { + fun `getCurrentCredits, with MediaItem, with credits`() { + val credits = listOf(mockk()) val player = mockk { every { currentMediaItem } returns MediaItem.Builder() .setUri("https://example.com/") - .setTag(PillarboxData()) + .setMediaMetadata( + MediaMetadata.Builder() + .setCredits(credits) + .build() + ) .build() } - assertEquals(emptyList(), player.getCurrentCredits()) + assertEquals(credits, player.getCurrentCredits()) } @Test - fun `getCurrentCredits, with MediaItem, with PillarboxData, with credits`() { - val credits = listOf(mockk()) + fun `getCurrentChapters, with MediaItem, with chapters`() { + val chapter = listOf(mockk()) val player = mockk { every { currentMediaItem } returns MediaItem.Builder() .setUri("https://example.com/") .setMediaMetadata( MediaMetadata.Builder() - .setCredits(credits) + .setChapters(chapters = chapter) .build() ) .build() } - assertEquals(credits, player.getCurrentCredits()) + assertEquals(chapter, player.getCurrentChapters()) + } + + @Test + fun `getCurrentChapter, with empty MediaMetadata`() { + val player = mockk { + every { currentMediaItem } returns MediaItem.Builder().build() + } + assertEquals(emptyList(), player.getCurrentChapters()) } } diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemPillarboxDataTrackerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemPillarboxDataTrackerTest.kt deleted file mode 100644 index d33bc700c..000000000 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemPillarboxDataTrackerTest.kt +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.tracker - -import android.content.Context -import android.os.Looper -import androidx.media3.common.Player -import androidx.media3.exoplayer.ExoPlayer -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.PillarboxExoPlayer -import ch.srgssr.pillarbox.player.asset.PillarboxData -import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory -import io.mockk.confirmVerified -import io.mockk.mockk -import io.mockk.verifyOrder -import org.junit.runner.RunWith -import org.robolectric.Shadows.shadowOf -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.Test - -@RunWith(AndroidJUnit4::class) -class CurrentMediaItemPillarboxDataTrackerTest { - private lateinit var clock: FakeClock - private lateinit var context: Context - private lateinit var player: ExoPlayer - private lateinit var dataTracker: CurrentMediaItemPillarboxDataTracker - - @BeforeTest - fun setUp() { - clock = FakeClock(true) - context = ApplicationProvider.getApplicationContext() - - player = PillarboxExoPlayer( - context = context, - mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { - addAssetLoader(FakeAssetLoader(context)) - }, - mediaItemTrackerProvider = FakeTrackerProvider(FakeMediaItemTracker()), - clock = clock, - coroutineContext = EmptyCoroutineContext, - ) - - dataTracker = CurrentMediaItemPillarboxDataTracker(player) - } - - @AfterTest - fun tearDown() { - player.release() - shadowOf(Looper.getMainLooper()).idle() - } - - @Test - fun `player with no media item`() { - val callback = mockk() - - player.prepare() - player.play() - - dataTracker.addCallback(callback) - - verifyOrder { - callback.hashCode() - } - - confirmVerified(callback) - } - - @Test - fun `player with no tracking data media item`() { - val callback = mockk(relaxed = true) - val mediaItem = FakeAssetLoader.MEDIA_NO_TRACKING_DATA - val expectedPillarboxData = PillarboxData( - MediaItemTrackerData.Builder() - .build() - ) - - player.setMediaItem(mediaItem) - player.prepare() - player.play() - - dataTracker.addCallback(callback) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - - verifyOrder { - callback.hashCode() - callback.onPillarboxDataChanged(expectedPillarboxData) - } - confirmVerified(callback) - } - - @Test - fun `player with tracking data media item`() { - val callback = mockk(relaxed = true) - val mediaItem = FakeAssetLoader.MEDIA_1 - val expectedPillarboxData = PillarboxData( - trackersData = MediaItemTrackerData.Builder() - .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) - .build() - ) - - player.setMediaItem(mediaItem) - player.prepare() - player.play() - - dataTracker.addCallback(callback) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - - verifyOrder { - callback.hashCode() - callback.onPillarboxDataChanged(expectedPillarboxData) - } - confirmVerified(callback) - } - - @Test - fun `player gets its media item replaced`() { - val callback = mockk(relaxed = true) - val mediaItem1 = FakeAssetLoader.MEDIA_1 - val mediaItem2 = FakeAssetLoader.MEDIA_2 - val expectedPillarboxData1 = PillarboxData( - trackersData = MediaItemTrackerData.Builder() - .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) - .build() - ) - val expectedPillarboxData2 = PillarboxData( - trackersData = MediaItemTrackerData.Builder() - .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_2)) - .build() - ) - - player.setMediaItem(mediaItem1) - player.prepare() - player.play() - - dataTracker.addCallback(callback) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - - player.setMediaItem(mediaItem2) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - - verifyOrder { - callback.hashCode() - callback.onPillarboxDataChanged(expectedPillarboxData1) - callback.onPillarboxDataChanged(null) - callback.onPillarboxDataChanged(expectedPillarboxData2) - } - confirmVerified(callback) - } - - @Test - fun `player gets its media item updated`() { - val callback = mockk(relaxed = true) - val mediaItem1 = FakeAssetLoader.MEDIA_1 - val mediaItem2 = mediaItem1.buildUpon() - .setMediaId(FakeAssetLoader.MEDIA_ID_2) - .build() - val expectedPillarboxData1 = PillarboxData( - MediaItemTrackerData.Builder() - .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) - .build() - ) - - player.setMediaItem(mediaItem1) - player.prepare() - player.play() - - dataTracker.addCallback(callback) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - - player.replaceMediaItem(player.currentMediaItemIndex, mediaItem2) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - - verifyOrder { - callback.hashCode() - callback.onPillarboxDataChanged(expectedPillarboxData1) - callback.onPillarboxDataChanged(null) - } - confirmVerified(callback) - } - - @Test - fun `player gets its media item removed`() { - val callback = mockk(relaxed = true) - val mediaItem1 = FakeAssetLoader.MEDIA_1 - val expectedPillarboxData = PillarboxData( - trackersData = MediaItemTrackerData.Builder() - .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) - .build() - ) - - player.setMediaItem(mediaItem1) - player.prepare() - player.play() - - dataTracker.addCallback(callback) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - player.removeMediaItem(0) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) - - verifyOrder { - callback.hashCode() - callback.onPillarboxDataChanged(expectedPillarboxData) - callback.onPillarboxDataChanged(null) - } - confirmVerified(callback) - } - - @Test - fun `player gets a new item added`() { - val callback = mockk(relaxed = true) - val mediaItem1 = FakeAssetLoader.MEDIA_1 - val mediaItem2 = FakeAssetLoader.MEDIA_2 - val expectedPillarboxData = PillarboxData( - trackersData = MediaItemTrackerData.Builder() - .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) - .build() - ) - - player.setMediaItem(mediaItem1) - player.prepare() - player.play() - - dataTracker.addCallback(callback) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - - player.addMediaItem(mediaItem2) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - - verifyOrder { - callback.hashCode() - callback.onPillarboxDataChanged(expectedPillarboxData) - } - confirmVerified(callback) - } - - @Test - fun `player transition to the next item`() { - val callback = mockk(relaxed = true) - val mediaItem1 = FakeAssetLoader.MEDIA_1 - val mediaItem2 = FakeAssetLoader.MEDIA_2 - val expectedPillarboxData1 = PillarboxData( - trackersData = MediaItemTrackerData.Builder() - .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) - .build() - ) - val expectedPillarboxData2 = PillarboxData( - trackersData = MediaItemTrackerData.Builder() - .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_2)) - .build() - ) - - player.addMediaItem(mediaItem1) - player.addMediaItem(mediaItem2) - player.prepare() - player.play() - - dataTracker.addCallback(callback) - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - player.seekToNextMediaItem() - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - - verifyOrder { - callback.hashCode() - callback.onPillarboxDataChanged(expectedPillarboxData1) - callback.onPillarboxDataChanged(null) - callback.onPillarboxDataChanged(expectedPillarboxData2) - } - confirmVerified(callback) - } - - @Test - fun `playlist gets cleared`() { - val callback = mockk(relaxed = true) - val mediaItem1 = FakeAssetLoader.MEDIA_1 - val mediaItem2 = FakeAssetLoader.MEDIA_2 - val expectedPillarboxData = PillarboxData( - MediaItemTrackerData.Builder() - .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) - .build() - ) - - player.setMediaItems(listOf(mediaItem1, mediaItem2)) - player.prepare() - player.play() - - dataTracker.addCallback(callback) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - - player.clearMediaItems() - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) - - verifyOrder { - callback.hashCode() - callback.onPillarboxDataChanged(expectedPillarboxData) - callback.onPillarboxDataChanged(null) - } - confirmVerified(callback) - } -} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/FakeAssetLoader.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/FakeAssetLoader.kt index 8d5630566..3006f583c 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/FakeAssetLoader.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/FakeAssetLoader.kt @@ -10,7 +10,10 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import ch.srgssr.pillarbox.player.asset.Asset import ch.srgssr.pillarbox.player.asset.AssetLoader -class FakeAssetLoader(context: Context) : AssetLoader(DefaultMediaSourceFactory(context)) { +class FakeAssetLoader( + context: Context, + private val fakeMediaItemTracker: FakeMediaItemTracker, +) : AssetLoader(DefaultMediaSourceFactory(context)) { override fun canLoadAsset(mediaItem: MediaItem): Boolean { return mediaItem.localConfiguration != null @@ -19,11 +22,14 @@ class FakeAssetLoader(context: Context) : AssetLoader(DefaultMediaSourceFactory( override suspend fun loadAsset(mediaItem: MediaItem): Asset { val itemBuilder = mediaItem.buildUpon() val trackerData = if (mediaItem.mediaId == MEDIA_ID_NO_TRACKING_DATA) { - MediaItemTrackerData.EMPTY + MutableMediaItemTrackerData.EMPTY.toMediaItemTrackerData() } else { - MediaItemTrackerData.Builder() - .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(mediaItem.mediaId)) - .build() + MutableMediaItemTrackerData().apply { + put( + FakeMediaItemTracker::class.java, + FactoryData(FakeMediaItemTracker.Factory(fakeMediaItemTracker), FakeMediaItemTracker.Data(mediaItem.mediaId)) + ) + }.toMediaItemTrackerData() } return Asset( mediaSource = mediaSourceFactory.createMediaSource(itemBuilder.build()), diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/FakeMediaItemTracker.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/FakeMediaItemTracker.kt index 7f5a33c47..1ec8bbd4c 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/FakeMediaItemTracker.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/FakeMediaItemTracker.kt @@ -6,32 +6,21 @@ package ch.srgssr.pillarbox.player.tracker import androidx.media3.exoplayer.ExoPlayer -class FakeMediaItemTracker : MediaItemTracker { +class FakeMediaItemTracker : MediaItemTracker { data class Data(val id: String) - override fun start(player: ExoPlayer, initialData: Any?) { - require(initialData is Data) - println("start $initialData") + override fun start(player: ExoPlayer, data: Data) { + println("start $data") } - override fun stop(player: ExoPlayer, reason: MediaItemTracker.StopReason, positionMs: Long) { + override fun stop(player: ExoPlayer) { // Nothing - println("stop $reason $positionMs") + println("stop") } - class Factory(private val fakeMediaItemTracker: FakeMediaItemTracker) : MediaItemTracker.Factory { - override fun create(): MediaItemTracker { + class Factory(private val fakeMediaItemTracker: FakeMediaItemTracker) : MediaItemTracker.Factory { + override fun create(): FakeMediaItemTracker { return fakeMediaItemTracker } } } - -class FakeTrackerProvider(private val fakeMediaItemTracker: FakeMediaItemTracker) : MediaItemTrackerProvider { - override fun getMediaItemTrackerFactory(trackerClass: Class<*>): MediaItemTracker.Factory { - return object : MediaItemTracker.Factory { - override fun create(): MediaItemTracker { - return fakeMediaItemTracker - } - } - } -} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerDataTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerDataTest.kt deleted file mode 100644 index 1e1492469..000000000 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerDataTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.tracker - -import androidx.media3.exoplayer.ExoPlayer -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotEquals -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class MediaItemTrackerDataTest { - @Test - fun `media item tracker data`() { - val emptyMediaItemTrackerData = MediaItemTrackerData.EMPTY - val mediaItemTracker1 = MediaItemTracker1() - val mediaItemTracker2 = MediaItemTracker2() - - assertTrue(emptyMediaItemTrackerData.trackers.isEmpty()) - assertTrue(emptyMediaItemTrackerData.isEmpty) - assertFalse(emptyMediaItemTrackerData.isNotEmpty) - assertNull(emptyMediaItemTrackerData.getData(mediaItemTracker1)) - assertNull(emptyMediaItemTrackerData.getDataAs(mediaItemTracker1)) - assertNull(emptyMediaItemTrackerData.getData(mediaItemTracker2)) - assertNull(emptyMediaItemTrackerData.getDataAs(mediaItemTracker2)) - - val mediaItemTrackerDataUpdated = emptyMediaItemTrackerData.buildUpon() - .putData(mediaItemTracker1::class.java, "Some value") - .putData(mediaItemTracker2::class.java) - .build() - - assertEquals(setOf(mediaItemTracker1::class.java, mediaItemTracker2::class.java), mediaItemTrackerDataUpdated.trackers) - assertFalse(mediaItemTrackerDataUpdated.isEmpty) - assertTrue(mediaItemTrackerDataUpdated.isNotEmpty) - assertEquals("Some value", mediaItemTrackerDataUpdated.getData(mediaItemTracker1)) - assertEquals("Some value", mediaItemTrackerDataUpdated.getDataAs(mediaItemTracker1)) - assertNull(mediaItemTrackerDataUpdated.getData(mediaItemTracker2)) - assertNull(mediaItemTrackerDataUpdated.getDataAs(mediaItemTracker2)) - } - - @Test - fun `empty media item tracker data are equals`() { - assertEquals(MediaItemTrackerData.EMPTY, MediaItemTrackerData.Builder().build()) - } - - @Test - fun `media item tracker data are equals`() { - val mediaItemTrackerData1 = MediaItemTrackerData.Builder() - .putData(MediaItemTracker1::class.java, "Data1") - .putData(MediaItemTracker2::class.java, "Data2") - .build() - val mediaItemTrackerData2 = MediaItemTrackerData.Builder() - .putData(MediaItemTracker1::class.java, "Data1") - .putData(MediaItemTracker2::class.java, "Data2") - .build() - assertEquals(mediaItemTrackerData1, mediaItemTrackerData2) - } - - @Test - fun `media item tracker data are not equals when data changes`() { - val mediaItemTrackerData1 = MediaItemTrackerData.Builder() - .putData(MediaItemTracker1::class.java, "Data1") - .putData(MediaItemTracker2::class.java, "Data2") - .build() - val mediaItemTrackerData2 = MediaItemTrackerData.Builder() - .putData(MediaItemTracker1::class.java, "Data1") - .build() - assertNotEquals(mediaItemTrackerData1, mediaItemTrackerData2) - val mediaItemTrackerData3 = MediaItemTrackerData.Builder() - .putData(MediaItemTracker1::class.java, "Data1") - val mediaItemTrackerData4 = MediaItemTrackerData.Builder() - .putData(MediaItemTracker1::class.java, "Data2") - assertNotEquals(mediaItemTrackerData3, mediaItemTrackerData4) - } - - private open class EmptyMediaItemTracker : MediaItemTracker { - override fun start(player: ExoPlayer, initialData: Any?) = Unit - - override fun stop(player: ExoPlayer, reason: MediaItemTracker.StopReason, positionMs: Long) = Unit - } - - private class MediaItemTracker1 : EmptyMediaItemTracker() - - private class MediaItemTracker2 : EmptyMediaItemTracker() -} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerListTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerListTest.kt deleted file mode 100644 index bfa10bbd6..000000000 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerListTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.tracker - -import androidx.media3.exoplayer.ExoPlayer -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class MediaItemTrackerListTest { - @Test - fun `empty tracker list`() { - val trackers = MediaItemTrackerList() - assertTrue(trackers.isEmpty()) - assertEquals(0, trackers.count()) - assertEquals(emptyList(), trackers.trackerList) - } - - @Test - fun `add single tracker`() { - val trackers = MediaItemTrackerList() - val tracker = ItemTrackerA() - assertTrue(trackers.add(tracker)) - assertFalse(trackers.isEmpty()) - assertEquals(1, trackers.count()) - assertEquals(listOf(tracker), trackers.trackerList) - } - - @Test - fun `add same kind of tracker multiple times`() { - val trackers = MediaItemTrackerList() - val trackerA = ItemTrackerA() - val trackerAA = ItemTrackerA() - assertTrue(trackers.add(trackerA)) - assertFalse(trackers.add(trackerAA)) - assertFalse(trackers.isEmpty()) - assertEquals(1, trackers.count()) - assertEquals(listOf(trackerA), trackers.trackerList) - } - - @Test - fun `add different kind of trackers`() { - val trackers = MediaItemTrackerList() - val trackerList = listOf(ItemTrackerA(), ItemTrackerB(), ItemTrackerC()) - for (tracker in trackerList) { - assertTrue(trackers.add(tracker)) - } - - assertFalse(trackers.isEmpty()) - assertEquals(trackerList.size, trackers.count()) - assertEquals(trackerList, trackers.trackerList) - } - - @Test - fun `add different kind of trackers with open tracker`() { - val trackers = MediaItemTrackerList() - val trackerList = listOf(ItemTrackerC(), ItemTrackerD()) - for (tracker in trackerList) { - assertTrue(trackers.add(tracker)) - } - - assertFalse(trackers.isEmpty()) - assertEquals(trackerList.size, trackers.count()) - assertEquals(trackerList, trackers.trackerList) - - val trackersRevert = MediaItemTrackerList() - val trackerListRevert = listOf(ItemTrackerD(), ItemTrackerC()) - for (tracker in trackerListRevert) { - assertTrue(trackersRevert.add(tracker)) - } - - assertFalse(trackerListRevert.isEmpty()) - assertEquals(trackerListRevert.size, trackersRevert.count()) - assertEquals(trackerListRevert, trackersRevert.trackerList) - } - - @Test - fun `add multiple trackers`() { - val trackers = MediaItemTrackerList() - val trackerList = listOf(ItemTrackerA(), ItemTrackerB(), ItemTrackerA(), ItemTrackerC()) - val expectedTrackers = trackerList.distinctBy { it::class.java } - assertFalse(trackers.addAll(trackerList)) - assertEquals(expectedTrackers.size, trackers.count()) - assertEquals(expectedTrackers, trackers.trackerList) - } - - private open class EmptyItemTracker : MediaItemTracker { - override fun start(player: ExoPlayer, initialData: Any?) { - // Nothing - } - - override fun stop(player: ExoPlayer, reason: MediaItemTracker.StopReason, positionMs: Long) { - // Nothing - } - } - - private class ItemTrackerA : EmptyItemTracker() - - private class ItemTrackerB : EmptyItemTracker() - - private open class ItemTrackerC : EmptyItemTracker() - - private class ItemTrackerD : ItemTrackerC() -} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerRepositoryTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerRepositoryTest.kt deleted file mode 100644 index c50b9fc75..000000000 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerRepositoryTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.tracker - -import androidx.media3.exoplayer.ExoPlayer -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class MediaItemTrackerRepositoryTest { - private lateinit var trackerRepository: MediaItemTrackerRepository - - @BeforeTest - fun init() { - trackerRepository = MediaItemTrackerRepository() - } - - @Test(expected = AssertionError::class) - fun `tracker not found`() { - trackerRepository.getMediaItemTrackerFactory(String::class.java) - } - - @Test - fun `retrieve tracker`() { - val testFactory = TestTracker.Factory() - trackerRepository.registerFactory(TestTracker::class.java, testFactory) - val factory = trackerRepository.getMediaItemTrackerFactory(TestTracker::class.java) - assertEquals(TestTracker.Factory::class.java, factory::class.java) - assertEquals(testFactory, factory) - } - - private class TestTracker : MediaItemTracker { - class Factory : MediaItemTracker.Factory { - override fun create(): MediaItemTracker { - return TestTracker() - } - } - - override fun start(player: ExoPlayer, initialData: Any?) { - // Nothing - } - - override fun stop(player: ExoPlayer, reason: MediaItemTracker.StopReason, positionMs: Long) { - // Nothing - } - } -} 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 b323829b7..86c9a8200 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 @@ -16,7 +16,6 @@ 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.extension.getPillarboxDataOrNull import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import io.mockk.clearAllMocks import io.mockk.confirmVerified @@ -51,9 +50,8 @@ class MediaItemTrackerTest { clock = fakeClock, coroutineContext = EmptyCoroutineContext, mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { - addAssetLoader(FakeAssetLoader(context)) + addAssetLoader(FakeAssetLoader(context, fakeMediaItemTracker)) }, - mediaItemTrackerProvider = FakeTrackerProvider(fakeMediaItemTracker) ) } @@ -80,7 +78,7 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.Stop, player.currentPosition) + fakeMediaItemTracker.stop(any()) } confirmVerified(fakeMediaItemTracker) } @@ -102,7 +100,7 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.Stop, player.currentPosition) + fakeMediaItemTracker.stop(any()) fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) } @@ -125,7 +123,6 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.EoF, player.currentPosition) } confirmVerified(fakeMediaItemTracker) } @@ -144,33 +141,7 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.Stop, player.currentPosition) - } - confirmVerified(fakeMediaItemTracker) - } - - @Test - fun `one MediaItem reach eof then seek back`() { - val mediaItem = FakeAssetLoader.MEDIA_1 - val mediaId = mediaItem.mediaId - player.apply { - setMediaItem(mediaItem) - prepare() - play() - } - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - player.seekTo(FakeAssetLoader.NEAR_END_POSITION_MS) - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) - - player.seekBack() - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) - - verifyOrder { - fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.EoF, player.duration) - fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) + fakeMediaItemTracker.stop(any()) } confirmVerified(fakeMediaItemTracker) } @@ -195,7 +166,7 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(firstMediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.Stop, any()) + fakeMediaItemTracker.stop(any()) fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(secondMediaId)) } confirmVerified(fakeMediaItemTracker) @@ -218,7 +189,7 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(firstMediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.Stop, any()) + fakeMediaItemTracker.stop(any()) } verify(exactly = 0) { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(secondMediaId)) @@ -240,7 +211,7 @@ class MediaItemTrackerTest { verifyAll { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.Stop, any()) + fakeMediaItemTracker.stop(any()) } confirmVerified(fakeMediaItemTracker) } @@ -265,7 +236,7 @@ class MediaItemTrackerTest { verifyAll { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(firstMediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.Stop, any()) + fakeMediaItemTracker.stop(any()) fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(secondMediaId)) } confirmVerified(fakeMediaItemTracker) @@ -284,7 +255,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) // Wait for MediaItemSource to be loaded RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull() != null + player.getMediaItemTrackerDataOrNull() != null } val currentMediaItem = player.currentMediaItem!! val mediaUpdate = currentMediaItem.buildUpon() @@ -310,12 +281,12 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull() != null + player.getMediaItemTrackerDataOrNull() != null } val mediaItem = player.currentMediaItem assertNotNull(mediaItem) val mediaUpdate = mediaItem.buildUpon() - // .setTrackerData(mediaItem.getPillarboxDataOrNull().buildUpon().build()) + .setTag(Any()) .build() println("replace media item") player.replaceMediaItem(0, mediaUpdate) @@ -341,56 +312,24 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull()?.trackersData?.getData(fakeMediaItemTracker) == FakeMediaItemTracker.Data( - FakeAssetLoader - .MEDIA_ID_1 - ) + player.getMediaItemTrackerDataOrNull() != null } player.replaceMediaItem(0, FakeAssetLoader.MEDIA_2) TestPlayerRunHelper.runUntilTimelineChanged(player) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull()?.trackersData?.getData(fakeMediaItemTracker) == FakeMediaItemTracker.Data( - FakeAssetLoader.MEDIA_ID_2 - ) + player.getMediaItemTrackerDataOrNull() != null } TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) verifyOrder { fakeMediaItemTracker.start(player, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) - fakeMediaItemTracker.stop(player, MediaItemTracker.StopReason.Stop, player.currentPosition) + fakeMediaItemTracker.stop(player) fakeMediaItemTracker.start(player, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_2)) } confirmVerified(fakeMediaItemTracker) } - @Test - fun `auto transition to next item stop current tracker`() { - val firstMediaId = FakeAssetLoader.MEDIA_ID_1 - val secondMediaId = FakeAssetLoader.MEDIA_ID_2 - player.apply { - addMediaItem(FakeAssetLoader.MEDIA_1) - addMediaItem(FakeAssetLoader.MEDIA_2) - prepare() - seekTo(FakeAssetLoader.NEAR_END_POSITION_MS) - play() - } - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) - - TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) - - verifyOrder { - fakeMediaItemTracker.start(player, FakeMediaItemTracker.Data(firstMediaId)) - fakeMediaItemTracker.stop(player, MediaItemTracker.StopReason.EoF, any()) - fakeMediaItemTracker.start(player, FakeMediaItemTracker.Data(secondMediaId)) - fakeMediaItemTracker.stop(player, MediaItemTracker.StopReason.EoF, any()) - } - confirmVerified(fakeMediaItemTracker) - } - @Test fun `skip next stop current tracker`() { val firstMediaId = FakeAssetLoader.MEDIA_ID_1 @@ -404,7 +343,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull() != null + player.getMediaItemTrackerDataOrNull() != null } player.seekToNextMediaItem() @@ -415,7 +354,7 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(player, FakeMediaItemTracker.Data(firstMediaId)) - fakeMediaItemTracker.stop(player, MediaItemTracker.StopReason.Stop, any()) + fakeMediaItemTracker.stop(player) fakeMediaItemTracker.start(player, FakeMediaItemTracker.Data(secondMediaId)) } confirmVerified(fakeMediaItemTracker) @@ -433,7 +372,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull() != null + player.getMediaItemTrackerDataOrNull() != null } player.seekToPreviousMediaItem() @@ -444,38 +383,9 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(player, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_2)) - fakeMediaItemTracker.stop(player, MediaItemTracker.StopReason.Stop, any()) + fakeMediaItemTracker.stop(player) fakeMediaItemTracker.start(player, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) } confirmVerified(fakeMediaItemTracker) } - - @Test - fun `repeat current item stop with EoF when start again`() { - val firstMediaId = FakeAssetLoader.MEDIA_ID_1 - player.apply { - setMediaItem( - FakeAssetLoader.MEDIA_1, - FakeAssetLoader.NEAR_END_POSITION_MS - ) - player.repeatMode = Player.REPEAT_MODE_ONE - prepare() - play() - } - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - TestPlayerRunHelper.runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION) - player.stop() // Stop player to stop the auto repeat mode - - // Wait on item transition - // Stop otherwise goes crazy. - - verifyAll { - fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(firstMediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.EoF, any()) - fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(firstMediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.Stop, any()) - } - confirmVerified(fakeMediaItemTracker) - } }