From 0de49506554abb456f375f3950e24fccbbb9cd59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Wed, 18 Sep 2024 16:43:51 +0200 Subject: [PATCH 01/32] Slip PillarboxData into separate TrackGroup --- .../pillarbox/player/PillarboxExoPlayer.kt | 6 +- .../pillarbox/player/asset/PillarboxData.kt | 27 -- .../pillarbox/player/extension/Tracks.kt | 18 +- .../player/source/PillarboxMediaPeriod.kt | 45 ++- .../player/source/PillarboxMediaSource.kt | 35 +- .../tracker/AnalyticsMediaItemTracker.kt | 63 ++-- .../player/tracker/BlockedTimeRangeTracker.kt | 32 +- .../CurrentMediaItemPillarboxDataTracker.kt | 75 ----- .../pillarbox/player/extension/PlayerTest.kt | 3 +- ...urrentMediaItemPillarboxDataTrackerTest.kt | 318 ------------------ .../player/tracker/MediaItemTrackerTest.kt | 6 +- 11 files changed, 121 insertions(+), 507 deletions(-) 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/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemPillarboxDataTrackerTest.kt 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 4a37311a9..ccdf19b48 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 @@ -41,7 +41,6 @@ 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.PillarboxMediaMetaDataTracker @@ -78,7 +77,6 @@ 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) internal val sessionManager = PlaybackSessionManager() private val window = Window() @@ -133,8 +131,6 @@ class PillarboxExoPlayer internal constructor( blockedTimeRangeTracker.setPlayer(this) addListener(analyticsCollector) exoPlayer.addListener(ComponentListener()) - itemPillarboxDataTracker.addCallback(blockedTimeRangeTracker) - itemPillarboxDataTracker.addCallback(analyticsTracker) if (BuildConfig.DEBUG) { addAnalyticsListener(PillarboxEventLogger()) } @@ -343,9 +339,9 @@ class PillarboxExoPlayer internal constructor( clearSeeking() mediaMetadataTracker.release() blockedTimeRangeTracker.release() + analyticsTracker.release() exoPlayer.release() listeners.release() - itemPillarboxDataTracker.release() } private fun notifyTimeRangeChanged(timeRange: TimeRange?) { 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/extension/Tracks.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Tracks.kt index 3f5d73c66..e8a6a8b00 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,17 @@ 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.getPillarboxDataOrNull(): 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 +} + +@Suppress("UNCHECKED_CAST") +internal 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..cde1a8923 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,43 @@ 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 = arrayOf( + 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(blockedTimeRanges) + .build(), + ) ) @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..9e984f8ae 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,8 @@ 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.utils.DebugLogger import kotlinx.coroutines.runBlocking import java.io.IOException @@ -48,7 +49,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 = MediaItemTrackerData.EMPTY + private var blockedTimeRanges: List = emptyList() @Suppress("TooGenericExceptionCaught") override fun prepareSourceInternal(mediaTransferListener: TransferListener?) { @@ -63,10 +65,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 +134,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 +216,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..36e7e1c49 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 @@ -7,9 +7,9 @@ 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.utils.DebugLogger @@ -17,22 +17,35 @@ 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. * @param player The [Player] whose current [MediaItem] is tracked for analytics. * @param mediaItemTrackerProvider The [MediaItemTrackerProvider] that provide new instance of [MediaItemTracker]. */ internal class AnalyticsMediaItemTracker( private val player: ExoPlayer, private val mediaItemTrackerProvider: MediaItemTrackerProvider, -) : CurrentMediaItemPillarboxDataTracker.Callback { +) : Player.Listener { private val listener = CurrentMediaItemListener() /** * Trackers are empty if the tracking session is stopped. */ private var trackers = MediaItemTrackerList() - private var currentPillarboxData: PillarboxData? = null + private var currentMediaItemTrackerData: MediaItemTrackerData? = null + set(value) { + if (field !== value) { + DebugLogger.info(TAG, "onPillarboxDataChanged $field -> $value") + stopSession(StopReason.Stop) + player.removeAnalyticsListener(listener) + field = value + field?.let { + if (it.isNotEmpty) { + player.addAnalyticsListener(listener) + startNewSession(it) + } + } + } + } var enabled: Boolean = true set(value) { @@ -42,7 +55,7 @@ internal class AnalyticsMediaItemTracker( field = value if (field) { - currentPillarboxData = player.currentTracks.getPillarboxDataOrNull()?.let { + currentMediaItemTrackerData = player.currentTracks.getPillarboxDataOrNull()?.let { startNewSession(data = it) it } @@ -51,19 +64,17 @@ internal class AnalyticsMediaItemTracker( } } - 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.addListener(this) + currentMediaItemTrackerData = player.currentTracks.getPillarboxDataOrNull() + } + + override fun onTracksChanged(tracks: Tracks) { + currentMediaItemTrackerData = tracks.getPillarboxDataOrNull() + } + + fun release() { + currentMediaItemTrackerData = null } private fun stopSession( @@ -78,8 +89,8 @@ internal class AnalyticsMediaItemTracker( trackers.clear() } - private fun startNewSession(data: PillarboxData) { - if (!enabled || data.trackersData.trackers.isEmpty()) { + private fun startNewSession(data: MediaItemTrackerData) { + if (!enabled || data.trackers.isEmpty()) { return } require(trackers.isEmpty()) @@ -87,10 +98,10 @@ internal class AnalyticsMediaItemTracker( DebugLogger.info(TAG, "Start new session for ${player.currentMediaItem?.prettyString()}") // Create each tracker for this new MediaItem - val trackers = data.trackersData.trackers + val trackers = data.trackers .map { trackerType -> mediaItemTrackerProvider.getMediaItemTrackerFactory(trackerType).create() - .also { it.start(player, data.trackersData.getData(it)) } + .also { it.start(player, data.getData(it)) } } this.trackers.addAll(trackers) @@ -110,8 +121,8 @@ internal class AnalyticsMediaItemTracker( Player.STATE_ENDED -> stopSession(StopReason.EoF) Player.STATE_IDLE -> stopSession(StopReason.Stop) Player.STATE_READY -> { - if (trackers.isEmpty() && currentPillarboxData != null) { - startNewSession(data = currentPillarboxData!!) + if (trackers.isEmpty() && currentMediaItemTrackerData != null) { + startNewSession(data = currentMediaItemTrackerData!!) } } @@ -139,7 +150,7 @@ internal class AnalyticsMediaItemTracker( Player.DISCONTINUITY_REASON_AUTO_TRANSITION -> { stopSession(StopReason.EoF, oldPositionMs) if (oldPosition.mediaItemIndex == newPosition.mediaItemIndex) { - currentPillarboxData?.let { startNewSession(it) } + currentMediaItemTrackerData?.let { startNewSession(it) } } } 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/test/java/ch/srgssr/pillarbox/player/extension/PlayerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/PlayerTest.kt index 20dae1dd2..a7a82fcac 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,6 @@ 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.Credit import io.mockk.every import io.mockk.mockk @@ -112,7 +111,7 @@ class PlayerTest { val player = mockk { every { currentMediaItem } returns MediaItem.Builder() .setUri("https://example.com/") - .setTag(PillarboxData()) + .setTag(Any()) .build() } 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/MediaItemTrackerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerTest.kt index b323829b7..4ab2a40c9 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 @@ -315,7 +315,7 @@ class MediaItemTrackerTest { 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,7 +341,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull()?.trackersData?.getData(fakeMediaItemTracker) == FakeMediaItemTracker.Data( + player.currentTracks.getPillarboxDataOrNull()?.getData(fakeMediaItemTracker) == FakeMediaItemTracker.Data( FakeAssetLoader .MEDIA_ID_1 ) @@ -350,7 +350,7 @@ class MediaItemTrackerTest { player.replaceMediaItem(0, FakeAssetLoader.MEDIA_2) TestPlayerRunHelper.runUntilTimelineChanged(player) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull()?.trackersData?.getData(fakeMediaItemTracker) == FakeMediaItemTracker.Data( + player.currentTracks.getPillarboxDataOrNull()?.getData(fakeMediaItemTracker) == FakeMediaItemTracker.Data( FakeAssetLoader.MEDIA_ID_2 ) } From cb968b2c5d74d5495c344351f32c11b3e766e0f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Wed, 18 Sep 2024 16:56:35 +0200 Subject: [PATCH 02/32] Don't fill pillarbox tracks if they are "empty" --- .../player/source/PillarboxMediaPeriod.kt | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) 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 cde1a8923..1c2cc4ad1 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 @@ -19,24 +19,30 @@ internal class PillarboxMediaPeriod( mediaItemTrackerData: MediaItemTrackerData, blockedTimeRanges: List, ) : MediaPeriod by mediaPeriod { - private val pillarboxTracks = arrayOf( - 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(blockedTimeRanges) - .build(), - ) - ) + private val pillarboxTracks = mutableListOf().apply { + if (mediaItemTrackerData.isNotEmpty) { + add( + TrackGroup( + "Pillarbox-Trackers", + Format.Builder() + .setId("TrackerData:0") + .setSampleMimeType(PILLARBOX_TRACKERS_MIME_TYPE) + .setCustomData(mediaItemTrackerData) + .build(), + ) + ) + } + if (blockedTimeRanges.isNotEmpty()) { + TrackGroup( + "Pillarbox-BlockedTimeRanges", + Format.Builder() + .setSampleMimeType(PILLARBOX_BLOCKED_MIME_TYPE) + .setId("BlockedTimeRanges") + .setCustomData(blockedTimeRanges) + .build(), + ) + } + }.toTypedArray() @Suppress("SpreadOperator") override fun getTrackGroups(): TrackGroupArray { From d59bfc210a43d2ed9048807215f092c3ba7277a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Thu, 19 Sep 2024 08:45:45 +0200 Subject: [PATCH 03/32] Remove useless comments --- .../srgssr/pillarbox/player/tracker/MediaItemTracker.kt | 9 --------- 1 file changed, 9 deletions(-) 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..deeae616f 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 @@ -44,15 +44,6 @@ interface MediaItemTracker { */ 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. - */ - // fun update(data: Any) {} - /** * Factory */ From 935b49c44d4819eb13e308e4892cd182ebb83108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Thu, 19 Sep 2024 09:35:13 +0200 Subject: [PATCH 04/32] Add tests for PillarboxMediaPeriod --- .../player/source/PillarboxMediaPeriod.kt | 16 +- .../player/PillarboxMediaPeriodTest.kt | 141 ++++++++++++++++++ 2 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt 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 1c2cc4ad1..ca27b5c8f 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 @@ -33,13 +33,15 @@ internal class PillarboxMediaPeriod( ) } if (blockedTimeRanges.isNotEmpty()) { - TrackGroup( - "Pillarbox-BlockedTimeRanges", - Format.Builder() - .setSampleMimeType(PILLARBOX_BLOCKED_MIME_TYPE) - .setId("BlockedTimeRanges") - .setCustomData(blockedTimeRanges) - .build(), + add( + TrackGroup( + "Pillarbox-BlockedTimeRanges", + Format.Builder() + .setSampleMimeType(PILLARBOX_BLOCKED_MIME_TYPE) + .setId("BlockedTimeRanges") + .setCustomData(blockedTimeRanges) + .build(), + ) ) } }.toTypedArray() 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..9b83d296d --- /dev/null +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt @@ -0,0 +1,141 @@ +/* + * 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.FakeMediaItemTracker +import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData +import io.mockk.mockk +import org.junit.runner.RunWith +import kotlin.test.Test + +@RunWith(AndroidJUnit4::class) +class PillarboxMediaPeriodTest { + + @Test + fun `test track group with no tracker data and no blocked time range`() { + val mediaItemTrackData = MediaItemTrackerData.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) + assert(mediaPeriod.trackGroups == expectedTrackGroup) + } + + @Test + fun `test track group with tracker data and blocked time range`() { + val mediaItemTrackData = MediaItemTrackerData.Builder() + .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data("TEST01")) + .build() + 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-Trackers", + Format.Builder() + .setId("TrackerData:0") + .setSampleMimeType(PILLARBOX_TRACKERS_MIME_TYPE) + .setCustomData(mediaItemTrackData) + .build() + ), + TrackGroup( + "Pillarbox-BlockedTimeRanges", + Format.Builder() + .setSampleMimeType(PILLARBOX_BLOCKED_MIME_TYPE) + .setId("BlockedTimeRanges") + .setCustomData(blockedTimeRangeList) + .build(), + ) + ) + mediaPeriod.prepare(mockk(relaxed = true), 0) + assert(mediaPeriod.trackGroups == expectedTrackGroup) + } + + @Test + fun `test track group with tracker data only`() { + val mediaItemTrackData = MediaItemTrackerData.Builder() + .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data("TEST01")) + .build() + val blockedTimeRangeList = emptyList() + val mediaPeriod = PillarboxMediaPeriod( + mediaPeriod = createFakeChildMediaPeriod(), + mediaItemTrackerData = mediaItemTrackData, + blockedTimeRanges = blockedTimeRangeList + ) + val expectedTrackGroup = TrackGroupArray( + TrackGroup(createDummyFormat("DummyId")), + TrackGroup( + "Pillarbox-Trackers", + Format.Builder() + .setId("TrackerData:0") + .setSampleMimeType(PILLARBOX_TRACKERS_MIME_TYPE) + .setCustomData(mediaItemTrackData) + .build() + ) + ) + mediaPeriod.prepare(mockk(relaxed = true), 0) + assert(mediaPeriod.trackGroups == expectedTrackGroup) + } + + @Test + fun `test track group with blocked time range only`() { + val mediaItemTrackData = MediaItemTrackerData.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) + assert(mediaPeriod.trackGroups == expectedTrackGroup) + } + + 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") + ) + ) + } + } +} From 09ea82d35f1983e2c054008bcecf8dc9df5c8389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Thu, 19 Sep 2024 09:42:48 +0200 Subject: [PATCH 05/32] Update doc --- .../pillarbox/player/tracker/AnalyticsMediaItemTracker.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 36e7e1c49..60cad7e99 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 @@ -18,7 +18,7 @@ import kotlin.time.Duration.Companion.milliseconds /** * Tracks [Player.getCurrentTracks] to handle [MediaItemTrackerData] changes. - * @param player The [Player] whose current [MediaItem] is tracked for analytics. + * @param player The [Player] whose current [Tracks] is tracked for analytics. * @param mediaItemTrackerProvider The [MediaItemTrackerProvider] that provide new instance of [MediaItemTracker]. */ internal class AnalyticsMediaItemTracker( @@ -52,7 +52,6 @@ internal class AnalyticsMediaItemTracker( if (field == value) { return } - field = value if (field) { currentMediaItemTrackerData = player.currentTracks.getPillarboxDataOrNull()?.let { @@ -82,7 +81,7 @@ internal class AnalyticsMediaItemTracker( positionMs: Long = player.currentPosition, ) { if (trackers.isEmpty()) return - DebugLogger.info(TAG, "Stop trackers $stopReason @${positionMs.milliseconds}") + DebugLogger.info(TAG, "Stop session $stopReason @${positionMs.milliseconds}") for (tracker in trackers) { tracker.stop(player, stopReason, positionMs) } @@ -96,8 +95,6 @@ internal class AnalyticsMediaItemTracker( require(trackers.isEmpty()) DebugLogger.info(TAG, "Start new session for ${player.currentMediaItem?.prettyString()}") - - // Create each tracker for this new MediaItem val trackers = data.trackers .map { trackerType -> mediaItemTrackerProvider.getMediaItemTrackerFactory(trackerType).create() From 2ffa69b190ebdc09eb1708d0e9da73938d3fccef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Thu, 19 Sep 2024 10:56:49 +0200 Subject: [PATCH 06/32] Remove remaining PillarboxData references --- .../ch/srgssr/pillarbox/player/asset/Asset.kt | 2 +- .../pillarbox/player/extension/Tracks.kt | 2 +- .../tracker/AnalyticsMediaItemTracker.kt | 10 +++--- .../pillarbox/player/extension/PlayerTest.kt | 33 ++++++++++++++----- .../player/tracker/MediaItemTrackerTest.kt | 14 ++++---- 5 files changed, 38 insertions(+), 23 deletions(-) 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..01de53340 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 @@ -13,7 +13,7 @@ import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData * 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. */ 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 e8a6a8b00..86a7a7c42 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 @@ -101,7 +101,7 @@ internal fun Tracks.Group.filterBy(predicate: (Tracks.Group, Int) -> Boolean): T /** * @return [MediaItemTrackerData] if it exists, `null` otherwise */ -fun Tracks.getPillarboxDataOrNull(): MediaItemTrackerData? { +fun Tracks.getMediaItemTrackerDataOrNull(): MediaItemTrackerData? { return groups.firstOrNull { it.type == PillarboxMediaSource.TRACK_TYPE_PILLARBOX_TRACKERS }?.getTrackFormat(0)?.customData as? MediaItemTrackerData 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 60cad7e99..86ce86d7c 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 @@ -10,7 +10,7 @@ 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.extension.getPillarboxDataOrNull +import ch.srgssr.pillarbox.player.extension.getMediaItemTrackerDataOrNull import ch.srgssr.pillarbox.player.tracker.MediaItemTracker.StopReason import ch.srgssr.pillarbox.player.utils.DebugLogger import ch.srgssr.pillarbox.player.utils.StringUtil @@ -34,7 +34,7 @@ internal class AnalyticsMediaItemTracker( private var currentMediaItemTrackerData: MediaItemTrackerData? = null set(value) { if (field !== value) { - DebugLogger.info(TAG, "onPillarboxDataChanged $field -> $value") + DebugLogger.info(TAG, "currentMediaItemTrackerData $field -> $value") stopSession(StopReason.Stop) player.removeAnalyticsListener(listener) field = value @@ -54,7 +54,7 @@ internal class AnalyticsMediaItemTracker( } field = value if (field) { - currentMediaItemTrackerData = player.currentTracks.getPillarboxDataOrNull()?.let { + currentMediaItemTrackerData = player.currentTracks.getMediaItemTrackerDataOrNull()?.let { startNewSession(data = it) it } @@ -65,11 +65,11 @@ internal class AnalyticsMediaItemTracker( init { player.addListener(this) - currentMediaItemTrackerData = player.currentTracks.getPillarboxDataOrNull() + currentMediaItemTrackerData = player.currentTracks.getMediaItemTrackerDataOrNull() } override fun onTracksChanged(tracks: Tracks) { - currentMediaItemTrackerData = tracks.getPillarboxDataOrNull() + currentMediaItemTrackerData = tracks.getMediaItemTrackerDataOrNull() } fun release() { 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 a7a82fcac..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,6 +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.timeRange.Chapter import ch.srgssr.pillarbox.player.asset.timeRange.Credit import io.mockk.every import io.mockk.mockk @@ -89,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() } @@ -107,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(Any()) + .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/MediaItemTrackerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/MediaItemTrackerTest.kt index 4ab2a40c9..ae1b664c1 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,7 @@ 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.extension.getMediaItemTrackerDataOrNull import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import io.mockk.clearAllMocks import io.mockk.confirmVerified @@ -284,7 +284,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) // Wait for MediaItemSource to be loaded RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull() != null + player.currentTracks.getMediaItemTrackerDataOrNull() != null } val currentMediaItem = player.currentMediaItem!! val mediaUpdate = currentMediaItem.buildUpon() @@ -310,7 +310,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull() != null + player.currentTracks.getMediaItemTrackerDataOrNull() != null } val mediaItem = player.currentMediaItem assertNotNull(mediaItem) @@ -341,7 +341,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull()?.getData(fakeMediaItemTracker) == FakeMediaItemTracker.Data( + player.currentTracks.getMediaItemTrackerDataOrNull()?.getData(fakeMediaItemTracker) == FakeMediaItemTracker.Data( FakeAssetLoader .MEDIA_ID_1 ) @@ -350,7 +350,7 @@ class MediaItemTrackerTest { player.replaceMediaItem(0, FakeAssetLoader.MEDIA_2) TestPlayerRunHelper.runUntilTimelineChanged(player) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull()?.getData(fakeMediaItemTracker) == FakeMediaItemTracker.Data( + player.currentTracks.getMediaItemTrackerDataOrNull()?.getData(fakeMediaItemTracker) == FakeMediaItemTracker.Data( FakeAssetLoader.MEDIA_ID_2 ) } @@ -404,7 +404,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull() != null + player.currentTracks.getMediaItemTrackerDataOrNull() != null } player.seekToNextMediaItem() @@ -433,7 +433,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getPillarboxDataOrNull() != null + player.currentTracks.getMediaItemTrackerDataOrNull() != null } player.seekToPreviousMediaItem() From 415e131f6940714e43e42db39bbe5a32c059a88d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Fri, 20 Sep 2024 17:13:15 +0200 Subject: [PATCH 07/32] Revisit MediaItemTracker --- .../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 | 44 ++++--- .../DefaultMediaItemTrackerRepository.kt | 81 ------------- .../business/tracker/SRGEventLoggerTracker.kt | 14 +-- .../commandersact/CommandersActTracker.kt | 93 ++++++++++++-- .../tracker/comscore/ComScoreTracker.kt | 16 ++- .../DefaultMediaItemTrackerRepositoryTest.kt | 51 -------- .../tracker/SRGEventLoggerTrackerTest.kt | 5 +- .../CommandersActTrackerIntegrationTest.kt | 36 +++--- .../commandersact/CommandersActTrackerTest.kt | 30 +---- .../ComScoreTrackerIntegrationTest.kt | 42 +++++-- .../tracker/comscore/ComScoreTrackerTest.kt | 29 +---- .../pillarbox/demo/shared/di/PlayerModule.kt | 2 - .../pillarbox/player/PillarboxExoPlayer.kt | 9 +- .../ch/srgssr/pillarbox/player/asset/Asset.kt | 3 +- .../pillarbox/player/asset/UrlAssetLoader.kt | 31 +---- .../player/source/PillarboxMediaPeriod.kt | 2 +- .../player/source/PillarboxMediaSource.kt | 3 +- .../tracker/AnalyticsMediaItemTracker.kt | 114 ++++-------------- .../player/tracker/MediaItemTracker.kt | 37 ++---- .../player/tracker/MediaItemTrackerData.kt | 112 +++-------------- .../player/tracker/MediaItemTrackerList.kt | 72 ----------- .../tracker/MediaItemTrackerProvider.kt | 20 --- .../tracker/MediaItemTrackerRepository.kt | 30 ----- .../player/PillarboxMediaPeriodTest.kt | 26 ++-- .../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 | 51 ++++---- 34 files changed, 329 insertions(+), 957 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-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 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..5aeabecc2 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 @@ -47,12 +52,20 @@ const val MimeTypeSrg = "${MimeTypes.BASE_TYPE_APPLICATION}/srg-ssr" * @param context The context. * @param mediaCompositionService The service to load a [MediaComposition]. */ -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))) ) { + + constructor( + context: Context, + mediaCompositionService: MediaCompositionService = HttpMediaCompositionService(), + ) : this(context, mediaCompositionService, SRGAnalytics.commandersAct, Dispatchers.Default) + /** * An interface to customize how [SRGAssetLoader] should fill [MediaMetadata]. */ @@ -80,13 +93,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 @@ -139,16 +152,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 +168,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/CommandersActTracker.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTracker.kt index 7f9242d2d..8081483a7 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 @@ -4,7 +4,10 @@ */ package ch.srgssr.pillarbox.core.business.tracker.commandersact +import androidx.media3.common.Player +import androidx.media3.common.Player.PositionInfo import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.analytics.AnalyticsListener import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct import ch.srgssr.pillarbox.player.tracker.MediaItemTracker import kotlin.coroutines.CoroutineContext @@ -21,7 +24,24 @@ import kotlin.time.Duration.Companion.milliseconds class CommandersActTracker( private val commandersAct: CommandersAct, private val coroutineContext: CoroutineContext, -) : MediaItemTracker { +) : MediaItemTracker, AnalyticsListener { + + /** + * 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 + } + /** * Data for CommandersAct * @@ -29,33 +49,38 @@ class CommandersActTracker( * @property sourceId TBD */ data class Data(val assets: Map, val sourceId: String? = null) - private var analyticsStreaming: CommandersActStreaming? = null - private var currentData: Data? = null + private lateinit var currentData: Data + private lateinit var player: ExoPlayer - override fun start(player: ExoPlayer, initialData: Any?) { - requireNotNull(initialData) - require(initialData is Data) + override fun start(player: ExoPlayer, data: Data) { + require(analyticsStreaming == null) { "AnalyticsStreaming already start" } + this.player = player commandersAct.enableRunningInBackground() - currentData = initialData + currentData = data analyticsStreaming = CommandersActStreaming( commandersAct = commandersAct, player = player, - currentData = initialData, + currentData = data, coroutineContext = coroutineContext, ) + player.addAnalyticsListener(this) analyticsStreaming?.let { player.addAnalyticsListener(it) } } - override fun stop(player: ExoPlayer, reason: MediaItemTracker.StopReason, positionMs: Long) { + override fun stop(player: ExoPlayer) { + player.removeAnalyticsListener(this) + stop(StopReason.Stop, player.currentPosition) + } + + private fun stop(reason: StopReason, positionMs: Long) { analyticsStreaming?.let { player.removeAnalyticsListener(it) - it.notifyStop(position = positionMs.milliseconds, reason == MediaItemTracker.StopReason.EoF) + it.notifyStop(position = positionMs.milliseconds, reason == StopReason.EoF) } analyticsStreaming = null - currentData = null } /** @@ -64,9 +89,51 @@ 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) } } + + override fun onPlaybackStateChanged( + eventTime: AnalyticsListener.EventTime, + @Player.State playbackState: Int, + ) { + when (playbackState) { + Player.STATE_ENDED -> stop(StopReason.EoF, player.currentPosition) + Player.STATE_IDLE -> stop(StopReason.Stop, player.currentPosition) + Player.STATE_READY -> { + if (analyticsStreaming == null) { + start(player, currentData) + } + } + + else -> Unit + } + } + + /* + * On position discontinuity handle stop session if required + */ + override fun onPositionDiscontinuity( + eventTime: AnalyticsListener.EventTime, + oldPosition: PositionInfo, + newPosition: PositionInfo, + @Player.DiscontinuityReason reason: Int, + ) { + val oldPositionMs = oldPosition.positionMs + when (reason) { + Player.DISCONTINUITY_REASON_REMOVE -> stop(StopReason.Stop, oldPositionMs) + Player.DISCONTINUITY_REASON_AUTO_TRANSITION -> { + stop(StopReason.EoF, oldPositionMs) + start(player, currentData) + } + + else -> { + if (oldPosition.mediaItemIndex != newPosition.mediaItemIndex) { + stop(StopReason.Stop, oldPositionMs) + } + } + } + } } 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..98e2de4eb 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() } @@ -223,13 +221,13 @@ class ComScoreTracker internal constructor( /** * Factory */ - class Factory : MediaItemTracker.Factory { - override fun create(): MediaItemTracker { + class Factory : MediaItemTracker.Factory { + override fun create(): ComScoreTracker { return ComScoreTracker() } } - private companion object { + companion object { private const val MEDIA_PLAYER_NAME = "Pillarbox" private const val TAG = "ComScoreTracker" private const val LIVE_ONLY_WINDOW_OFFSET = 0L 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/CommandersActTrackerIntegrationTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt index e54e8e2da..8879035a9 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,13 +22,12 @@ 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 @@ -71,24 +70,23 @@ class CommandersActTrackerIntegrationTest { testDispatcher = UnconfinedTestDispatcher() val context = ApplicationProvider.getApplicationContext() - val mediaItemTrackerRepository = DefaultMediaItemTrackerRepository( - trackerRepository = MediaItemTrackerRepository(), - commandersAct = commandersAct, - coroutineContext = testDispatcher, - ) - mediaItemTrackerRepository.registerFactory(ComScoreTracker::class.java) { - mockk(relaxed = true) - } - val mediaCompositionWithFallbackService = LocalMediaCompositionWithFallbackService(context) - player = DefaultPillarbox( - context = context, - mediaItemTrackerRepository = mediaItemTrackerRepository, + val assetLoader = SRGAssetLoader( + context, mediaCompositionService = mediaCompositionWithFallbackService, - clock = clock, - // Use other CoroutineContext to avoid infinite loop because Heartbeat is also running in Pillarbox. - coroutineContext = EmptyCoroutineContext, + commandersAct = commandersAct, + coroutineContext = testDispatcher ) + player = + PillarboxExoPlayer( + context = context, + mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { + addAssetLoader(assetLoader) + }, + clock = clock, + // Use other CoroutineContext to avoid infinite loop because Heartbeat is also running in Pillarbox. + coroutineContext = EmptyCoroutineContext, + ) } @AfterTest 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 index 657c7adff..a0450154b 100644 --- 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 @@ -10,7 +10,6 @@ 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 @@ -26,29 +25,6 @@ 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`() { @@ -62,7 +38,7 @@ class CommandersActTrackerTest { commandersActTracker.start( player = player, - initialData = CommandersActTracker.Data(emptyMap()), + data = CommandersActTracker.Data(emptyMap()), ) verify { @@ -77,9 +53,7 @@ class CommandersActTrackerTest { val commandersActStreaming = commandersActStreamingSlot.captured commandersActTracker.stop( - player = player, - reason = MediaItemTracker.StopReason.EoF, - positionMs = 30.seconds.inWholeMilliseconds, + player = player ) verify { 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..aa03dc5eb 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() + } + }) + } + player = PillarboxExoPlayer( context = ApplicationProvider.getApplicationContext(), - mediaItemTrackerRepository = mediaItemTrackerRepository, - mediaCompositionService = mediaCompositionWithFallbackService, + mediaSourceFactory = mediaSourceFactory, clock = clock, coroutineContext = EmptyCoroutineContext, ) 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 ccdf19b48..a1a182560 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 @@ -41,8 +41,6 @@ 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.MediaItemTrackerProvider -import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository import ch.srgssr.pillarbox.player.tracker.PillarboxMediaMetaDataTracker import ch.srgssr.pillarbox.player.utils.PillarboxEventLogger import kotlinx.coroutines.CoroutineScope @@ -69,7 +67,6 @@ class PillarboxExoPlayer internal constructor( context: Context, coroutineContext: CoroutineContext, private val exoPlayer: ExoPlayer, - mediaItemTrackerProvider: MediaItemTrackerProvider, analyticsCollector: PillarboxAnalyticsCollector, private val metricsCollector: MetricsCollector = MetricsCollector(), monitoringMessageHandler: MonitoringMessageHandler, @@ -77,7 +74,7 @@ class PillarboxExoPlayer internal constructor( private val listeners = ListenerSet(applicationLooper, clock) { listener, flags -> listener.onEvents(this, Player.Events(flags)) } - private val analyticsTracker = AnalyticsMediaItemTracker(this, mediaItemTrackerProvider) + private val analyticsTracker = AnalyticsMediaItemTracker(this) internal val sessionManager = PlaybackSessionManager() private val window = Window() @@ -140,7 +137,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, @@ -157,7 +153,6 @@ class PillarboxExoPlayer internal constructor( context = context, mediaSourceFactory = mediaSourceFactory, loadControl = loadControl, - mediaItemTrackerProvider = mediaItemTrackerProvider, seekIncrement = seekIncrement, maxSeekToPreviousPosition = maxSeekToPreviousPosition, clock = Clock.DEFAULT, @@ -170,7 +165,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, @@ -205,7 +199,6 @@ class PillarboxExoPlayer internal constructor( .setAnalyticsCollector(analyticsCollector) .setDeviceVolumeControlEnabled(true) // allow player to control device volume .build(), - mediaItemTrackerProvider = mediaItemTrackerProvider, analyticsCollector = analyticsCollector, metricsCollector = metricsCollector, monitoringMessageHandler = monitoringMessageHandler, 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 01de53340..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,6 +8,7 @@ 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 @@ -19,7 +20,7 @@ import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData */ 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/UrlAssetLoader.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/UrlAssetLoader.kt index de73392f6..bee027b08 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 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/source/PillarboxMediaPeriod.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/source/PillarboxMediaPeriod.kt index ca27b5c8f..fc43f3ce9 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 @@ -20,7 +20,7 @@ internal class PillarboxMediaPeriod( blockedTimeRanges: List, ) : MediaPeriod by mediaPeriod { private val pillarboxTracks = mutableListOf().apply { - if (mediaItemTrackerData.isNotEmpty) { + if (mediaItemTrackerData.isNotEmpty()) { add( TrackGroup( "Pillarbox-Trackers", 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 9e984f8ae..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 @@ -23,6 +23,7 @@ import androidx.media3.exoplayer.upstream.Allocator import ch.srgssr.pillarbox.player.asset.AssetLoader 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 @@ -49,7 +50,7 @@ class PillarboxMediaSource internal constructor( private val eventDispatcher by lazy { createEventDispatcher(null) } private var loadTaskId = 0L private var timeMarkLoadStart: TimeMark? = null - private var mediaItemTrackerData: MediaItemTrackerData = MediaItemTrackerData.EMPTY + private var mediaItemTrackerData: MediaItemTrackerData = MutableMediaItemTrackerData.EMPTY.toMediaItemTrackerData() private var blockedTimeRanges: List = emptyList() @Suppress("TooGenericExceptionCaught") 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 86ce86d7c..f6b750803 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,41 +6,31 @@ 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.extension.getMediaItemTrackerDataOrNull -import ch.srgssr.pillarbox.player.tracker.MediaItemTracker.StopReason import ch.srgssr.pillarbox.player.utils.DebugLogger -import ch.srgssr.pillarbox.player.utils.StringUtil -import kotlin.time.Duration.Companion.milliseconds /** * Tracks [Player.getCurrentTracks] to handle [MediaItemTrackerData] changes. * @param player The [Player] whose current [Tracks] is tracked for analytics. - * @param mediaItemTrackerProvider The [MediaItemTrackerProvider] that provide new instance of [MediaItemTracker]. */ internal class AnalyticsMediaItemTracker( private val player: ExoPlayer, - private val mediaItemTrackerProvider: MediaItemTrackerProvider, ) : Player.Listener { - private val listener = CurrentMediaItemListener() /** * Trackers are empty if the tracking session is stopped. */ - private var trackers = MediaItemTrackerList() + private var trackers = mutableListOf>() private var currentMediaItemTrackerData: MediaItemTrackerData? = null set(value) { if (field !== value) { DebugLogger.info(TAG, "currentMediaItemTrackerData $field -> $value") - stopSession(StopReason.Stop) - player.removeAnalyticsListener(listener) + stopSession() field = value field?.let { - if (it.isNotEmpty) { - player.addAnalyticsListener(listener) + if (it.isNotEmpty()) { startNewSession(it) } } @@ -53,13 +43,10 @@ internal class AnalyticsMediaItemTracker( return } field = value - if (field) { - currentMediaItemTrackerData = player.currentTracks.getMediaItemTrackerDataOrNull()?.let { - startNewSession(data = it) - it - } + currentMediaItemTrackerData = if (field) { + player.currentTracks.getMediaItemTrackerDataOrNull() } else { - stopSession(StopReason.Stop) + null } } @@ -76,88 +63,27 @@ internal class AnalyticsMediaItemTracker( currentMediaItemTrackerData = null } - private fun stopSession( - stopReason: StopReason, - positionMs: Long = player.currentPosition, - ) { + private fun stopSession() { if (trackers.isEmpty()) return - DebugLogger.info(TAG, "Stop session $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: MediaItemTrackerData) { - if (!enabled || data.trackers.isEmpty()) { + if (!enabled || data.isEmpty()) { return } require(trackers.isEmpty()) - DebugLogger.info(TAG, "Start new session for ${player.currentMediaItem?.prettyString()}") - val trackers = data.trackers - .map { trackerType -> - mediaItemTrackerProvider.getMediaItemTrackerFactory(trackerType).create() - .also { it.start(player, data.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() && currentMediaItemTrackerData != null) { - startNewSession(data = currentMediaItemTrackerData!!) - } - } - - 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) { - currentMediaItemTrackerData?.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) } } + this.trackers.addAll(delegates) } private companion object { @@ -167,3 +93,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/MediaItemTracker.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTracker.kt index deeae616f..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,50 +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. + * @param player The player tracked. The current player state may reflect the next item. */ - fun stop(player: ExoPlayer, reason: StopReason, positionMs: Long) + 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..e0b2e0b9f 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 @@ -4,108 +4,30 @@ */ package ch.srgssr.pillarbox.player.tracker +class FactoryData(val factory: MediaItemTracker.Factory, val data: T) + /** - * Immutable MediaItem tracker data. + * Mutable MediaItem tracker data. + * + * @constructor Create empty Mutable media item tracker data */ -class MediaItemTrackerData private constructor(private val map: Map, Any?>) { - - /** - * List of tracker class that have data. - */ - val trackers: Collection> - get() { - return map.keys - } - +class MutableMediaItemTrackerData : MutableMap> by mutableMapOf() { /** - * Is empty + * To media item tracker data */ - 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] - } - - /** - * Build upon - * - * @return A builder filled with current data. - */ - fun buildUpon(): Builder = Builder(this) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false + fun toMediaItemTrackerData() = MediaItemTrackerData(this) - other as MediaItemTrackerData - - return map == other.map - } - - override fun hashCode(): Int { - return map.hashCode() - } - - override fun toString(): String { - return "MediaItemTrackerData(map=$map)" - } - - @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 index 9b83d296d..1e7a002fd 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt @@ -13,8 +13,10 @@ 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 @@ -24,7 +26,7 @@ class PillarboxMediaPeriodTest { @Test fun `test track group with no tracker data and no blocked time range`() { - val mediaItemTrackData = MediaItemTrackerData.EMPTY + val mediaItemTrackData = MediaItemTrackerData(MutableMediaItemTrackerData.EMPTY) val blockedTimeRangeList = emptyList() val mediaPeriod = PillarboxMediaPeriod( mediaPeriod = createFakeChildMediaPeriod(), @@ -40,13 +42,13 @@ class PillarboxMediaPeriodTest { @Test fun `test track group with tracker data and blocked time range`() { - val mediaItemTrackData = MediaItemTrackerData.Builder() - .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data("TEST01")) - .build() + 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 = mediaItemTrackData, + mediaItemTrackerData = mediaItemTrackerData, blockedTimeRanges = blockedTimeRangeList ) val expectedTrackGroup = TrackGroupArray( @@ -56,7 +58,7 @@ class PillarboxMediaPeriodTest { Format.Builder() .setId("TrackerData:0") .setSampleMimeType(PILLARBOX_TRACKERS_MIME_TYPE) - .setCustomData(mediaItemTrackData) + .setCustomData(mediaItemTrackerData) .build() ), TrackGroup( @@ -74,13 +76,13 @@ class PillarboxMediaPeriodTest { @Test fun `test track group with tracker data only`() { - val mediaItemTrackData = MediaItemTrackerData.Builder() - .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data("TEST01")) - .build() + 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 = mediaItemTrackData, + mediaItemTrackerData = mediaItemTrackerData, blockedTimeRanges = blockedTimeRangeList ) val expectedTrackGroup = TrackGroupArray( @@ -90,7 +92,7 @@ class PillarboxMediaPeriodTest { Format.Builder() .setId("TrackerData:0") .setSampleMimeType(PILLARBOX_TRACKERS_MIME_TYPE) - .setCustomData(mediaItemTrackData) + .setCustomData(mediaItemTrackerData) .build() ) ) @@ -100,7 +102,7 @@ class PillarboxMediaPeriodTest { @Test fun `test track group with blocked time range only`() { - val mediaItemTrackData = MediaItemTrackerData.EMPTY + val mediaItemTrackData = MediaItemTrackerData(MutableMediaItemTrackerData.EMPTY) val blockedTimeRangeList = listOf(BlockedTimeRange(0L, 100L), BlockedTimeRange(200L, 300L)) val mediaPeriod = PillarboxMediaPeriod( mediaPeriod = createFakeChildMediaPeriod(), 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 ae1b664c1..1326b0086 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 @@ -51,9 +51,8 @@ class MediaItemTrackerTest { clock = fakeClock, coroutineContext = EmptyCoroutineContext, mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { - addAssetLoader(FakeAssetLoader(context)) + addAssetLoader(FakeAssetLoader(context, fakeMediaItemTracker)) }, - mediaItemTrackerProvider = FakeTrackerProvider(fakeMediaItemTracker) ) } @@ -80,7 +79,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 +101,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 +124,9 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.EoF, player.currentPosition) + } + verify(exactly = 0) { + fakeMediaItemTracker.stop(any()) } confirmVerified(fakeMediaItemTracker) } @@ -144,11 +145,14 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.Stop, player.currentPosition) + } + verify(exactly = 0) { + fakeMediaItemTracker.stop(any()) } confirmVerified(fakeMediaItemTracker) } + // FIXME : Should be move to CommandersAct as it is internal behaviors @Test fun `one MediaItem reach eof then seek back`() { val mediaItem = FakeAssetLoader.MEDIA_1 @@ -169,7 +173,7 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.EoF, player.duration) + fakeMediaItemTracker.stop(any()) fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) } confirmVerified(fakeMediaItemTracker) @@ -195,7 +199,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 +222,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 +244,7 @@ class MediaItemTrackerTest { verifyAll { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.Stop, any()) + fakeMediaItemTracker.stop(any()) } confirmVerified(fakeMediaItemTracker) } @@ -265,7 +269,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) @@ -341,29 +345,25 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getMediaItemTrackerDataOrNull()?.getData(fakeMediaItemTracker) == FakeMediaItemTracker.Data( - FakeAssetLoader - .MEDIA_ID_1 - ) + player.currentTracks.getMediaItemTrackerDataOrNull() != null } player.replaceMediaItem(0, FakeAssetLoader.MEDIA_2) TestPlayerRunHelper.runUntilTimelineChanged(player) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getMediaItemTrackerDataOrNull()?.getData(fakeMediaItemTracker) == FakeMediaItemTracker.Data( - FakeAssetLoader.MEDIA_ID_2 - ) + player.currentTracks.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) } + // FIXME : Should be move to CommandersAct as it is internal behaviors @Test fun `auto transition to next item stop current tracker`() { val firstMediaId = FakeAssetLoader.MEDIA_ID_1 @@ -384,9 +384,9 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(player, FakeMediaItemTracker.Data(firstMediaId)) - fakeMediaItemTracker.stop(player, MediaItemTracker.StopReason.EoF, any()) + fakeMediaItemTracker.stop(player) fakeMediaItemTracker.start(player, FakeMediaItemTracker.Data(secondMediaId)) - fakeMediaItemTracker.stop(player, MediaItemTracker.StopReason.EoF, any()) + fakeMediaItemTracker.stop(player) } confirmVerified(fakeMediaItemTracker) } @@ -415,7 +415,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) @@ -444,12 +444,13 @@ 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) } + // FIXME : Should be move to CommandersAct as it is internal behaviors @Test fun `repeat current item stop with EoF when start again`() { val firstMediaId = FakeAssetLoader.MEDIA_ID_1 @@ -472,9 +473,9 @@ class MediaItemTrackerTest { verifyAll { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(firstMediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.EoF, any()) + fakeMediaItemTracker.stop(any()) fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(firstMediaId)) - fakeMediaItemTracker.stop(any(), MediaItemTracker.StopReason.Stop, any()) + fakeMediaItemTracker.stop(any()) } confirmVerified(fakeMediaItemTracker) } From d39069b4e3fa71d2135d964042103ee24d4236f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Mon, 23 Sep 2024 16:11:32 +0200 Subject: [PATCH 08/32] Update tests --- .../core/business/SRGAssetLoaderTest.kt | 82 ++++++++++++++++++ .../CommandersActTrackerIntegrationTest.kt | 82 ++++++++++++++++++ .../commandersact/CommandersActTrackerTest.kt | 75 ---------------- .../ComScoreTrackerIntegrationTest.kt | 42 ++++++--- .../player/tracker/MediaItemTrackerTest.kt | 85 ------------------- 5 files changed, 192 insertions(+), 174 deletions(-) delete mode 100644 pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.kt 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/commandersact/CommandersActTrackerIntegrationTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt index 8879035a9..8ba98d3be 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 @@ -34,6 +34,7 @@ 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 @@ -847,6 +848,8 @@ class CommandersActTrackerIntegrationTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) + verifyOrder { commandersAct.enableRunningInBackground() commandersAct.sendTcMediaEvent(capture(tcMediaEvents)) @@ -861,6 +864,85 @@ class CommandersActTrackerIntegrationTest { assertTrue(tcMediaEvents.all { it.sourceId == null }) } + @Test + fun `repeat current item stop with EoF when start again`() { + 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 stop current tracker`() { + 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`() { + 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 a0450154b..000000000 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.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.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 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 - 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, - data = 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 - ) - - 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 aa03dc5eb..62462a296 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 @@ -77,7 +77,7 @@ class ComScoreTrackerIntegrationTest { val asset = srgAssetLoader.loadAsset(mediaItem) val mediaItemTracker = MutableMediaItemTrackerData() mediaItemTracker["FakeComScore"] = FactoryData(comScoreFactory, ComScoreTracker.Data(emptyMap())) - return asset.copy() + return asset.copy(trackersData = mediaItemTracker.toMediaItemTrackerData()) } }) } @@ -136,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)) @@ -330,7 +319,7 @@ class ComScoreTrackerIntegrationTest { verifyPlaybackRate(playbackRate = 1f) verifyBufferEvents() verifyPlayEvent() - verifyEndEvent() + verifyPauseEvent() } confirmVerified(streamingAnalytics) } @@ -548,6 +537,32 @@ class ComScoreTrackerIntegrationTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_IDLE) + verifyOrder { + verifyPlayerInformation() + verifyCreatePlaybackSession() + verifyMetadata() + verifyPlaybackRate(playbackRate = 1f) + verifyBufferEvents() + verifySeekEvent(0L) + verifyPlayEvent() + verifyPauseEvent() + } + 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() + + // TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_IDLE) + verifyOrder { verifyPlayerInformation() verifyCreatePlaybackSession() @@ -685,7 +700,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-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 1326b0086..adc5d3c04 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 @@ -152,33 +152,6 @@ class MediaItemTrackerTest { confirmVerified(fakeMediaItemTracker) } - // FIXME : Should be move to CommandersAct as it is internal behaviors - @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()) - fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) - } - confirmVerified(fakeMediaItemTracker) - } - @Test fun `item seek to another item stop current tracker and start the other`() { val firstMediaId = FakeAssetLoader.MEDIA_ID_1 @@ -363,34 +336,6 @@ class MediaItemTrackerTest { confirmVerified(fakeMediaItemTracker) } - // FIXME : Should be move to CommandersAct as it is internal behaviors - @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) - fakeMediaItemTracker.start(player, FakeMediaItemTracker.Data(secondMediaId)) - fakeMediaItemTracker.stop(player) - } - confirmVerified(fakeMediaItemTracker) - } - @Test fun `skip next stop current tracker`() { val firstMediaId = FakeAssetLoader.MEDIA_ID_1 @@ -449,34 +394,4 @@ class MediaItemTrackerTest { } confirmVerified(fakeMediaItemTracker) } - - // FIXME : Should be move to CommandersAct as it is internal behaviors - @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()) - fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(firstMediaId)) - fakeMediaItemTracker.stop(any()) - } - confirmVerified(fakeMediaItemTracker) - } } From 39a06d57c65f57431ef2d6e66fa6f540c6789cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Tue, 24 Sep 2024 08:38:57 +0200 Subject: [PATCH 09/32] Rename tests --- .../commandersact/CommandersActTrackerIntegrationTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 8ba98d3be..52ec12d16 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 @@ -865,7 +865,7 @@ class CommandersActTrackerIntegrationTest { } @Test - fun `repeat current item stop with EoF when start again`() { + fun `repeat current item reset the session`() { val tcMediaEvents = mutableListOf() val firstMediaId = URN_VOD_SHORT player.apply { @@ -891,7 +891,7 @@ class CommandersActTrackerIntegrationTest { } @Test - fun `auto transition to next item stop current tracker`() { + fun `auto transition to next item EoF between items`() { val tcMediaEvents = mutableListOf() val firstMediaId = URN_VOD_SHORT val secondMediaId = URN_VOD_SHORT @@ -918,7 +918,7 @@ class CommandersActTrackerIntegrationTest { } @Test - fun `one MediaItem reach eof then seek back`() { + fun `one MediaItem reach eof then seek back start a new session`() { val tcMediaEvents = mutableListOf() val mediaItem = SRGMediaItemBuilder(URN_VOD_SHORT).build() player.apply { From 76b032dd627c1ce58a6c9444706e48c1e81edeb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Tue, 24 Sep 2024 08:57:07 +0200 Subject: [PATCH 10/32] Add methods to PillarboxExoPlayer too and expose methods --- .../pillarbox/player/PillarboxExoPlayer.kt | 17 +++++++++++++++++ .../srgssr/pillarbox/player/extension/Tracks.kt | 5 ++++- .../player/tracker/AnalyticsMediaItemTracker.kt | 9 +++++---- .../player/tracker/MediaItemTrackerTest.kt | 13 ++++++------- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt index a1a182560..4bc33b55c 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,6 +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.MediaItemTrackerData import ch.srgssr.pillarbox.player.tracker.PillarboxMediaMetaDataTracker import ch.srgssr.pillarbox.player.utils.PillarboxEventLogger import kotlinx.coroutines.CoroutineScope @@ -337,6 +340,20 @@ class PillarboxExoPlayer internal constructor( listeners.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?) { when (timeRange) { is Chapter? -> listeners.sendEvent(PillarboxPlayer.EVENT_CHAPTER_CHANGED) { listener -> 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 86a7a7c42..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 @@ -107,8 +107,11 @@ fun Tracks.getMediaItemTrackerDataOrNull(): MediaItemTrackerData? { }?.getTrackFormat(0)?.customData as? MediaItemTrackerData } +/** + * @return a list of [BlockedTimeRange] if it exists, `null` otherwise + */ @Suppress("UNCHECKED_CAST") -internal fun Tracks.getBlockedTimeRangeOrNull(): List? { +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/tracker/AnalyticsMediaItemTracker.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/AnalyticsMediaItemTracker.kt index f6b750803..0ce981bf8 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 @@ -8,6 +8,7 @@ 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.PillarboxExoPlayer import ch.srgssr.pillarbox.player.extension.getMediaItemTrackerDataOrNull import ch.srgssr.pillarbox.player.utils.DebugLogger @@ -16,7 +17,7 @@ import ch.srgssr.pillarbox.player.utils.DebugLogger * @param player The [Player] whose current [Tracks] is tracked for analytics. */ internal class AnalyticsMediaItemTracker( - private val player: ExoPlayer, + private val player: PillarboxExoPlayer, ) : Player.Listener { /** @@ -44,7 +45,7 @@ internal class AnalyticsMediaItemTracker( } field = value currentMediaItemTrackerData = if (field) { - player.currentTracks.getMediaItemTrackerDataOrNull() + player.getMediaItemTrackerDataOrNull() } else { null } @@ -52,7 +53,7 @@ internal class AnalyticsMediaItemTracker( init { player.addListener(this) - currentMediaItemTrackerData = player.currentTracks.getMediaItemTrackerDataOrNull() + currentMediaItemTrackerData = player.getMediaItemTrackerDataOrNull() } override fun onTracksChanged(tracks: Tracks) { @@ -83,7 +84,7 @@ internal class AnalyticsMediaItemTracker( this.start(player, Unit) } } - this.trackers.addAll(delegates) + trackers.addAll(delegates) } private companion object { 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 adc5d3c04..edde17c2c 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.getMediaItemTrackerDataOrNull import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import io.mockk.clearAllMocks import io.mockk.confirmVerified @@ -261,7 +260,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) // Wait for MediaItemSource to be loaded RobolectricUtil.runMainLooperUntil { - player.currentTracks.getMediaItemTrackerDataOrNull() != null + player.getMediaItemTrackerDataOrNull() != null } val currentMediaItem = player.currentMediaItem!! val mediaUpdate = currentMediaItem.buildUpon() @@ -287,7 +286,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getMediaItemTrackerDataOrNull() != null + player.getMediaItemTrackerDataOrNull() != null } val mediaItem = player.currentMediaItem assertNotNull(mediaItem) @@ -318,13 +317,13 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getMediaItemTrackerDataOrNull() != null + player.getMediaItemTrackerDataOrNull() != null } player.replaceMediaItem(0, FakeAssetLoader.MEDIA_2) TestPlayerRunHelper.runUntilTimelineChanged(player) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getMediaItemTrackerDataOrNull() != null + player.getMediaItemTrackerDataOrNull() != null } TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) @@ -349,7 +348,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getMediaItemTrackerDataOrNull() != null + player.getMediaItemTrackerDataOrNull() != null } player.seekToNextMediaItem() @@ -378,7 +377,7 @@ class MediaItemTrackerTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) RobolectricUtil.runMainLooperUntil { - player.currentTracks.getMediaItemTrackerDataOrNull() != null + player.getMediaItemTrackerDataOrNull() != null } player.seekToPreviousMediaItem() From 1807f906250f9bdc3b761a627dae8af62a355461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Tue, 24 Sep 2024 14:18:44 +0200 Subject: [PATCH 11/32] Release first exoplayer to avoid side effects with Listeners --- .../java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt index 4bc33b55c..181cc895d 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 @@ -333,11 +333,11 @@ class PillarboxExoPlayer internal constructor( */ override fun release() { clearSeeking() + exoPlayer.release() + listeners.release() mediaMetadataTracker.release() blockedTimeRangeTracker.release() analyticsTracker.release() - exoPlayer.release() - listeners.release() } /** From 5d2eaa30fa5bdc236f48065417dcf67844d03c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Tue, 24 Sep 2024 17:05:45 +0200 Subject: [PATCH 12/32] Also update with AnalyticsListener --- .../pillarbox/player/tracker/AnalyticsMediaItemTracker.kt | 7 +++++++ 1 file changed, 7 insertions(+) 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 0ce981bf8..c6522032c 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 @@ -8,6 +8,7 @@ import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.Tracks import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.analytics.AnalyticsListener import ch.srgssr.pillarbox.player.PillarboxExoPlayer import ch.srgssr.pillarbox.player.extension.getMediaItemTrackerDataOrNull import ch.srgssr.pillarbox.player.utils.DebugLogger @@ -50,9 +51,15 @@ internal class AnalyticsMediaItemTracker( null } } + private val analyticsListener = object : AnalyticsListener { + override fun onTracksChanged(eventTime: AnalyticsListener.EventTime, tracks: Tracks) { + currentMediaItemTrackerData = tracks.getMediaItemTrackerDataOrNull() + } + } init { player.addListener(this) + player.addAnalyticsListener(analyticsListener) currentMediaItemTrackerData = player.getMediaItemTrackerDataOrNull() } From b73a51ebe7872ca2fdaf34daf711990350d1a03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Tue, 24 Sep 2024 17:07:49 +0200 Subject: [PATCH 13/32] Fix CommandersAct lifecycle --- .../commandersact/CommandersActStreaming.kt | 140 +++++++++++++----- .../commandersact/CommandersActTracker.kt | 84 +---------- 2 files changed, 105 insertions(+), 119 deletions(-) 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 8081483a7..e77f26687 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 @@ -4,14 +4,10 @@ */ package ch.srgssr.pillarbox.core.business.tracker.commandersact -import androidx.media3.common.Player -import androidx.media3.common.Player.PositionInfo import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.analytics.AnalyticsListener 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 @@ -24,23 +20,7 @@ import kotlin.time.Duration.Companion.milliseconds class CommandersActTracker( private val commandersAct: CommandersAct, private val coroutineContext: CoroutineContext, -) : MediaItemTracker, AnalyticsListener { - - /** - * 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 - } +) : MediaItemTracker { /** * Data for CommandersAct @@ -49,36 +29,26 @@ class CommandersActTracker( * @property sourceId TBD */ data class Data(val assets: Map, val sourceId: String? = null) + private var analyticsStreaming: CommandersActStreaming? = null - private lateinit var currentData: Data private lateinit var player: ExoPlayer override fun start(player: ExoPlayer, data: Data) { - require(analyticsStreaming == null) { "AnalyticsStreaming already start" } this.player = player - commandersAct.enableRunningInBackground() - currentData = data analyticsStreaming = CommandersActStreaming( - commandersAct = commandersAct, player = player, + commandersAct = commandersAct, currentData = data, - coroutineContext = coroutineContext, - ) - player.addAnalyticsListener(this) - analyticsStreaming?.let { + coroutineContext = coroutineContext + ).also { player.addAnalyticsListener(it) } } override fun stop(player: ExoPlayer) { - player.removeAnalyticsListener(this) - stop(StopReason.Stop, player.currentPosition) - } - - private fun stop(reason: StopReason, positionMs: Long) { analyticsStreaming?.let { player.removeAnalyticsListener(it) - it.notifyStop(position = positionMs.milliseconds, reason == StopReason.EoF) + it.stop() } analyticsStreaming = null } @@ -94,46 +64,4 @@ class CommandersActTracker( return CommandersActTracker(commandersAct, coroutineContext) } } - - override fun onPlaybackStateChanged( - eventTime: AnalyticsListener.EventTime, - @Player.State playbackState: Int, - ) { - when (playbackState) { - Player.STATE_ENDED -> stop(StopReason.EoF, player.currentPosition) - Player.STATE_IDLE -> stop(StopReason.Stop, player.currentPosition) - Player.STATE_READY -> { - if (analyticsStreaming == null) { - start(player, currentData) - } - } - - else -> Unit - } - } - - /* - * On position discontinuity handle stop session if required - */ - override fun onPositionDiscontinuity( - eventTime: AnalyticsListener.EventTime, - oldPosition: PositionInfo, - newPosition: PositionInfo, - @Player.DiscontinuityReason reason: Int, - ) { - val oldPositionMs = oldPosition.positionMs - when (reason) { - Player.DISCONTINUITY_REASON_REMOVE -> stop(StopReason.Stop, oldPositionMs) - Player.DISCONTINUITY_REASON_AUTO_TRANSITION -> { - stop(StopReason.EoF, oldPositionMs) - start(player, currentData) - } - - else -> { - if (oldPosition.mediaItemIndex != newPosition.mediaItemIndex) { - stop(StopReason.Stop, oldPositionMs) - } - } - } - } } From 80f1d59e2e93de0f3f012d4dba4b4e713752a673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Wed, 25 Sep 2024 13:41:03 +0200 Subject: [PATCH 14/32] Small documentation changes --- .../CommandersActTrackerIntegrationTest.kt | 21 +++++++++---------- .../pillarbox/player/PillarboxExoPlayer.kt | 1 - .../pillarbox/player/asset/UrlAssetLoader.kt | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) 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 52ec12d16..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 @@ -78,16 +78,15 @@ class CommandersActTrackerIntegrationTest { commandersAct = commandersAct, coroutineContext = testDispatcher ) - player = - PillarboxExoPlayer( - context = context, - mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { - addAssetLoader(assetLoader) - }, - clock = clock, - // Use other CoroutineContext to avoid infinite loop because Heartbeat is also running in Pillarbox. - coroutineContext = EmptyCoroutineContext, - ) + player = PillarboxExoPlayer( + context = context, + mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { + addAssetLoader(assetLoader) + }, + clock = clock, + // Use other CoroutineContext to avoid infinite loop because Heartbeat is also running in Pillarbox. + coroutineContext = EmptyCoroutineContext, + ) } @AfterTest @@ -877,7 +876,7 @@ class CommandersActTrackerIntegrationTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) TestPlayerRunHelper.runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION) - player.stop() // Stop player to stop the auto repeat mode + player.stop() // Stop player to stop the auto-repeat mode // Wait on item transition. // Stop otherwise goes crazy. 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 181cc895d..e99648023 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 @@ -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. 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 bee027b08..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 @@ -8,7 +8,7 @@ import androidx.media3.common.MediaItem import androidx.media3.exoplayer.source.DefaultMediaSourceFactory /** - * AssetLoader to load Asset from a stream url. + * [AssetLoader] to load an [Asset] from a stream url. * * @param defaultMediaSourceFactory The [DefaultMediaSourceFactory] to create a MediaSource for the player. */ From e5636d045d4eb41cbccc30222e69c01a1e4229e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Tue, 24 Sep 2024 17:16:35 +0200 Subject: [PATCH 15/32] Don't handle seek to another item --- .../pillarbox/core/business/tracker/comscore/ComScoreTracker.kt | 1 + 1 file changed, 1 insertion(+) 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 98e2de4eb..e4090a9d8 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 @@ -174,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) From 1b6c3d73da2fa53b3b3d86a65c8f696ea9a2556b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 25 Sep 2024 16:57:00 +0200 Subject: [PATCH 16/32] Update pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gaëtan Muller --- .../java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 1e7a002fd..9e392c7f8 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt @@ -37,7 +37,7 @@ class PillarboxMediaPeriodTest { TrackGroup(createDummyFormat("DummyId")) ) mediaPeriod.prepare(mockk(relaxed = true), 0) - assert(mediaPeriod.trackGroups == expectedTrackGroup) + assertEquals(expectedTrackGroup, mediaPeriod.trackGroups) } @Test From 93617715db1bdbe55cd80f21a777c78da035489f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 25 Sep 2024 16:58:12 +0200 Subject: [PATCH 17/32] Update pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/AnalyticsMediaItemTracker.kt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gaëtan Muller --- .../pillarbox/player/tracker/AnalyticsMediaItemTracker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c6522032c..f24bc4347 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 @@ -24,7 +24,7 @@ internal class AnalyticsMediaItemTracker( /** * Trackers are empty if the tracking session is stopped. */ - private var trackers = mutableListOf>() + private val trackers = mutableListOf>() private var currentMediaItemTrackerData: MediaItemTrackerData? = null set(value) { if (field !== value) { From 1aa7b008545b61c20f2ba3fca4a43dbeb0b13255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Wed, 25 Sep 2024 16:46:55 +0200 Subject: [PATCH 18/32] Remove not necessary attribute --- .../core/business/tracker/commandersact/CommandersActTracker.kt | 2 -- 1 file changed, 2 deletions(-) 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 e77f26687..bfafb9af7 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 @@ -31,10 +31,8 @@ class CommandersActTracker( data class Data(val assets: Map, val sourceId: String? = null) private var analyticsStreaming: CommandersActStreaming? = null - private lateinit var player: ExoPlayer override fun start(player: ExoPlayer, data: Data) { - this.player = player analyticsStreaming = CommandersActStreaming( player = player, commandersAct = commandersAct, From 1c8c6fdcdb200b7b0d2096b1efbaf9943edbd284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Wed, 25 Sep 2024 16:49:49 +0200 Subject: [PATCH 19/32] Apply suggestion --- .../ch/srgssr/pillarbox/player/source/PillarboxMediaPeriod.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fc43f3ce9..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 @@ -19,7 +19,7 @@ internal class PillarboxMediaPeriod( mediaItemTrackerData: MediaItemTrackerData, blockedTimeRanges: List, ) : MediaPeriod by mediaPeriod { - private val pillarboxTracks = mutableListOf().apply { + private val pillarboxTracks = buildList { if (mediaItemTrackerData.isNotEmpty()) { add( TrackGroup( From 7737a1f4f3f89c80a0ac2503b1ff282d4da17d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Wed, 25 Sep 2024 16:55:14 +0200 Subject: [PATCH 20/32] Remove wait for state IDLE after a release --- .../business/tracker/comscore/ComScoreTrackerIntegrationTest.kt | 2 -- 1 file changed, 2 deletions(-) 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 62462a296..fd27797df 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 @@ -561,8 +561,6 @@ class ComScoreTrackerIntegrationTest { clock.advanceTime(2.minutes.inWholeMilliseconds) player.release() - // TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_IDLE) - verifyOrder { verifyPlayerInformation() verifyCreatePlaybackSession() From 31fc93990f7e32f8eabdb9bc7c8cd795c1119187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Wed, 25 Sep 2024 17:00:52 +0200 Subject: [PATCH 21/32] Fix suggestion --- .../java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt | 1 + 1 file changed, 1 insertion(+) 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 index 9e392c7f8..fa75f2d62 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt @@ -20,6 +20,7 @@ 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 { From 8e8937640092d6619e3b2cb346206fda7d252bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Wed, 25 Sep 2024 17:03:56 +0200 Subject: [PATCH 22/32] Remove useless verify exactly 0 --- .../srgssr/pillarbox/player/tracker/MediaItemTrackerTest.kt | 6 ------ 1 file changed, 6 deletions(-) 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 edde17c2c..15c325367 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 @@ -124,9 +124,6 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) } - verify(exactly = 0) { - fakeMediaItemTracker.stop(any()) - } confirmVerified(fakeMediaItemTracker) } @@ -145,9 +142,6 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) } - verify(exactly = 0) { - fakeMediaItemTracker.stop(any()) - } confirmVerified(fakeMediaItemTracker) } From 4e9d4470ff76d17729e5a5e0b85e4146d0e10f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Thu, 26 Sep 2024 09:15:52 +0200 Subject: [PATCH 23/32] Fix doc --- .../pillarbox/core/business/source/SRGAssetLoader.kt | 2 ++ .../core/business/tracker/comscore/ComScoreTracker.kt | 2 +- .../pillarbox/player/tracker/MediaItemTrackerData.kt | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) 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 5aeabecc2..8d4bbc872 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 @@ -51,6 +51,8 @@ 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 internal constructor( context: Context, 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 e4090a9d8..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 @@ -228,7 +228,7 @@ class ComScoreTracker internal constructor( } } - companion object { + private companion object { private const val MEDIA_PLAYER_NAME = "Pillarbox" private const val TAG = "ComScoreTracker" private const val LIVE_ONLY_WINDOW_OFFSET = 0L 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 e0b2e0b9f..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 @@ -4,6 +4,13 @@ */ package ch.srgssr.pillarbox.player.tracker +/** + * 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 FactoryData(val factory: MediaItemTracker.Factory, val data: T) /** @@ -17,6 +24,7 @@ class MutableMediaItemTrackerData : MutableMap> by mutableMa */ fun toMediaItemTrackerData() = MediaItemTrackerData(this) + @Suppress("UndocumentedPublicClass") companion object { /** * Empty mutable media item tracker data. From ed9dd8f48ded7b62be31a96625f516b6dc935a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Thu, 26 Sep 2024 10:05:13 +0200 Subject: [PATCH 24/32] Fix missing enabledRunningInBackground --- .../core/business/tracker/commandersact/CommandersActTracker.kt | 1 + 1 file changed, 1 insertion(+) 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 bfafb9af7..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 @@ -33,6 +33,7 @@ class CommandersActTracker( private var analyticsStreaming: CommandersActStreaming? = null override fun start(player: ExoPlayer, data: Data) { + commandersAct.enableRunningInBackground() analyticsStreaming = CommandersActStreaming( player = player, commandersAct = commandersAct, From 6269006ccf2d2cf2fc52a57eeab1b22331a38d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Thu, 26 Sep 2024 10:05:28 +0200 Subject: [PATCH 25/32] Fix mock in test --- .../tracker/commandersact/CommandersActStreamingTest.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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..d96d4a374 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,8 @@ class CommandersActStreamingTest { return mockk { val player = this val looper = ApplicationProvider.getApplicationContext().mainLooper + val timelineWindowDefinition = + FakeTimeline.TimelineWindowDefinition(1, Any(), true, true, true, false, duration * 1000L, C.TIME_UNSET, 0, AdPlaybackState.NONE) every { player.playWhenReady } returns true every { player.isPlaying } returns isPlaying @@ -276,6 +280,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 } } From 94261c97ffa1cdf254af2fb77e74f88df6649757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Thu, 26 Sep 2024 11:34:05 +0200 Subject: [PATCH 26/32] Fix vod tests --- .../commandersact/CommandersActStreamingTest.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 d96d4a374..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 @@ -267,8 +267,18 @@ class CommandersActStreamingTest { return mockk { val player = this val looper = ApplicationProvider.getApplicationContext().mainLooper - val timelineWindowDefinition = - FakeTimeline.TimelineWindowDefinition(1, Any(), true, true, true, false, duration * 1000L, C.TIME_UNSET, 0, AdPlaybackState.NONE) + 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 From 6f3346496c7a5a6ad4b7da822793a89a1431d9d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Thu, 26 Sep 2024 13:16:46 +0200 Subject: [PATCH 27/32] Handle stop with CommandersActTracker --- .../tracker/commandersact/CommandersActStreaming.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 19f6bbbd0..8898f90f6 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 @@ -230,12 +230,18 @@ internal class CommandersActStreaming( @Player.State playbackState: Int, ) { when (playbackState) { - Player.STATE_ENDED -> { - reachEoF = true + Player.STATE_ENDED, Player.STATE_IDLE -> { + reachEoF = playbackState == Player.STATE_ENDED oldPosition = null stop() } + Player.STATE_READY -> { + if (player.playWhenReady) { + notifyPlaying() + } + } + else -> Unit } } From 1d2d842956881433e68fafe8a32b017da1985a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Thu, 26 Sep 2024 13:56:21 +0200 Subject: [PATCH 28/32] MediaItemTracker are stop when player reach Player.STATE_IDLE. --- .../commandersact/CommandersActStreaming.kt | 10 ++-------- .../ComScoreTrackerIntegrationTest.kt | 4 ++-- .../tracker/AnalyticsMediaItemTracker.kt | 20 ++++++++++--------- .../player/tracker/MediaItemTrackerTest.kt | 1 + 4 files changed, 16 insertions(+), 19 deletions(-) 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 8898f90f6..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 @@ -230,18 +230,12 @@ internal class CommandersActStreaming( @Player.State playbackState: Int, ) { when (playbackState) { - Player.STATE_ENDED, Player.STATE_IDLE -> { - reachEoF = playbackState == Player.STATE_ENDED + Player.STATE_ENDED -> { + reachEoF = true oldPosition = null stop() } - Player.STATE_READY -> { - if (player.playWhenReady) { - notifyPlaying() - } - } - else -> Unit } } 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 fd27797df..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 @@ -319,7 +319,7 @@ class ComScoreTrackerIntegrationTest { verifyPlaybackRate(playbackRate = 1f) verifyBufferEvents() verifyPlayEvent() - verifyPauseEvent() + verifyEndEvent() } confirmVerified(streamingAnalytics) } @@ -545,7 +545,7 @@ class ComScoreTrackerIntegrationTest { verifyBufferEvents() verifySeekEvent(0L) verifyPlayEvent() - verifyPauseEvent() + verifyEndEvent() } confirmVerified(streamingAnalytics) } 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 f24bc4347..fad228642 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 @@ -15,11 +15,13 @@ import ch.srgssr.pillarbox.player.utils.DebugLogger /** * Tracks [Player.getCurrentTracks] to handle [MediaItemTrackerData] changes. + * When player is stopped player state = IDLE the MediaPeriod is destroyed and then prepared is called, it will create a session by calling start. + * * @param player The [Player] whose current [Tracks] is tracked for analytics. */ internal class AnalyticsMediaItemTracker( private val player: PillarboxExoPlayer, -) : Player.Listener { +) : AnalyticsListener { /** * Trackers are empty if the tracking session is stopped. @@ -51,22 +53,22 @@ internal class AnalyticsMediaItemTracker( null } } - private val analyticsListener = object : AnalyticsListener { - override fun onTracksChanged(eventTime: AnalyticsListener.EventTime, tracks: Tracks) { - currentMediaItemTrackerData = tracks.getMediaItemTrackerDataOrNull() - } - } init { - player.addListener(this) - player.addAnalyticsListener(analyticsListener) + player.addAnalyticsListener(this) currentMediaItemTrackerData = player.getMediaItemTrackerDataOrNull() } - override fun onTracksChanged(tracks: Tracks) { + 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() + } + } + fun release() { currentMediaItemTrackerData = null } 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 15c325367..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 @@ -141,6 +141,7 @@ class MediaItemTrackerTest { verifyOrder { fakeMediaItemTracker.start(any(), FakeMediaItemTracker.Data(mediaId)) + fakeMediaItemTracker.stop(any()) } confirmVerified(fakeMediaItemTracker) } From 7e93883edf685521a2a6e253e6f0a37bd31dad63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Thu, 26 Sep 2024 16:47:12 +0200 Subject: [PATCH 29/32] use assertEquals instead of == --- .../ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index fa75f2d62..76fbf4120 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/PillarboxMediaPeriodTest.kt @@ -72,7 +72,7 @@ class PillarboxMediaPeriodTest { ) ) mediaPeriod.prepare(mockk(relaxed = true), 0) - assert(mediaPeriod.trackGroups == expectedTrackGroup) + assertEquals(expectedTrackGroup, mediaPeriod.trackGroups) } @Test @@ -98,7 +98,7 @@ class PillarboxMediaPeriodTest { ) ) mediaPeriod.prepare(mockk(relaxed = true), 0) - assert(mediaPeriod.trackGroups == expectedTrackGroup) + assertEquals(expectedTrackGroup, mediaPeriod.trackGroups) } @Test @@ -122,7 +122,7 @@ class PillarboxMediaPeriodTest { ) ) mediaPeriod.prepare(mockk(relaxed = true), 0) - assert(mediaPeriod.trackGroups == expectedTrackGroup) + assertEquals(expectedTrackGroup, mediaPeriod.trackGroups) } companion object { From c850f42ffabecf514a3c54e977e34ba7427225de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Fri, 27 Sep 2024 08:34:26 +0200 Subject: [PATCH 30/32] Fix detect --- .../pillarbox/core/business/source/SRGAssetLoader.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 8d4bbc872..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 @@ -63,11 +63,6 @@ class SRGAssetLoader internal constructor( mediaSourceFactory = DefaultMediaSourceFactory(AkamaiTokenDataSource.Factory(AkamaiTokenProvider(), DefaultDataSource.Factory(context))) ) { - constructor( - context: Context, - mediaCompositionService: MediaCompositionService = HttpMediaCompositionService(), - ) : this(context, mediaCompositionService, SRGAnalytics.commandersAct, Dispatchers.Default) - /** * An interface to customize how [SRGAssetLoader] should fill [MediaMetadata]. */ @@ -120,6 +115,11 @@ class SRGAssetLoader internal constructor( */ 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 From e2e1e3f748b539d460ff9c8ec9af14de5669720e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Fri, 27 Sep 2024 14:06:18 +0200 Subject: [PATCH 31/32] Update pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/AnalyticsMediaItemTracker.kt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gaëtan Muller --- .../pillarbox/player/tracker/AnalyticsMediaItemTracker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fad228642..d95dcf854 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 @@ -15,7 +15,7 @@ import ch.srgssr.pillarbox.player.utils.DebugLogger /** * Tracks [Player.getCurrentTracks] to handle [MediaItemTrackerData] changes. - * When player is stopped player state = IDLE the MediaPeriod is destroyed and then prepared is called, it will create a session by calling start. + * 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 [Tracks] is tracked for analytics. */ From f4eb5a5666ceecc2591a55fe0934acb760522c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20Sta=CC=88hli?= Date: Fri, 27 Sep 2024 14:13:05 +0200 Subject: [PATCH 32/32] lint --- .../pillarbox/player/tracker/AnalyticsMediaItemTracker.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 d95dcf854..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 @@ -15,7 +15,8 @@ import ch.srgssr.pillarbox.player.utils.DebugLogger /** * 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()`. + * 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 [Tracks] is tracked for analytics. */