From d9e96c207465c248848a99a4c5dd31325f7bf5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Fri, 5 Apr 2024 14:39:33 +0200 Subject: [PATCH] Extract analytics from `CurrentMediaItemTracker` (#483) --- .../PillarboxAndroidApplicationPlugin.kt | 2 +- .../business/SRGErrorMessageProviderTest.kt | 10 + .../pillarbox/player/PillarboxPlayer.kt | 25 +- .../pillarbox/player/asset/AssetLoader.kt | 2 +- .../tracker/AnalyticsMediaItemTracker.kt | 222 ++++++++++++ .../tracker/CurrentMediaItemTagTracker.kt | 96 ++++++ .../player/tracker/CurrentMediaItemTracker.kt | 202 ----------- .../player/tracker/MediaItemTrackerList.kt | 71 ++-- ... AnalyticsMediaItemTrackerAreEqualTest.kt} | 36 +- .../tracker/CurrentMediaItemTagTrackerTest.kt | 323 ++++++++++++++++++ .../tracker/MediaItemTrackerListTest.kt | 79 ++--- .../player/tracker/MediaItemTrackerTest.kt | 25 +- 12 files changed, 754 insertions(+), 339 deletions(-) create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/AnalyticsMediaItemTracker.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTagTracker.kt delete mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTracker.kt rename pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/{CurrentMediaItemTrackerAreEqualTest.kt => AnalyticsMediaItemTrackerAreEqualTest.kt} (82%) create mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTagTrackerTest.kt diff --git a/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/PillarboxAndroidApplicationPlugin.kt b/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/PillarboxAndroidApplicationPlugin.kt index 3f9e70290..6f54ec906 100644 --- a/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/PillarboxAndroidApplicationPlugin.kt +++ b/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/PillarboxAndroidApplicationPlugin.kt @@ -55,7 +55,7 @@ class PillarboxAndroidApplicationPlugin : Plugin { } release { - signingConfig = signingConfigs.getByName("release") + signingConfig = signingConfigs.named("release").get() isMinifyEnabled = false isDebuggable = true diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGErrorMessageProviderTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGErrorMessageProviderTest.kt index ccdcb260b..701fbb08c 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGErrorMessageProviderTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGErrorMessageProviderTest.kt @@ -8,6 +8,7 @@ import android.content.Context import androidx.core.util.component1 import androidx.core.util.component2 import androidx.media3.common.PlaybackException +import androidx.media3.datasource.DataSourceException import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import ch.srgssr.pillarbox.core.business.exception.BlockReasonException @@ -67,6 +68,15 @@ class SRGErrorMessageProviderTest { assertEquals(exception.message, errorMessage) } + @Test + fun `getErrorMessage DataSourceException`() { + val exception = DataSourceException(PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND) + val (errorCode, errorMessage) = errorMessageProvider.getErrorMessage(playbackException(exception)) + + assertEquals(PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, errorCode) + assertEquals(exception.message, errorMessage) + } + @Test fun `getErrorMessage IOException`() { val exception = IOException() diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt index fa592d778..b9e5738c1 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt @@ -23,7 +23,8 @@ import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed import ch.srgssr.pillarbox.player.extension.setPreferredAudioRoleFlagsToAccessibilityManagerSettings import ch.srgssr.pillarbox.player.extension.setSeekIncrements import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory -import ch.srgssr.pillarbox.player.tracker.CurrentMediaItemTracker +import ch.srgssr.pillarbox.player.tracker.AnalyticsMediaItemTracker +import ch.srgssr.pillarbox.player.tracker.CurrentMediaItemTagTracker import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository @@ -37,11 +38,11 @@ import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository */ class PillarboxPlayer internal constructor( private val exoPlayer: ExoPlayer, - mediaItemTrackerProvider: MediaItemTrackerProvider? -) : - ExoPlayer by exoPlayer, PillarboxExoPlayer { + mediaItemTrackerProvider: MediaItemTrackerProvider, +) : ExoPlayer by exoPlayer, PillarboxExoPlayer { private val listeners = HashSet() - private val itemTracker: CurrentMediaItemTracker? + private val itemTagTracker = CurrentMediaItemTagTracker(this) + private val analyticsTracker = AnalyticsMediaItemTracker(this, mediaItemTrackerProvider) private val window = Window() override var smoothSeekingEnabled: Boolean = false set(value) { @@ -61,17 +62,19 @@ class PillarboxPlayer internal constructor( private var isSeeking: Boolean = false /** - * Enable or disable MediaItem tracking + * Enable or disable analytics tracking for the current [MediaItem]. */ var trackingEnabled: Boolean - set(value) = itemTracker?.let { it.enabled = value } ?: Unit - get() = itemTracker?.enabled ?: false + get() = analyticsTracker.enabled + set(value) { + analyticsTracker.enabled = value + } init { exoPlayer.addListener(ComponentListener()) - itemTracker = mediaItemTrackerProvider?.let { - CurrentMediaItemTracker(this, it) - } + + itemTagTracker.addCallback(analyticsTracker) + if (BuildConfig.DEBUG) { addAnalyticsListener(EventLogger()) } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/AssetLoader.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/AssetLoader.kt index 97bd4c55b..0f93969e5 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/AssetLoader.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/asset/AssetLoader.kt @@ -13,7 +13,7 @@ import androidx.media3.exoplayer.source.MediaSource * @property mediaSourceFactory * @constructor Create empty Asset loader */ -abstract class AssetLoader(var mediaSourceFactory: MediaSource.Factory) { +abstract class AssetLoader(val mediaSourceFactory: MediaSource.Factory) { /** * Can load asset * 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 new file mode 100644 index 000000000..ad325f385 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/AnalyticsMediaItemTracker.kt @@ -0,0 +1,222 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.tracker + +import androidx.annotation.VisibleForTesting +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +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 + +/** + * Custom [CurrentMediaItemTagTracker.Callback] to manage analytics. + * + * @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, +) : CurrentMediaItemTagTracker.Callback { + private val listener = CurrentMediaItemListener() + + /** + * Trackers are empty if the tracking session is stopped. + */ + private var trackers = MediaItemTrackerList() + + /** + * Current [MediaItem]. + * Detect `mediaId` changes or URLs if no `mediaId`. + */ + private var currentMediaItem: MediaItem? = null + + private var hasAnalyticsListener = false + + var enabled: Boolean = true + set(value) { + if (field == value) { + return + } + + field = value + if (field) { + player.currentMediaItem?.let { setMediaItem(it) } + } else { + stopSession(StopReason.Stop) + } + } + + override fun onTagChanged( + mediaItem: MediaItem?, + tag: Any?, + ) { + if (mediaItem == null) { + stopSession(StopReason.Stop) + } else if (tag != null) { + if (!hasAnalyticsListener) { + player.addAnalyticsListener(listener) + + hasAnalyticsListener = true + } + + setMediaItem(mediaItem) + } + } + + private fun setMediaItem(mediaItem: MediaItem) { + if (!areEqual(mediaItem, currentMediaItem)) { + stopSession(StopReason.Stop) + } + + if (mediaItem.canHaveTrackingSession() && currentMediaItem?.getMediaItemTrackerDataOrNull() == null) { + startNewSession(mediaItem) + } + + // Update the current MediaItem with tracker data + this.currentMediaItem = mediaItem + } + + private fun stopSession( + stopReason: StopReason, + positionMs: Long = player.currentPosition, + ) { + DebugLogger.info(TAG, "Stop trackers $stopReason @${positionMs.milliseconds}") + + for (tracker in trackers) { + tracker.stop(player, stopReason, positionMs) + } + + trackers.clear() + currentMediaItem = null + } + + private fun startNewSession(mediaItem: MediaItem) { + if (!enabled) { + return + } + + require(trackers.isEmpty()) + + DebugLogger.info(TAG, "Start new session for ${mediaItem.prettyString()}") + + // Create each tracker for this new MediaItem + val mediaItemTrackerData = mediaItem.getMediaItemTrackerDataOrNull() ?: return + val trackers = mediaItemTrackerData.trackers + .map { trackerType -> + mediaItemTrackerProvider.getMediaItemTrackerFactory(trackerType).create() + .also { it.start(player, mediaItemTrackerData.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 (currentMediaItem == null) { + player.currentMediaItem?.let { setMediaItem(it) } + } + } + + else -> Unit + } + } + + /* + * On position discontinuity handle stop session if required + */ + override fun onPositionDiscontinuity( + eventTime: AnalyticsListener.EventTime, + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + @Player.DiscontinuityReason reason: Int, + ) { + DebugLogger.debug( + TAG, + "onPositionDiscontinuity ${StringUtil.discontinuityReasonString(reason)} ${player.currentMediaItem?.prettyString()}" + ) + + val oldPositionMs = oldPosition.positionMs + when (reason) { + Player.DISCONTINUITY_REASON_REMOVE -> stopSession(StopReason.Stop, oldPositionMs) + Player.DISCONTINUITY_REASON_AUTO_TRANSITION -> stopSession(StopReason.EoF, oldPositionMs) + else -> { + if (oldPosition.mediaItemIndex != newPosition.mediaItemIndex) { + stopSession(StopReason.Stop, oldPositionMs) + } + } + } + } + + /* + * Event received after position_discontinuity + * if MediaItemTracker are using AnalyticsListener too + * They may received discontinuity for media item transition. + */ + override fun onMediaItemTransition( + eventTime: AnalyticsListener.EventTime, + mediaItem: MediaItem?, + @Player.MediaItemTransitionReason reason: Int, + ) { + DebugLogger.debug( + TAG, + "onMediaItemTransition ${StringUtil.mediaItemTransitionReasonString(reason)} ${player.currentMediaItem?.prettyString()}" + ) + + if (mediaItem == null) { + stopSession(StopReason.Stop) + } else { + setMediaItem(mediaItem) + } + } + } + + internal companion object { + private const val TAG = "AnalyticsMediaItemTracker" + + private fun MediaItem.prettyString(): String { + return "$mediaId / ${localConfiguration?.uri} ${getMediaItemTrackerDataOrNull()}" + } + + /** + * Are equals only checks mediaId and localConfiguration.uri + * + * @param m1 + * @param m2 + * @return + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun areEqual(m1: MediaItem?, m2: MediaItem?): Boolean { + return when { + m1 == null && m2 == null -> true + m1 == null || m2 == null -> false + else -> + m1.mediaId == m2.mediaId && + m1.buildUpon().setTag(null).build().localConfiguration?.uri == m2.buildUpon().setTag(null).build().localConfiguration?.uri + } + } + + private fun MediaItem.canHaveTrackingSession(): Boolean { + return this.getMediaItemTrackerDataOrNull() != null + } + } +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTagTracker.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTagTracker.kt new file mode 100644 index 000000000..b4ab62b9b --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTagTracker.kt @@ -0,0 +1,96 @@ +/* + * 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.Timeline +import androidx.media3.exoplayer.ExoPlayer + +/** + * This class detects when the `tag` of the current [MediaItem] changes. You can be notified of each change by providing a custom `Callback` to + * this class: + * ```kotlin + * val mediaItemTagTracker = CurrentMediaItemTagTracker(player) + * mediaItemTagTracker.addCallback(object : CurrentMediaItemTagTracker.Callback { + * override fun onTagChanged(mediaItem: MediaItem?, tag: Any?) { + * // The tag of the current `MediaItem` has changed + * } + * }) + * ``` + * + * @param player The [Player] for which the current media item's tag must be tracked. + */ +internal class CurrentMediaItemTagTracker(private val player: ExoPlayer) { + interface Callback { + /** + * Called when the tag of the current media item changes. + * + * @param mediaItem The current [MediaItem]. + * @param tag The tag of the current [MediaItem]. Might be `null` if no tag is set. + */ + fun onTagChanged( + mediaItem: MediaItem?, + tag: Any?, + ) + } + + /** + * The callbacks managed by this tracker. + */ + private val callbacks = mutableSetOf() + + private var lastMediaId: String? = null + private var lastTag: Any? = null + + init { + player.addListener(CurrentMediaItemListener()) + } + + fun addCallback(callback: Callback) { + callbacks.add(callback) + + // If the player already has a MediaItem set, let the new callback know about its current tag + player.currentMediaItem?.let { mediaItem -> + val tag = mediaItem.localConfiguration?.tag + + callback.onTagChanged(mediaItem, tag) + } + } + + private fun notifyTagChange(mediaItem: MediaItem?) { + val mediaId = mediaItem?.mediaId + val tag = mediaItem?.localConfiguration?.tag + // Only send the tag if either the media id or the tag have changed + if (lastMediaId == mediaId && lastTag == tag) { + return + } + + callbacks.forEach { callback -> + callback.onTagChanged(mediaItem, tag) + } + + lastMediaId = mediaId + lastTag = tag + } + + private inner class CurrentMediaItemListener : Player.Listener { + override fun onMediaItemTransition( + mediaItem: MediaItem?, + @Player.MediaItemTransitionReason reason: Int, + ) { + if (reason != Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT) { + notifyTagChange(mediaItem) + } + } + + override fun onTimelineChanged( + timeline: Timeline, + @Player.TimelineChangeReason reason: Int, + ) { + notifyTagChange(player.currentMediaItem) + } + } +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTracker.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTracker.kt deleted file mode 100644 index 2e55707f2..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTracker.kt +++ /dev/null @@ -1,202 +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.annotation.VisibleForTesting -import androidx.media3.common.MediaItem -import androidx.media3.common.Player -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.analytics.AnalyticsListener -import ch.srgssr.pillarbox.player.extension.getMediaItemTrackerData -import ch.srgssr.pillarbox.player.extension.getMediaItemTrackerDataOrNull -import ch.srgssr.pillarbox.player.utils.DebugLogger -import ch.srgssr.pillarbox.player.utils.StringUtil -import kotlin.time.Duration.Companion.milliseconds - -/** - * Current media item tracker - * - * Track current media item transition or lifecycle. - * Tracking session start when current item changed and have [MediaItemTrackerData] set. - * Tracking session stop when current item changed or when it reached the end of lifecycle. - * - * MediaItem asynchronously call this callback after loaded - * - onTimelineChanged with reason = [Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE] - * - * @param player The Player for which the current media item must be tracked. - * @param mediaItemTrackerProvider The MediaItemTrackerProvider that provide new instance of [MediaItemTracker]. - */ -internal class CurrentMediaItemTracker internal constructor( - private val player: ExoPlayer, - private val mediaItemTrackerProvider: MediaItemTrackerProvider -) : AnalyticsListener { - - /** - * Trackers are null if tracking session is stopped! - */ - private var trackers: MediaItemTrackerList? = null - - /** - * Current media item - * Detect mediaId changes or urls if no mediaId - */ - private var currentMediaItem: MediaItem? = null - - var enabled: Boolean = true - set(value) { - if (field == value) return - field = value - if (!field) { - stopSession(MediaItemTracker.StopReason.Stop) - } else { - player.currentMediaItem?.let { setMediaItem(it) } - } - } - - init { - player.addAnalyticsListener(this) - player.currentMediaItem?.let { setMediaItem(it) } - } - - private fun setMediaItem(mediaItem: MediaItem) { - if (!areEqual(mediaItem, currentMediaItem)) { - stopSession(MediaItemTracker.StopReason.Stop) - currentMediaItem = mediaItem - if (mediaItem.canHaveTrackingSession()) { - startNewSession(mediaItem) - } - return - } - if (mediaItem.canHaveTrackingSession()) { - startNewSession(mediaItem) - // Update current media item with tracker data - this.currentMediaItem = mediaItem - } - } - - private fun stopSession(stopReason: MediaItemTracker.StopReason, positionMs: Long = player.currentPosition) { - trackers?.let { - DebugLogger.info(TAG, "stop trackers $stopReason @${positionMs.milliseconds}") - for (tracker in it) { - tracker.stop(player, stopReason, positionMs) - } - } - trackers = null - currentMediaItem = null - } - - private fun startNewSession(mediaItem: MediaItem) { - if (!enabled || trackers != null) return - DebugLogger.info(TAG, "start new session for ${mediaItem.prettyString()}") - - mediaItem.getMediaItemTrackerData().also { trackerData -> - val trackers = MediaItemTrackerList() - // Create each tracker for this new MediaItem - for (trackerType in trackerData.trackers) { - val tracker = mediaItemTrackerProvider.getMediaItemTrackerFactory(trackerType).create() - trackers.append(tracker) - tracker.start(player, trackerData.getData(tracker)) - } - this.trackers = trackers - } - } - - override fun onTimelineChanged(eventTime: AnalyticsListener.EventTime, reason: Int) { - DebugLogger.debug( - TAG, - "onTimelineChanged ${StringUtil.timelineChangeReasonString(reason)} ${player.currentMediaItem.prettyString()}" - ) - if (reason == Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) { - player.currentMediaItem?.let { setMediaItem(it) } - } - } - - override fun onPlaybackStateChanged(eventTime: AnalyticsListener.EventTime, playbackState: Int) { - DebugLogger.debug( - TAG, - "onPlaybackStateChanged ${StringUtil.playerStateString(playbackState)} ${player.currentMediaItem.prettyString()}" - ) - when (playbackState) { - Player.STATE_ENDED -> stopSession(MediaItemTracker.StopReason.EoF) - Player.STATE_IDLE -> stopSession(MediaItemTracker.StopReason.Stop) - Player.STATE_READY -> { - if (currentMediaItem == null) { - player.currentMediaItem?.let { setMediaItem(it) } - } - } - - else -> { - // Nothing - } - } - } - - /* - * On position discontinuity handle stop session if required - */ - override fun onPositionDiscontinuity( - eventTime: AnalyticsListener.EventTime, - oldPosition: Player.PositionInfo, - newPosition: Player.PositionInfo, - reason: Int - ) { - DebugLogger.debug(TAG, "onPositionDiscontinuity ${StringUtil.discontinuityReasonString(reason)} ${player.currentMediaItem.prettyString()}") - val oldPositionMs = oldPosition.positionMs - when (reason) { - Player.DISCONTINUITY_REASON_REMOVE -> stopSession(MediaItemTracker.StopReason.Stop, oldPositionMs) - Player.DISCONTINUITY_REASON_AUTO_TRANSITION -> stopSession(MediaItemTracker.StopReason.EoF, oldPositionMs) - else -> { - if (oldPosition.mediaItemIndex != newPosition.mediaItemIndex) { - stopSession(MediaItemTracker.StopReason.Stop, oldPositionMs) - } - } - } - } - - /* - * Event received after position_discontinuity - * if MediaItemTracker are using AnalyticsListener too - * They may received discontinuity for media item transition. - */ - override fun onMediaItemTransition(eventTime: AnalyticsListener.EventTime, mediaItem: MediaItem?, reason: Int) { - DebugLogger.debug( - TAG, - "onMediaItemTransition ${StringUtil.mediaItemTransitionReasonString(reason)} ${player.currentMediaItem.prettyString()}" - ) - mediaItem?.let { - setMediaItem(it) - } - } - - internal companion object { - private const val TAG = "CurrentMediaItemTracker" - private fun MediaItem?.prettyString(): String { - if (this == null) return "null" - return "$mediaId / ${localConfiguration?.uri} ${getMediaItemTrackerDataOrNull()}" - } - - /** - * Are equals only checks mediaId and localConfiguration.uri - * - * @param m1 - * @param m2 - * @return - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun areEqual(m1: MediaItem?, m2: MediaItem?): Boolean { - return when { - m1 == null && m2 == null -> true - m1 == null || m2 == null -> false - else -> - m1.mediaId == m2.mediaId && - m1.buildUpon().setTag(null).build().localConfiguration?.uri == m2.buildUpon().setTag(null).build().localConfiguration?.uri - } - } - - private fun MediaItem.canHaveTrackingSession(): Boolean { - return this.getMediaItemTrackerDataOrNull() != null - } - } -} 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 index 07c40c876..4d2ea8d3c 100644 --- 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 @@ -5,52 +5,45 @@ package ch.srgssr.pillarbox.player.tracker /** - * Trackers hold a list of [MediaItemTracker]. + * This class holds a list of [MediaItemTracker]. * - * val trackers = mediaItem.getTrackers() - * trackers.append(trackerA) + * ```kotlin + * val trackers = MediaItemTrackerList() + * trackers.add(tracker) + * trackers.addAll(tracker1, tracker2) + * ``` * - * @constructor Create empty Trackers. + * @constructor Create an empty `MediaItemTrackerList`. */ class MediaItemTrackerList internal constructor() : Iterable { - private val listTracker = mutableListOf() + private val trackers = mutableListOf() - /** - * Immutable list of [MediaItemTracker]. - */ - val list: List = listTracker - - /** - * The number of [MediaItemTracker] appended. - */ - val size: Int - get() = list.size + internal val trackerList: List = trackers /** - * Append tracker to the list. You can append only one type of Tracker. + * Add a tracker to the list. Each [tracker] type can only be added once to this [MediaItemTracker]. * - * @param tracker The track to add. - * @return true if the tracker was successfully added, false otherwise. + * @param tracker The tracker to add. + * @return `true` if the tracker was successfully added, `false` otherwise. */ - fun append(tracker: MediaItemTracker): Boolean { - if (listTracker.none { it::class.java == tracker::class.java }) { - listTracker.add(tracker) - return true + fun add(tracker: MediaItemTracker): Boolean { + return if (trackers.none { it::class.java == tracker::class.java }) { + trackers.add(tracker) + } else { + false } - return false } /** - * Appends multiple MediaTracker at once. + * Add multiple trackers at once to the list. Each [tracker] type can only be added once to this [MediaItemTracker]. * - * @param trackers The MediaTracker list to append. - * @return false if one of the trackers is already added. + * @param trackers The trackers to add. + * @return `false` if one of the trackers was already added, `true` otherwise. */ - fun appends(vararg trackers: MediaItemTracker): Boolean { + fun addAll(trackers: List): Boolean { var added = true for (tracker in trackers) { - val currentAdded = append(tracker) - if (!currentAdded) { + if (!add(tracker)) { added = false } } @@ -58,18 +51,22 @@ class MediaItemTrackerList internal constructor() : Iterable { } /** - * Find [MediaItemTracker] from T + * Clear the list of trackers. + */ + fun clear() { + trackers.clear() + } + + /** + * Check if the list of trackers is empty of not. * - * @param T The [MediaItemTracker] type to find. - * @param trackerClass The class to find. - * @return null if not found. + * @return `true` if the list is empty, `false` otherwise. */ - @Suppress("UNCHECKED_CAST") - fun findTracker(trackerClass: Class): T? { - return listTracker.find { it::class.java == trackerClass } as T? + fun isEmpty(): Boolean { + return trackers.isEmpty() } override fun iterator(): Iterator { - return list.iterator() + return trackers.iterator() } } diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTrackerAreEqualTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/AnalyticsMediaItemTrackerAreEqualTest.kt similarity index 82% rename from pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTrackerAreEqualTest.kt rename to pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/AnalyticsMediaItemTrackerAreEqualTest.kt index e3df73bec..b7b325d0b 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTrackerAreEqualTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/AnalyticsMediaItemTrackerAreEqualTest.kt @@ -16,49 +16,49 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) -class CurrentMediaItemTrackerAreEqualTest { +class AnalyticsMediaItemTrackerAreEqualTest { @Test fun `areEqual both mediaItem are null`() { - assertTrue(CurrentMediaItemTracker.areEqual(null, null)) + assertTrue(AnalyticsMediaItemTracker.areEqual(null, null)) } @Test fun `areEqual first mediaItem is null`() { - assertFalse(CurrentMediaItemTracker.areEqual(null, MediaItem.EMPTY)) + assertFalse(AnalyticsMediaItemTracker.areEqual(null, MediaItem.EMPTY)) } @Test fun `areEqual second mediaItem is null`() { - assertFalse(CurrentMediaItemTracker.areEqual(MediaItem.EMPTY, null)) + assertFalse(AnalyticsMediaItemTracker.areEqual(MediaItem.EMPTY, null)) } @Test fun `areEqual with different media id without tag and url`() { val mediaItem = createMediaItemWithMediaId("M1") val mediaItem2 = createMediaItemWithMediaId("M2") - assertFalse(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertFalse(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test fun `areEqual with same media id without tag and url`() { val mediaItem = createMediaItemWithMediaId("M1") val mediaItem2 = createMediaItemWithMediaId("M1") - assertTrue(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertTrue(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test fun `areEqual with one default media id`() { val mediaItem = createMediaItemWithMediaId(MediaItem.DEFAULT_MEDIA_ID) val mediaItem2 = createMediaItemWithMediaId("M1") - assertFalse(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertFalse(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test fun `areEqual with both default media id`() { val mediaItem = createMediaItemWithMediaId(MediaItem.DEFAULT_MEDIA_ID) val mediaItem2 = createMediaItemWithMediaId(MediaItem.DEFAULT_MEDIA_ID) - assertTrue(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertTrue(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test @@ -67,7 +67,7 @@ class CurrentMediaItemTrackerAreEqualTest { val url = "https://streaming.com/video.mp4" val mediaItem = createMediaItemWithMediaId(mediaId = mediaId, url = url, tag = "Tag1") val mediaItem2 = createMediaItemWithMediaId(mediaId = mediaId, url = url, tag = "Tag1") - assertTrue(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertTrue(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test @@ -76,7 +76,7 @@ class CurrentMediaItemTrackerAreEqualTest { val url = "https://streaming.com/video.mp4" val mediaItem = createMediaItemWithMediaId(mediaId = mediaId, url = url) val mediaItem2 = createMediaItemWithMediaId(mediaId = mediaId, url = url) - assertTrue(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertTrue(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test @@ -85,7 +85,7 @@ class CurrentMediaItemTrackerAreEqualTest { val url = "https://streaming.com/video.mp4" val mediaItem = createMediaItemWithMediaId(mediaId = mediaId, url = url, tag = null) val mediaItem2 = createMediaItemWithMediaId(mediaId = mediaId, url = url, tag = "Tag2") - assertTrue(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertTrue(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test @@ -94,7 +94,7 @@ class CurrentMediaItemTrackerAreEqualTest { val url = "https://streaming.com/video.mp4" val mediaItem = createMediaItemWithMediaId(mediaId = mediaId, url = url, tag = "Tag1") val mediaItem2 = createMediaItemWithMediaId(mediaId = mediaId, url = "https://streaming.com/video2.mp4", tag = "Tag1") - assertFalse(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertFalse(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test @@ -107,7 +107,7 @@ class CurrentMediaItemTrackerAreEqualTest { val mediaItem2 = mediaItem.buildUpon() .setTag("Tag2") .build() - assertTrue(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertTrue(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test @@ -121,7 +121,7 @@ class CurrentMediaItemTrackerAreEqualTest { .setUri("https://streaming.com/video.mp4") .setTrackerData(MediaItemTrackerData.Builder().putData(Tracker::class.java, "data1").build()) .build() - assertTrue(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertTrue(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test @@ -134,7 +134,7 @@ class CurrentMediaItemTrackerAreEqualTest { val mediaItem2 = mediaItem.buildUpon() .setTrackerData(mediaItem.getMediaItemTrackerData().buildUpon().putData(Tracker::class.java, "data1").build()) .build() - assertTrue(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertTrue(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test @@ -148,11 +148,11 @@ class CurrentMediaItemTrackerAreEqualTest { .setTrackerData(mediaItem.getMediaItemTrackerData().buildUpon().putData(Tracker::class.java, "data1").build()) .setMediaMetadata(MediaMetadata.Builder().setTitle("New title").build()) .build() - assertTrue(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertTrue(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test - fun `are Equal different data`() { + fun `areEqual different data`() { val mediaItem = MediaItem.Builder() .setUri("https://streaming.com/video.mp4") .setTrackerData(MediaItemTrackerData.Builder().putData(Tracker::class.java, "data1").build()) @@ -161,7 +161,7 @@ class CurrentMediaItemTrackerAreEqualTest { val mediaItem2 = mediaItem.buildUpon() .setTrackerData(mediaItem.getMediaItemTrackerData().buildUpon().putData(Tracker::class.java, "data2").build()) .build() - assertTrue(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) + assertTrue(AnalyticsMediaItemTracker.areEqual(mediaItem, mediaItem2)) } private class Tracker : MediaItemTracker { diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTagTrackerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTagTrackerTest.kt new file mode 100644 index 000000000..bcf4c6a50 --- /dev/null +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/tracker/CurrentMediaItemTagTrackerTest.kt @@ -0,0 +1,323 @@ +/* + * 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 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.PillarboxPlayer +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 kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +@RunWith(AndroidJUnit4::class) +class CurrentMediaItemTagTrackerTest { + private lateinit var clock: FakeClock + private lateinit var context: Context + private lateinit var player: ExoPlayer + private lateinit var tagTracker: CurrentMediaItemTagTracker + + @BeforeTest + fun setUp() { + clock = FakeClock(true) + context = ApplicationProvider.getApplicationContext() + + player = PillarboxPlayer( + context = context, + mediaSourceFactory = PillarboxMediaSourceFactory(context).apply { + addAssetLoader(FakeAssetLoader(context)) + }, + mediaItemTrackerProvider = FakeTrackerProvider(FakeMediaItemTracker()), + clock = clock, + ) + + tagTracker = CurrentMediaItemTagTracker(player) + } + + @AfterTest + fun tearDown() { + player.release() + } + + @Test + fun `player with no media item`() { + val callback = mockk() + + player.prepare() + player.play() + + tagTracker.addCallback(callback) + + verifyOrder { + callback.hashCode() + } + confirmVerified(callback) + } + + @Test + fun `player with tag-less media item`() { + val callback = mockk(relaxed = true) + val mediaItem = FakeAssetLoader.MEDIA_NO_TRACKING_DATA + val expectedTrackerData = MediaItemTrackerData.Builder() + .build() + + player.setMediaItem(mediaItem) + player.prepare() + player.play() + + tagTracker.addCallback(callback) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + val currentMediaItem = player.currentMediaItem!! + + verifyOrder { + callback.hashCode() + callback.onTagChanged(mediaItem, null) + callback.onTagChanged(currentMediaItem, expectedTrackerData) + } + confirmVerified(callback) + } + + @Test + fun `player with tagged media item`() { + val callback = mockk(relaxed = true) + val mediaItem = FakeAssetLoader.MEDIA_1 + val expectedTrackerData = MediaItemTrackerData.Builder() + .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) + .build() + + player.setMediaItem(mediaItem) + player.prepare() + player.play() + + tagTracker.addCallback(callback) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + val currentMediaItem = player.currentMediaItem!! + + verifyOrder { + callback.hashCode() + callback.onTagChanged(mediaItem, null) + callback.onTagChanged(currentMediaItem, expectedTrackerData) + } + 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 expectedTrackerData1 = MediaItemTrackerData.Builder() + .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) + .build() + val expectedTrackerData2 = MediaItemTrackerData.Builder() + .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_2)) + .build() + + player.setMediaItem(mediaItem1) + player.prepare() + player.play() + + tagTracker.addCallback(callback) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + val currentMediaItem1 = player.currentMediaItem!! + + player.setMediaItem(mediaItem2) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + val currentMediaItem2 = player.currentMediaItem!! + + verifyOrder { + callback.hashCode() + callback.onTagChanged(mediaItem1, null) + callback.onTagChanged(currentMediaItem1, expectedTrackerData1) + callback.onTagChanged(mediaItem2, null) + callback.onTagChanged(currentMediaItem2, expectedTrackerData2) + } + 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 expectedTrackerData1 = MediaItemTrackerData.Builder() + .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) + .build() + + player.setMediaItem(mediaItem1) + player.prepare() + player.play() + + tagTracker.addCallback(callback) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + val currentMediaItem = player.currentMediaItem!! + + player.replaceMediaItem(player.currentMediaItemIndex, mediaItem2) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + verifyOrder { + callback.hashCode() + callback.onTagChanged(mediaItem1, null) + callback.onTagChanged(currentMediaItem, expectedTrackerData1) + callback.onTagChanged(mediaItem2, null) + } + confirmVerified(callback) + } + + @Test + fun `player gets its media item removed`() { + val callback = mockk(relaxed = true) + val mediaItem1 = FakeAssetLoader.MEDIA_1 + val expectedTrackerData1 = MediaItemTrackerData.Builder() + .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) + .build() + + player.setMediaItem(mediaItem1) + player.prepare() + player.play() + + tagTracker.addCallback(callback) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + val currentMediaItem = player.currentMediaItem!! + + player.removeMediaItem(0) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + + verifyOrder { + callback.hashCode() + callback.onTagChanged(mediaItem1, null) + callback.onTagChanged(currentMediaItem, expectedTrackerData1) + callback.onTagChanged(null, 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 expectedTrackerData1 = MediaItemTrackerData.Builder() + .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) + .build() + + player.setMediaItem(mediaItem1) + player.prepare() + player.play() + + tagTracker.addCallback(callback) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + val currentMediaItem = player.currentMediaItem!! + + player.addMediaItem(mediaItem2) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + verifyOrder { + callback.hashCode() + callback.onTagChanged(mediaItem1, null) + callback.onTagChanged(currentMediaItem, expectedTrackerData1) + } + 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 expectedTrackerData1 = MediaItemTrackerData.Builder() + .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) + .build() + val expectedTrackerData2 = MediaItemTrackerData.Builder() + .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_2)) + .build() + + player.addMediaItem(mediaItem1) + player.addMediaItem(mediaItem2) + player.prepare() + player.play() + + tagTracker.addCallback(callback) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + val currentMediaItem1 = player.currentMediaItem!! + + player.seekToNextMediaItem() + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + val currentMediaItem2 = player.currentMediaItem!! + + verifyOrder { + callback.hashCode() + callback.onTagChanged(mediaItem1, null) + callback.onTagChanged(currentMediaItem1, expectedTrackerData1) + callback.onTagChanged(mediaItem2, null) + callback.onTagChanged(currentMediaItem2, expectedTrackerData2) + } + confirmVerified(callback) + } + + @Test + fun `playlist gets cleared`() { + val callback = mockk(relaxed = true) + val mediaItem1 = FakeAssetLoader.MEDIA_1 + val mediaItem2 = FakeAssetLoader.MEDIA_2 + val expectedTrackerData1 = MediaItemTrackerData.Builder() + .putData(FakeMediaItemTracker::class.java, FakeMediaItemTracker.Data(FakeAssetLoader.MEDIA_ID_1)) + .build() + + player.setMediaItems(listOf(mediaItem1, mediaItem2)) + player.prepare() + player.play() + + tagTracker.addCallback(callback) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) + + val currentMediaItem = player.currentMediaItem!! + + player.clearMediaItems() + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + + verifyOrder { + callback.hashCode() + callback.onTagChanged(mediaItem1, null) + callback.onTagChanged(currentMediaItem, expectedTrackerData1) + callback.onTagChanged(null, null) + } + confirmVerified(callback) + } +} 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 index 19ae069fc..bfa10bbd6 100644 --- 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 @@ -8,94 +8,83 @@ import androidx.media3.exoplayer.ExoPlayer import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse -import kotlin.test.assertNull import kotlin.test.assertTrue class MediaItemTrackerListTest { @Test fun `empty tracker list`() { val trackers = MediaItemTrackerList() - assertEquals(0, trackers.size) - assertTrue(trackers.list.isEmpty()) + assertTrue(trackers.isEmpty()) + assertEquals(0, trackers.count()) + assertEquals(emptyList(), trackers.trackerList) } @Test - fun `append single tracker`() { + fun `add single tracker`() { val trackers = MediaItemTrackerList() val tracker = ItemTrackerA() - assertTrue(trackers.append(tracker)) - assertEquals(1, trackers.size) - assertEquals(listOf(tracker), trackers.list) + assertTrue(trackers.add(tracker)) + assertFalse(trackers.isEmpty()) + assertEquals(1, trackers.count()) + assertEquals(listOf(tracker), trackers.trackerList) } @Test - fun `append same kind of tracker multiple times`() { + fun `add same kind of tracker multiple times`() { val trackers = MediaItemTrackerList() val trackerA = ItemTrackerA() val trackerAA = ItemTrackerA() - assertTrue(trackers.append(trackerA)) - assertFalse(trackers.append(trackerAA)) - assertEquals(1, trackers.size) - assertEquals(listOf(trackerA), trackers.list) + assertTrue(trackers.add(trackerA)) + assertFalse(trackers.add(trackerAA)) + assertFalse(trackers.isEmpty()) + assertEquals(1, trackers.count()) + assertEquals(listOf(trackerA), trackers.trackerList) } @Test - fun `append different kind of trackers`() { + fun `add different kind of trackers`() { val trackers = MediaItemTrackerList() val trackerList = listOf(ItemTrackerA(), ItemTrackerB(), ItemTrackerC()) for (tracker in trackerList) { - assertTrue(trackers.append(tracker)) + assertTrue(trackers.add(tracker)) } - assertEquals(trackerList.size, trackers.size) - assertEquals(trackerList, trackers.list) + assertFalse(trackers.isEmpty()) + assertEquals(trackerList.size, trackers.count()) + assertEquals(trackerList, trackers.trackerList) } @Test - fun `append different kind of trackers with open tracker`() { + fun `add different kind of trackers with open tracker`() { val trackers = MediaItemTrackerList() val trackerList = listOf(ItemTrackerC(), ItemTrackerD()) for (tracker in trackerList) { - assertTrue(trackers.append(tracker)) + assertTrue(trackers.add(tracker)) } - assertEquals(trackerList.size, trackers.size) - assertEquals(trackerList, trackers.list) + + 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.append(tracker)) + assertTrue(trackersRevert.add(tracker)) } - assertEquals(trackerListRevert.size, trackersRevert.size) - assertEquals(trackerListRevert, trackersRevert.list) + + assertFalse(trackerListRevert.isEmpty()) + assertEquals(trackerListRevert.size, trackersRevert.count()) + assertEquals(trackerListRevert, trackersRevert.trackerList) } @Test - fun `appends multiple trackers`() { + fun `add multiple trackers`() { val trackers = MediaItemTrackerList() val trackerList = listOf(ItemTrackerA(), ItemTrackerB(), ItemTrackerA(), ItemTrackerC()) val expectedTrackers = trackerList.distinctBy { it::class.java } - assertFalse(trackers.appends(*trackerList.toTypedArray())) - assertEquals(expectedTrackers.size, trackers.size) - assertEquals(expectedTrackers, trackers.list) - } - - @Test - fun `find tracker`() { - val trackers = MediaItemTrackerList() - val tracker = ItemTrackerA() - val tracker2 = ItemTrackerB() - trackers.append(tracker) - trackers.append(tracker2) - - val trackerA = trackers.findTracker(ItemTrackerA::class.java) - assertEquals(tracker, trackerA) - - val trackerB = trackers.findTracker(ItemTrackerB::class.java) - assertEquals(tracker2, trackerB) - - val trackerC = trackers.findTracker(ItemTrackerC::class.java) - assertNull(trackerC) + assertFalse(trackers.addAll(trackerList)) + assertEquals(expectedTrackers.size, trackers.count()) + assertEquals(expectedTrackers, trackers.trackerList) } private open class EmptyItemTracker : MediaItemTracker { 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 0e6435096..1ec473e81 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 @@ -5,10 +5,8 @@ package ch.srgssr.pillarbox.player.tracker import android.content.Context -import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.Player -import androidx.media3.common.util.Assertions import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.test.utils.FakeClock import androidx.media3.test.utils.robolectric.RobolectricUtil @@ -29,8 +27,6 @@ import io.mockk.verifyOrder import org.junit.After import org.junit.Before import org.junit.runner.RunWith -import java.util.concurrent.TimeoutException -import java.util.concurrent.atomic.AtomicReference import kotlin.test.Test import kotlin.test.assertNotNull @@ -461,7 +457,7 @@ class MediaItemTrackerTest { } TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) - runUntilMediaItemTransition(player) + TestPlayerRunHelper.runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION) player.stop() // Stop player to stop the auto repeat mode // Wait on item transition @@ -475,23 +471,4 @@ class MediaItemTrackerTest { } confirmVerified(fakeMediaItemTracker) } - - companion object { - @Throws(TimeoutException::class) - private fun runUntilMediaItemTransition(player: Player): Pair { - val receivedEvent = AtomicReference?>() - val listener: Player.Listener = object : Player.Listener { - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - receivedEvent.set(Pair(mediaItem, reason)) - } - } - player.addListener(listener) - RobolectricUtil.runMainLooperUntil { receivedEvent.get() != null || player.playerError != null } - player.removeListener(listener) - if (player.playerError != null) { - throw IllegalStateException(player.playerError) - } - return Assertions.checkNotNull(receivedEvent.get()) - } - } }