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 d2371e43e..d770c04f1 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 @@ -22,6 +22,7 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter import ch.srgssr.pillarbox.player.analytics.MetricsCollector import ch.srgssr.pillarbox.player.analytics.PillarboxAnalyticsCollector +import ch.srgssr.pillarbox.player.analytics.PlaybackSessionManager import ch.srgssr.pillarbox.player.analytics.StallTracker import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange import ch.srgssr.pillarbox.player.asset.timeRange.Chapter @@ -68,6 +69,7 @@ class PillarboxExoPlayer internal constructor( } private val itemPillarboxDataTracker = CurrentMediaItemPillarboxDataTracker(this) private val analyticsTracker = AnalyticsMediaItemTracker(this, mediaItemTrackerProvider) + private val sessionManager = PlaybackSessionManager() private val window = Window() override var smoothSeekingEnabled: Boolean = false set(value) { @@ -127,10 +129,11 @@ class PillarboxExoPlayer internal constructor( QoSCoordinator( context = context, player = this, - eventsDispatcher = PillarboxEventsDispatcher(), + eventsDispatcher = PillarboxEventsDispatcher(sessionManager), startupTimesTracker = StartupTimesTracker(), metricsCollector = MetricsCollector(this), messageHandler = DummyQoSHandler, + sessionManager = sessionManager, coroutineContext = coroutineContext, ) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/analytics/PlaybackSessionManager.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/analytics/PlaybackSessionManager.kt index 8534240e4..cdf56ec30 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/analytics/PlaybackSessionManager.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/analytics/PlaybackSessionManager.kt @@ -4,7 +4,20 @@ */ package ch.srgssr.pillarbox.player.analytics +import androidx.media3.common.C import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.common.Player.DiscontinuityReason +import androidx.media3.common.Player.MediaItemTransitionReason +import androidx.media3.common.Player.TimelineChangeReason +import androidx.media3.common.Timeline +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.analytics.AnalyticsListener +import androidx.media3.exoplayer.source.LoadEventInfo +import androidx.media3.exoplayer.source.MediaLoadData +import ch.srgssr.pillarbox.player.utils.DebugLogger +import ch.srgssr.pillarbox.player.utils.StringUtil import java.util.UUID /** @@ -12,8 +25,7 @@ import java.util.UUID * * @constructor Create empty Playback session manager */ -class PlaybackSessionManager : PillarboxAnalyticsListener { - +class PlaybackSessionManager { /** * - A session is linked to the period inside the timeline, see [Timeline.getUidOfPeriod][androidx.media3.common.Timeline.getUidOfPeriod]. * - A session is created when the player does something with a [MediaItem]. @@ -35,8 +47,6 @@ class PlaybackSessionManager : PillarboxAnalyticsListener { /** * Listener - * - * @constructor Create empty Listener */ interface Listener { /** @@ -61,13 +71,50 @@ class PlaybackSessionManager : PillarboxAnalyticsListener { fun onSessionFinished(session: Session) = Unit } + private val analyticsListener = SessionManagerAnalyticsListener() + private val listeners = mutableSetOf() + private val sessions = mutableMapOf() + private val window = Timeline.Window() + + private var currentSession: Session? = null + set(value) { + if (field != value) { + field?.let { session -> + notifyListeners { onSessionFinished(session) } + sessions.remove(session.periodUid) + } + field = value + field?.let { session -> + notifyListeners { onCurrentSession(session) } + } + } + } + + /** + * Register player + * + * @param player + */ + fun registerPlayer(player: ExoPlayer) { + player.addAnalyticsListener(analyticsListener) + } + + /** + * Unregister player + * + * @param player + */ + fun unregisterPlayer(player: ExoPlayer) { + player.removeAnalyticsListener(analyticsListener) + } + /** * Add listener * * @param listener */ fun addListener(listener: Listener) { - TODO("Implement addListener") + listeners.add(listener) } /** @@ -76,7 +123,7 @@ class PlaybackSessionManager : PillarboxAnalyticsListener { * @param listener */ fun removeListener(listener: Listener) { - TODO("implement removeListener") + listeners.remove(listener) } /** @@ -85,7 +132,7 @@ class PlaybackSessionManager : PillarboxAnalyticsListener { * @return */ fun getCurrentSession(): Session? { - TODO("implement getCurrentSession") + return currentSession } /** @@ -94,7 +141,183 @@ class PlaybackSessionManager : PillarboxAnalyticsListener { * @param sessionId * @return */ - fun getSessionFromId(sessionId: String): Session? { - TODO("implement getSessionFromId") + fun getSessionById(sessionId: String): Session? { + return sessions.values.find { it.sessionId == sessionId } + } + + /** + * Get session from event time + * + * @param eventTime + * @return + */ + fun getSessionFromEventTime(eventTime: AnalyticsListener.EventTime): Session? { + if (eventTime.timeline.isEmpty) { + return null + } + + eventTime.timeline.getWindow(eventTime.windowIndex, window) + + val periodUid = eventTime.timeline.getUidOfPeriod(window.firstPeriodIndex) + + return sessions[periodUid] + } + + private inline fun notifyListeners(event: Listener.() -> Unit) { + listeners.toList() + .forEach { listener -> + listener.event() + } + } + + private inner class SessionManagerAnalyticsListener : PillarboxAnalyticsListener { + override fun onPositionDiscontinuity( + eventTime: AnalyticsListener.EventTime, + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + @DiscontinuityReason reason: Int + ) { + val oldItemIndex = oldPosition.mediaItemIndex + val newItemIndex = newPosition.mediaItemIndex + + DebugLogger.debug(TAG, "onPositionDiscontinuity reason = ${StringUtil.discontinuityReasonString(reason)}") + + if (oldItemIndex != newItemIndex && !eventTime.timeline.isEmpty) { + currentSession = getOrCreateSession(eventTime) + } + } + + override fun onMediaItemTransition( + eventTime: AnalyticsListener.EventTime, + mediaItem: MediaItem?, + @MediaItemTransitionReason reason: Int, + ) { + DebugLogger.debug( + TAG, + "onMediaItemTransition reason = ${StringUtil.mediaItemTransitionReasonString(reason)} ${mediaItem?.mediaMetadata?.title}", + ) + + currentSession = mediaItem?.let { getOrCreateSession(eventTime) } + } + + override fun onTimelineChanged( + eventTime: AnalyticsListener.EventTime, + @TimelineChangeReason reason: Int, + ) { + val mediaItem = if (eventTime.timeline.isEmpty) { + MediaItem.EMPTY + } else { + eventTime.timeline.getWindow(eventTime.windowIndex, window).mediaItem + } + + DebugLogger.debug(TAG, "onTimelineChanged reason = ${StringUtil.timelineChangeReasonString(reason)} ${mediaItem.mediaMetadata.title}") + + val timeline = eventTime.timeline + if (timeline.isEmpty) { + finishAllSessions() + return + } + + // Finish sessions that are no longer in the timeline + val currentSessions = sessions.values.toSet() + currentSessions.forEach { session -> + val periodUid = session.periodUid + val periodIndex = timeline.getIndexOfPeriod(periodUid) + if (periodIndex == C.INDEX_UNSET) { + if (session == currentSession) { + currentSession = null + } else { + notifyListeners { onSessionFinished(session) } + sessions.remove(session.periodUid) + } + } + } + } + + override fun onLoadStarted( + eventTime: AnalyticsListener.EventTime, + loadEventInfo: LoadEventInfo, + mediaLoadData: MediaLoadData, + ) { + getOrCreateSession(eventTime) + } + + override fun onPlayerError( + eventTime: AnalyticsListener.EventTime, + error: PlaybackException, + ) { + getOrCreateSession(eventTime) + } + + override fun onAudioPositionAdvancing( + eventTime: AnalyticsListener.EventTime, + playoutStartSystemTimeMs: Long, + ) { + getOrCreateSession(eventTime) + } + + override fun onRenderedFirstFrame( + eventTime: AnalyticsListener.EventTime, + output: Any, + renderTimeMs: Long, + ) { + getOrCreateSession(eventTime) + } + + override fun onStallChanged( + eventTime: AnalyticsListener.EventTime, + isStall: Boolean, + ) { + getOrCreateSession(eventTime) + } + + override fun onIsPlayingChanged( + eventTime: AnalyticsListener.EventTime, + isPlaying: Boolean, + ) { + getOrCreateSession(eventTime) + } + + override fun onPlayerReleased(eventTime: AnalyticsListener.EventTime) { + DebugLogger.debug(TAG, "onPlayerReleased") + finishAllSessions() + } + + private fun getOrCreateSession(eventTime: AnalyticsListener.EventTime): Session? { + if (eventTime.timeline.isEmpty) { + return null + } + + eventTime.timeline.getWindow(eventTime.windowIndex, window) + + val periodUid = eventTime.timeline.getUidOfPeriod(window.firstPeriodIndex) + var session = sessions[periodUid] + if (session == null) { + val newSession = Session(periodUid, window.mediaItem) + sessions[periodUid] = newSession + notifyListeners { onSessionCreated(newSession) } + + if (currentSession == null) { + currentSession = newSession + } + + session = newSession + } + + return session + } + + private fun finishAllSessions() { + currentSession = null + + sessions.values.forEach { session -> + notifyListeners { onSessionFinished(session) } + } + sessions.clear() + } + } + + private companion object { + private const val TAG = "PlaybackSessionManager" } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/PillarboxEventsDispatcher.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/PillarboxEventsDispatcher.kt index 9fad02278..a29a8e6e9 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/PillarboxEventsDispatcher.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/PillarboxEventsDispatcher.kt @@ -4,30 +4,25 @@ */ package ch.srgssr.pillarbox.player.qos -import androidx.media3.common.C -import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK import androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT import androidx.media3.common.Player.DiscontinuityReason -import androidx.media3.common.Player.MediaItemTransitionReason -import androidx.media3.common.Player.TimelineChangeReason -import androidx.media3.common.Timeline import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.analytics.AnalyticsListener.EventTime -import androidx.media3.exoplayer.source.LoadEventInfo -import androidx.media3.exoplayer.source.MediaLoadData import ch.srgssr.pillarbox.player.analytics.PillarboxAnalyticsListener +import ch.srgssr.pillarbox.player.analytics.PlaybackSessionManager import ch.srgssr.pillarbox.player.qos.QoSEventsDispatcher.Listener -import ch.srgssr.pillarbox.player.qos.QoSEventsDispatcher.Session import ch.srgssr.pillarbox.player.utils.DebugLogger import ch.srgssr.pillarbox.player.utils.StringUtil /** * Pillarbox provided implementation of [QoSEventsDispatcher]. */ -class PillarboxEventsDispatcher : QoSEventsDispatcher { +class PillarboxEventsDispatcher( + private val sessionManager: PlaybackSessionManager, +) : QoSEventsDispatcher { private val analyticsListener = EventsDispatcherAnalyticsListener() private val listeners = mutableSetOf() @@ -55,23 +50,6 @@ class PillarboxEventsDispatcher : QoSEventsDispatcher { } private inner class EventsDispatcherAnalyticsListener : PillarboxAnalyticsListener { - private val sessions = mutableMapOf() - private val window = Timeline.Window() - - private var currentSession: Session? = null - set(value) { - if (field != value) { - field?.let { session -> - notifyListeners { onSessionFinished(session) } - sessions.remove(session.periodUid) - } - field = value - field?.let { session -> - notifyListeners { onCurrentSession(session) } - } - } - } - override fun onPositionDiscontinuity( eventTime: EventTime, oldPosition: Player.PositionInfo, @@ -83,87 +61,24 @@ class PillarboxEventsDispatcher : QoSEventsDispatcher { DebugLogger.debug(TAG, "onPositionDiscontinuity reason = ${StringUtil.discontinuityReasonString(reason)}") - if (oldItemIndex != newItemIndex && !eventTime.timeline.isEmpty) { - currentSession = getOrCreateSession(eventTime) - } if (oldItemIndex == newItemIndex && reason == DISCONTINUITY_REASON_SEEK || reason == DISCONTINUITY_REASON_SEEK_ADJUSTMENT) { - currentSession?.let { - notifyListeners { - onSeek(it) - } - } - } - } - - override fun onMediaItemTransition( - eventTime: EventTime, - mediaItem: MediaItem?, - @MediaItemTransitionReason reason: Int, - ) { - DebugLogger.debug( - TAG, - "onMediaItemTransition reason = ${StringUtil.mediaItemTransitionReasonString(reason)} ${mediaItem?.mediaMetadata?.title}", - ) - - currentSession = mediaItem?.let { getOrCreateSession(eventTime) } - } + val session = sessionManager.getCurrentSession() ?: return - override fun onPlayerError(eventTime: EventTime, error: PlaybackException) { - val session = getOrCreateSession(eventTime) - session?.let { - notifyListeners { - onError(session) - } + notifyListeners { onSeek(session) } } } - override fun onTimelineChanged( - eventTime: EventTime, - @TimelineChangeReason reason: Int, - ) { - val mediaItem = if (eventTime.timeline.isEmpty) { - MediaItem.EMPTY - } else { - eventTime.timeline.getWindow(eventTime.windowIndex, window).mediaItem - } - - DebugLogger.debug(TAG, "onTimelineChanged reason = ${StringUtil.timelineChangeReasonString(reason)} ${mediaItem.mediaMetadata.title}") - - val timeline = eventTime.timeline - if (timeline.isEmpty) { - finishAllSessions() - return - } - - // Finish sessions that are no longer in the timeline - val currentSessions = sessions.values.toSet() - currentSessions.forEach { session -> - val periodUid = session.periodUid - val periodIndex = timeline.getIndexOfPeriod(periodUid) - if (periodIndex == C.INDEX_UNSET) { - if (session == currentSession) { - currentSession = null - } else { - notifyListeners { onSessionFinished(session) } - sessions.remove(session.periodUid) - } - } - } - } + override fun onPlayerError(eventTime: EventTime, error: PlaybackException) { + val session = sessionManager.getSessionFromEventTime(eventTime) ?: return - override fun onLoadStarted( - eventTime: EventTime, - loadEventInfo: LoadEventInfo, - mediaLoadData: MediaLoadData, - ) { - getOrCreateSession(eventTime) + notifyListeners { onError(session) } } override fun onAudioPositionAdvancing( eventTime: EventTime, playoutStartSystemTimeMs: Long, ) { - val session = getOrCreateSession(eventTime) ?: return + val session = sessionManager.getSessionFromEventTime(eventTime) ?: return notifyListeners { onMediaStart(session) } } @@ -173,60 +88,28 @@ class PillarboxEventsDispatcher : QoSEventsDispatcher { output: Any, renderTimeMs: Long, ) { - val session = getOrCreateSession(eventTime) ?: return + val session = sessionManager.getSessionFromEventTime(eventTime) ?: return notifyListeners { onMediaStart(session) } } override fun onStallChanged(eventTime: EventTime, isStall: Boolean) { - val session = getOrCreateSession(eventTime) ?: return if (isStall) { + val session = sessionManager.getSessionFromEventTime(eventTime) ?: return + notifyListeners { onStall(session) } } } override fun onPlayerReleased(eventTime: EventTime) { DebugLogger.debug(TAG, "onPlayerReleased") - finishAllSessions() notifyListeners { onPlayerReleased() } } override fun onIsPlayingChanged(eventTime: EventTime, isPlaying: Boolean) { - val session = getOrCreateSession(eventTime) ?: return - notifyListeners { onIsPlaying(session, isPlaying) } - } - - private fun getOrCreateSession(eventTime: EventTime): Session? { - if (eventTime.timeline.isEmpty) { - return null - } - - eventTime.timeline.getWindow(eventTime.windowIndex, window) + val session = sessionManager.getSessionFromEventTime(eventTime) ?: return - val periodUid = eventTime.timeline.getUidOfPeriod(window.firstPeriodIndex) - var session = sessions[periodUid] - if (session == null) { - val newSession = Session(periodUid, window.mediaItem) - sessions[periodUid] = newSession - notifyListeners { onSessionCreated(newSession) } - - if (currentSession == null) { - currentSession = newSession - } - - session = newSession - } - - return session - } - - private fun finishAllSessions() { - currentSession = null - - sessions.values.forEach { session -> - notifyListeners { onSessionFinished(session) } - } - sessions.clear() + notifyListeners { onIsPlaying(session, isPlaying) } } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSCoordinator.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSCoordinator.kt index 68440bc74..65702fcda 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSCoordinator.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSCoordinator.kt @@ -12,6 +12,7 @@ import androidx.media3.exoplayer.source.LoadEventInfo import androidx.media3.exoplayer.source.MediaLoadData import ch.srgssr.pillarbox.player.analytics.MetricsCollector import ch.srgssr.pillarbox.player.analytics.PillarboxAnalyticsListener +import ch.srgssr.pillarbox.player.analytics.PlaybackSessionManager import ch.srgssr.pillarbox.player.analytics.PlaybackStats import ch.srgssr.pillarbox.player.utils.DebugLogger import ch.srgssr.pillarbox.player.utils.Heartbeat @@ -25,6 +26,7 @@ internal class QoSCoordinator( private val startupTimesTracker: StartupTimesTracker, private val metricsCollector: MetricsCollector, private val messageHandler: QoSMessageHandler, + private val sessionManager: PlaybackSessionManager, coroutineContext: CoroutineContext, ) : PillarboxAnalyticsListener { private val heartbeat = Heartbeat( @@ -38,14 +40,19 @@ internal class QoSCoordinator( ) private var url: String = "" - private val sessions = mutableMapOf() - private var currentSession: QoSEventsDispatcher.Session? = null + private var currentSession: PlaybackSessionManager.Session? = null init { + val eventsDispatcherListener = EventsDispatcherListener() + eventsDispatcher.registerPlayer(player) - eventsDispatcher.addListener(EventsDispatcherListener()) + eventsDispatcher.addListener(eventsDispatcherListener) eventsDispatcher.addListener(startupTimesTracker) + sessionManager.registerPlayer(player) + sessionManager.addListener(eventsDispatcherListener) + sessionManager.addListener(startupTimesTracker) + player.addAnalyticsListener(startupTimesTracker) player.addAnalyticsListener(metricsCollector) player.addAnalyticsListener(this) @@ -66,7 +73,7 @@ internal class QoSCoordinator( private fun sendEvent( eventName: String, - session: QoSEventsDispatcher.Session, + session: PlaybackSessionManager.Session, data: Any? = null, ) { val message = QoSMessage( @@ -93,19 +100,19 @@ internal class QoSCoordinator( ) } - private inner class EventsDispatcherListener : QoSEventsDispatcher.Listener { + private inner class EventsDispatcherListener : PlaybackSessionManager.Listener, QoSEventsDispatcher.Listener { - override fun onCurrentSession(session: QoSEventsDispatcher.Session) { + override fun onCurrentSession(session: PlaybackSessionManager.Session) { currentSession = session } - override fun onSessionFinished(session: QoSEventsDispatcher.Session) { + override fun onSessionFinished(session: PlaybackSessionManager.Session) { heartbeat.stop() sendEvent("END", session) currentSession = null } - override fun onMediaStart(session: QoSEventsDispatcher.Session) { + override fun onMediaStart(session: PlaybackSessionManager.Session) { val startupTimes = startupTimesTracker.consumeStartupTimes(session.sessionId) ?: return heartbeat.start(restart = false) @@ -114,7 +121,7 @@ internal class QoSCoordinator( } override fun onIsPlaying( - session: QoSEventsDispatcher.Session, + session: PlaybackSessionManager.Session, isPlaying: Boolean, ) { if (isPlaying) { @@ -124,16 +131,16 @@ internal class QoSCoordinator( } } - override fun onSeek(session: QoSEventsDispatcher.Session) { + override fun onSeek(session: PlaybackSessionManager.Session) { sendEvent("SEEK", session) } - override fun onStall(session: QoSEventsDispatcher.Session) { + override fun onStall(session: PlaybackSessionManager.Session) { sendEvent("STALL", session) } - override fun onError(session: QoSEventsDispatcher.Session) { - if (!sessions.containsKey(session.sessionId)) { + override fun onError(session: PlaybackSessionManager.Session) { + if (sessionManager.getSessionById(session.sessionId) == null) { sendStartEvent(session, QoSSessionTimings.Zero) } @@ -155,13 +162,17 @@ internal class QoSCoordinator( eventsDispatcher.removeListener(this) eventsDispatcher.removeListener(startupTimesTracker) + sessionManager.unregisterPlayer(player) + sessionManager.removeListener(this) + sessionManager.removeListener(startupTimesTracker) + player.removeAnalyticsListener(startupTimesTracker) player.removeAnalyticsListener(metricsCollector) player.removeAnalyticsListener(this@QoSCoordinator) } private fun sendStartEvent( - session: QoSEventsDispatcher.Session, + session: PlaybackSessionManager.Session, timings: QoSSessionTimings, ) { sendEvent( diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSEventsDispatcher.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSEventsDispatcher.kt index e5d3ff76a..d1ddb0a3c 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSEventsDispatcher.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSEventsDispatcher.kt @@ -4,57 +4,23 @@ */ package ch.srgssr.pillarbox.player.qos -import androidx.media3.common.MediaItem import androidx.media3.exoplayer.ExoPlayer -import java.util.UUID +import ch.srgssr.pillarbox.player.analytics.PlaybackSessionManager /** * Events dispatcher that notifies when specific events happen (related to a session, media playback, ...). */ interface QoSEventsDispatcher { - /** - * - A session is linked to the period inside the timeline, see [Timeline.getUidOfPeriod][androidx.media3.common.Timeline.getUidOfPeriod]. - * - A session is created when the player does something with a [MediaItem]. - * - A session is current if the media item associated with the session is the current [MediaItem]. - * - A session is finished when it is no longer the current session, or when the session is removed from the player. - * - * @property periodUid The period id from [Timeline.getUidOfPeriod][androidx.media3.common.Timeline.getUidOfPeriod] for [mediaItem]. - * @property mediaItem The [MediaItem] linked to the session. - */ - data class Session( - val periodUid: Any, - val mediaItem: MediaItem, - ) { - /** - * Unique session id. - */ - val sessionId = UUID.randomUUID().toString() - } - /** * Listener to be notified for every event dispatched by [QoSEventsDispatcher]. */ interface Listener { - /** - * On session created - * - * @param session - */ - fun onSessionCreated(session: Session) = Unit - - /** - * On current session - * - * @param session - */ - fun onCurrentSession(session: Session) = Unit - /** * On media start * * @param session */ - fun onMediaStart(session: Session) = Unit + fun onMediaStart(session: PlaybackSessionManager.Session) = Unit /** * On is playing @@ -63,7 +29,7 @@ interface QoSEventsDispatcher { * @param isPlaying */ fun onIsPlaying( - session: Session, + session: PlaybackSessionManager.Session, isPlaying: Boolean, ) = Unit @@ -72,28 +38,21 @@ interface QoSEventsDispatcher { * * @param session */ - fun onSeek(session: Session) = Unit + fun onSeek(session: PlaybackSessionManager.Session) = Unit /** * On stall * * @param session */ - fun onStall(session: Session) = Unit + fun onStall(session: PlaybackSessionManager.Session) = Unit /** * On error * * @param session */ - fun onError(session: Session) = Unit - - /** - * On session finished - * - * @param session - */ - fun onSessionFinished(session: Session) = Unit + fun onError(session: PlaybackSessionManager.Session) = Unit /** * On player released diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/StartupTimesTracker.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/StartupTimesTracker.kt index cbf55a4c7..3a13480a6 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/StartupTimesTracker.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/StartupTimesTracker.kt @@ -9,10 +9,11 @@ import androidx.media3.common.Timeline import androidx.media3.exoplayer.analytics.AnalyticsListener import androidx.media3.exoplayer.source.LoadEventInfo import androidx.media3.exoplayer.source.MediaLoadData +import ch.srgssr.pillarbox.player.analytics.PlaybackSessionManager import ch.srgssr.pillarbox.player.source.PillarboxMediaSource import kotlin.time.Duration.Companion.milliseconds -internal class StartupTimesTracker : AnalyticsListener, QoSEventsDispatcher.Listener { +internal class StartupTimesTracker : AnalyticsListener, PlaybackSessionManager.Listener, QoSEventsDispatcher.Listener { private val loadingSessions = mutableSetOf() private val periodUidToSessionId = mutableMapOf() private val currentSessionToMediaStart = mutableMapOf() @@ -34,17 +35,17 @@ internal class StartupTimesTracker : AnalyticsListener, QoSEventsDispatcher.List return null } - override fun onSessionCreated(session: QoSEventsDispatcher.Session) { + override fun onSessionCreated(session: PlaybackSessionManager.Session) { loadingSessions.add(session.sessionId) periodUidToSessionId[session.periodUid] = session.sessionId qosSessionsTimings[session.sessionId] = QoSSessionTimings.Zero } - override fun onCurrentSession(session: QoSEventsDispatcher.Session) { + override fun onCurrentSession(session: PlaybackSessionManager.Session) { currentSessionToMediaStart[session.sessionId] = System.currentTimeMillis() } - override fun onSessionFinished(session: QoSEventsDispatcher.Session) { + override fun onSessionFinished(session: PlaybackSessionManager.Session) { loadingSessions.remove(session.sessionId) periodUidToSessionId.remove(session.periodUid) qosSessionsTimings.remove(session.sessionId) diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/analytics/PlaybackSessionManagerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/analytics/PlaybackSessionManagerTest.kt new file mode 100644 index 000000000..433e63b0c --- /dev/null +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/analytics/PlaybackSessionManagerTest.kt @@ -0,0 +1,333 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.analytics + +import android.content.Context +import android.os.Looper +import androidx.media3.common.MediaItem +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 io.mockk.clearAllMocks +import io.mockk.clearMocks +import io.mockk.confirmVerified +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import io.mockk.verifyOrder +import org.junit.runner.RunWith +import org.robolectric.Shadows.shadowOf +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class PlaybackSessionManagerTest { + private lateinit var clock: FakeClock + private lateinit var player: ExoPlayer + private lateinit var sessionManager: PlaybackSessionManager + private lateinit var sessionManagerListener: PlaybackSessionManager.Listener + + @BeforeTest + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + + clock = FakeClock(true) + sessionManagerListener = mockk(relaxed = true) + player = ExoPlayer.Builder(context) + .setClock(clock) + .build() + .apply { + prepare() + } + + sessionManager = PlaybackSessionManager().apply { + registerPlayer(player) + addListener(sessionManagerListener) + } + + clearMocks(sessionManagerListener) + } + + @AfterTest + fun tearDown() { + clearAllMocks() + sessionManager.unregisterPlayer(player) + sessionManager.removeListener(sessionManagerListener) + player.release() + shadowOf(Looper.getMainLooper()).idle() + } + + @Test + fun `get session single media item`() { + val mediaItem = MediaItem.fromUri(VOD1) + + assertNull(sessionManager.getCurrentSession()) + assertNull(sessionManager.getSessionById("some-invalid-session-id")) + + player.setMediaItem(mediaItem) + player.play() + + TestPlayerRunHelper.playUntilStartOfMediaItem(player, 0) + + val sessionSlot = slot() + + verify { + sessionManagerListener.onSessionCreated(capture(sessionSlot)) + } + + val session = sessionManager.getCurrentSession() + + assertNotNull(session) + assertEquals(sessionSlot.captured.sessionId, session.sessionId) + assertEquals(session, sessionManager.getSessionById(session.sessionId)) + } + + @Test + fun `get session multiple media items`() { + val mediaItems = listOf(VOD1, VOD2, VOD3).map { MediaItem.fromUri(it) } + + assertNull(sessionManager.getCurrentSession()) + assertNull(sessionManager.getSessionById("some-invalid-session-id")) + + player.setMediaItems(mediaItems) + player.play() + + TestPlayerRunHelper.playUntilStartOfMediaItem(player, 2) + + val onSessionCreated = mutableListOf() + + verify { + sessionManagerListener.onSessionCreated(capture(onSessionCreated)) + } + + val session = sessionManager.getCurrentSession() + + assertEquals(3, onSessionCreated.distinctBy { it.sessionId }.size) + assertNotNull(session) + assertEquals(onSessionCreated[1].sessionId, session.sessionId) + assertEquals(session, sessionManager.getSessionById(session.sessionId)) + } + + @Test + fun `play single media item`() { + val mediaItem = MediaItem.fromUri(VOD1) + + player.setMediaItem(mediaItem) + player.play() + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + + val onSessionCreated = mutableListOf() + val onCurrentSession = mutableListOf() + + verify { + sessionManagerListener.onSessionCreated(capture(onSessionCreated)) + sessionManagerListener.onCurrentSession(capture(onCurrentSession)) + } + confirmVerified(sessionManagerListener) + + assertEquals(1, onSessionCreated.size) + assertEquals(1, onCurrentSession.size) + + assertEquals(1, onSessionCreated.distinctBy { it.sessionId }.size) + assertEquals(1, onCurrentSession.distinctBy { it.sessionId }.size) + + assertEquals(listOf(mediaItem), onSessionCreated.map { it.mediaItem }) + assertEquals(listOf(mediaItem), onCurrentSession.map { it.mediaItem }) + } + + @Test + fun `play single media item, remove media item`() { + val mediaItem = MediaItem.fromUri(VOD1) + + player.setMediaItem(mediaItem) + player.play() + player.removeMediaItem(player.currentMediaItemIndex) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + + val sessions = mutableListOf() + + verifyOrder { + sessionManagerListener.onSessionCreated(capture(sessions)) + sessionManagerListener.onCurrentSession(capture(sessions)) + sessionManagerListener.onSessionFinished(capture(sessions)) + } + confirmVerified(sessionManagerListener) + + assertEquals(3, sessions.size) + assertEquals(1, sessions.distinctBy { it.sessionId }.size) + assertTrue(sessions.all { it.mediaItem == mediaItem }) + } + + @Test + fun `play multiple media items`() { + val mediaItems = listOf(VOD1, VOD2, VOD3).map { MediaItem.fromUri(it) } + + player.setMediaItems(mediaItems) + player.play() + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + + // To ensure that the final `onSessionFinished` is triggered. + player.clearMediaItems() + + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) + + val onSessionCreated = mutableListOf() + val onCurrentSession = mutableListOf() + val onSessionFinished = mutableListOf() + + verify { + sessionManagerListener.onSessionCreated(capture(onSessionCreated)) + sessionManagerListener.onCurrentSession(capture(onCurrentSession)) + sessionManagerListener.onSessionFinished(capture(onSessionFinished)) + } + confirmVerified(sessionManagerListener) + + assertEquals(3, onSessionCreated.size) + assertEquals(3, onCurrentSession.size) + assertEquals(3, onSessionFinished.size) + + assertEquals(3, onSessionCreated.distinctBy { it.sessionId }.size) + assertEquals(3, onCurrentSession.distinctBy { it.sessionId }.size) + assertEquals(3, onSessionFinished.distinctBy { it.sessionId }.size) + + assertEquals(mediaItems, onSessionCreated.map { it.mediaItem }) + assertEquals(mediaItems, onCurrentSession.map { it.mediaItem }) + assertEquals(mediaItems, onSessionFinished.map { it.mediaItem }) + } + + @Test + fun `play multiple media items, remove upcoming media item`() { + val mediaItems = listOf(VOD1, VOD2, VOD3).map { MediaItem.fromUri(it) } + val expectedMediaItems = listOf(mediaItems[0], mediaItems[2]) + + player.setMediaItems(mediaItems) + player.play() + player.removeMediaItem(player.currentMediaItemIndex + 1) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + + // To ensure that the final `onSessionFinished` is triggered. + player.clearMediaItems() + + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) + + val createdSessions = mutableListOf() + val currentSessions = mutableListOf() + val finishedSessions = mutableListOf() + + verify { + sessionManagerListener.onSessionCreated(capture(createdSessions)) + sessionManagerListener.onCurrentSession(capture(currentSessions)) + sessionManagerListener.onSessionFinished(capture(finishedSessions)) + } + confirmVerified(sessionManagerListener) + + assertEquals(2, createdSessions.size) + assertEquals(2, currentSessions.size) + assertEquals(2, finishedSessions.size) + + assertEquals(2, createdSessions.distinctBy { it.sessionId }.size) + assertEquals(2, currentSessions.distinctBy { it.sessionId }.size) + assertEquals(2, finishedSessions.distinctBy { it.sessionId }.size) + + assertEquals(expectedMediaItems, createdSessions.map { it.mediaItem }) + assertEquals(expectedMediaItems, currentSessions.map { it.mediaItem }) + assertEquals(expectedMediaItems, finishedSessions.map { it.mediaItem }) + } + + @Test + fun `play multiple media items, remove current media item`() { + val mediaItems = listOf(VOD1, VOD2, VOD3).map { MediaItem.fromUri(it) } + + player.setMediaItems(mediaItems) + player.play() + player.removeMediaItem(player.currentMediaItemIndex) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + + // To ensure that the final `onSessionFinished` is triggered. + player.clearMediaItems() + + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) + + val createdSessions = mutableListOf() + val currentSessions = mutableListOf() + val finishedSessions = mutableListOf() + + verify { + sessionManagerListener.onSessionCreated(capture(createdSessions)) + sessionManagerListener.onCurrentSession(capture(currentSessions)) + sessionManagerListener.onSessionFinished(capture(finishedSessions)) + } + confirmVerified(sessionManagerListener) + + assertEquals(3, createdSessions.size) + assertEquals(3, currentSessions.size) + assertEquals(3, finishedSessions.size) + + assertEquals(3, createdSessions.distinctBy { it.sessionId }.size) + assertEquals(3, currentSessions.distinctBy { it.sessionId }.size) + assertEquals(3, finishedSessions.distinctBy { it.sessionId }.size) + + assertEquals(mediaItems, createdSessions.map { it.mediaItem }) + assertEquals(mediaItems, currentSessions.map { it.mediaItem }) + assertEquals(mediaItems, finishedSessions.map { it.mediaItem }) + } + + @Test + fun `play multiple same media items create multiple sessions`() { + val mediaItems = listOf(VOD1, VOD1, VOD3).map { MediaItem.fromUri(it) } + + player.setMediaItems(mediaItems) + player.play() + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + + // To ensure that the final `onSessionFinished` is triggered. + player.clearMediaItems() + + TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) + + val createdSessions = mutableListOf() + val currentSessions = mutableListOf() + val finishedSessions = mutableListOf() + + verify { + sessionManagerListener.onSessionCreated(capture(createdSessions)) + sessionManagerListener.onCurrentSession(capture(currentSessions)) + sessionManagerListener.onSessionFinished(capture(finishedSessions)) + } + confirmVerified(sessionManagerListener) + + assertEquals(3, createdSessions.size) + assertEquals(3, currentSessions.size) + assertEquals(3, finishedSessions.size) + + assertEquals(3, createdSessions.distinctBy { it.sessionId }.size) + assertEquals(3, currentSessions.distinctBy { it.sessionId }.size) + assertEquals(3, finishedSessions.distinctBy { it.sessionId }.size) + + assertEquals(mediaItems, createdSessions.map { it.mediaItem }) + assertEquals(mediaItems, currentSessions.map { it.mediaItem }) + assertEquals(mediaItems, finishedSessions.map { it.mediaItem }) + } + + private companion object { + private const val VOD1 = "https://rts-vod-amd.akamaized.net/ww/13444390/f1b478f7-2ae9-3166-94b9-c5d5fe9610df/master.m3u8" + private const val VOD2 = "https://rts-vod-amd.akamaized.net/ww/13444333/feb1d08d-e62c-31ff-bac9-64c0a7081612/master.m3u8" + private const val VOD3 = "https://rts-vod-amd.akamaized.net/ww/13444466/2787e520-412f-35fb-83d7-8dbb31b5c684/master.m3u8" + } +} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/QoSEventsDispatcherTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/QoSEventsDispatcherTest.kt index cf02f8848..26bfab447 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/QoSEventsDispatcherTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/QoSEventsDispatcherTest.kt @@ -13,19 +13,18 @@ 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.analytics.PlaybackSessionManager import io.mockk.clearAllMocks import io.mockk.clearMocks import io.mockk.confirmVerified import io.mockk.mockk import io.mockk.verify -import io.mockk.verifyOrder import org.junit.runner.RunWith import org.robolectric.Shadows.shadowOf import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class QoSEventsDispatcherTest { @@ -46,7 +45,11 @@ class QoSEventsDispatcherTest { prepare() } - PillarboxEventsDispatcher().apply { + val sessionManager = PlaybackSessionManager().apply { + registerPlayer(player) + } + + PillarboxEventsDispatcher(sessionManager).apply { registerPlayer(player) addListener(eventsDispatcherListener) } @@ -70,55 +73,19 @@ class QoSEventsDispatcherTest { TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) - val onSessionCreated = mutableListOf() - val onCurrentSession = mutableListOf() - val onIsPlayingSessions = mutableListOf() + val onIsPlayingSessions = mutableListOf() val onIsPlayingValue = mutableListOf() verify { - eventsDispatcherListener.onSessionCreated(capture(onSessionCreated)) - eventsDispatcherListener.onCurrentSession(capture(onCurrentSession)) eventsDispatcherListener.onIsPlaying(capture(onIsPlayingSessions), capture(onIsPlayingValue)) } confirmVerified(eventsDispatcherListener) - assertEquals(1, onSessionCreated.size) - assertEquals(1, onCurrentSession.size) assertEquals(2, onIsPlayingValue.size) - - assertEquals(1, onSessionCreated.distinctBy { it.sessionId }.size) - assertEquals(1, onCurrentSession.distinctBy { it.sessionId }.size) assertEquals(1, onIsPlayingSessions.distinctBy { it.sessionId }.size) - - assertEquals(listOf(mediaItem), onSessionCreated.map { it.mediaItem }) - assertEquals(listOf(mediaItem), onCurrentSession.map { it.mediaItem }) assertEquals(listOf(true, false), onIsPlayingValue) } - @Test - fun `play single media item, remove media item`() { - val mediaItem = MediaItem.fromUri(VOD1) - - player.setMediaItem(mediaItem) - player.play() - player.removeMediaItem(player.currentMediaItemIndex) - - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) - - val sessions = mutableListOf() - - verifyOrder { - eventsDispatcherListener.onSessionCreated(capture(sessions)) - eventsDispatcherListener.onCurrentSession(capture(sessions)) - eventsDispatcherListener.onSessionFinished(capture(sessions)) - } - confirmVerified(eventsDispatcherListener) - - assertEquals(3, sessions.size) - assertEquals(1, sessions.distinctBy { it.sessionId }.size) - assertTrue(sessions.all { it.mediaItem == mediaItem }) - } - @Test fun `play multiple media items`() { val mediaItems = listOf(VOD1, VOD2, VOD3).map { MediaItem.fromUri(it) } @@ -133,40 +100,22 @@ class QoSEventsDispatcherTest { TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) - val onSessionCreated = mutableListOf() - val onCurrentSession = mutableListOf() - val onSessionFinished = mutableListOf() - val onIsPlayingSessions = mutableListOf() + val onIsPlayingSessions = mutableListOf() val onIsPlayingValue = mutableListOf() verify { - eventsDispatcherListener.onSessionCreated(capture(onSessionCreated)) - eventsDispatcherListener.onCurrentSession(capture(onCurrentSession)) - eventsDispatcherListener.onSessionFinished(capture(onSessionFinished)) eventsDispatcherListener.onIsPlaying(capture(onIsPlayingSessions), capture(onIsPlayingValue)) } confirmVerified(eventsDispatcherListener) - assertEquals(3, onSessionCreated.size) - assertEquals(3, onCurrentSession.size) - assertEquals(3, onSessionFinished.size) assertEquals(6, onIsPlayingValue.size) - - assertEquals(3, onSessionCreated.distinctBy { it.sessionId }.size) - assertEquals(3, onCurrentSession.distinctBy { it.sessionId }.size) - assertEquals(3, onSessionFinished.distinctBy { it.sessionId }.size) assertEquals(3, onIsPlayingSessions.distinctBy { it.sessionId }.size) - - assertEquals(mediaItems, onSessionCreated.map { it.mediaItem }) - assertEquals(mediaItems, onCurrentSession.map { it.mediaItem }) - assertEquals(mediaItems, onSessionFinished.map { it.mediaItem }) assertEquals(listOf(true, false, true, false, true, false), onIsPlayingValue) } @Test fun `play multiple media items, remove upcoming media item`() { val mediaItems = listOf(VOD1, VOD2, VOD3).map { MediaItem.fromUri(it) } - val expectedMediaItems = listOf(mediaItems[0], mediaItems[2]) player.setMediaItems(mediaItems) player.play() @@ -179,33 +128,16 @@ class QoSEventsDispatcherTest { TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) - val createdSessions = mutableListOf() - val currentSessions = mutableListOf() - val finishedSessions = mutableListOf() - val onIsPlayingSessions = mutableListOf() + val onIsPlayingSessions = mutableListOf() val onIsPlayingValue = mutableListOf() verify { - eventsDispatcherListener.onSessionCreated(capture(createdSessions)) - eventsDispatcherListener.onCurrentSession(capture(currentSessions)) - eventsDispatcherListener.onSessionFinished(capture(finishedSessions)) eventsDispatcherListener.onIsPlaying(capture(onIsPlayingSessions), capture(onIsPlayingValue)) } confirmVerified(eventsDispatcherListener) - assertEquals(2, createdSessions.size) - assertEquals(2, currentSessions.size) - assertEquals(2, finishedSessions.size) assertEquals(4, onIsPlayingValue.size) - - assertEquals(2, createdSessions.distinctBy { it.sessionId }.size) - assertEquals(2, currentSessions.distinctBy { it.sessionId }.size) - assertEquals(2, finishedSessions.distinctBy { it.sessionId }.size) assertEquals(2, onIsPlayingSessions.distinctBy { it.sessionId }.size) - - assertEquals(expectedMediaItems, createdSessions.map { it.mediaItem }) - assertEquals(expectedMediaItems, currentSessions.map { it.mediaItem }) - assertEquals(expectedMediaItems, finishedSessions.map { it.mediaItem }) assertEquals(listOf(true, false, true, false), onIsPlayingValue) } @@ -224,33 +156,16 @@ class QoSEventsDispatcherTest { TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) - val createdSessions = mutableListOf() - val currentSessions = mutableListOf() - val finishedSessions = mutableListOf() - val onIsPlayingSessions = mutableListOf() + val onIsPlayingSessions = mutableListOf() val onIsPlayingValue = mutableListOf() verify { - eventsDispatcherListener.onSessionCreated(capture(createdSessions)) - eventsDispatcherListener.onCurrentSession(capture(currentSessions)) - eventsDispatcherListener.onSessionFinished(capture(finishedSessions)) eventsDispatcherListener.onIsPlaying(capture(onIsPlayingSessions), capture(onIsPlayingValue)) } confirmVerified(eventsDispatcherListener) - assertEquals(3, createdSessions.size) - assertEquals(3, currentSessions.size) - assertEquals(3, finishedSessions.size) assertEquals(4, onIsPlayingValue.size) - - assertEquals(3, createdSessions.distinctBy { it.sessionId }.size) - assertEquals(3, currentSessions.distinctBy { it.sessionId }.size) - assertEquals(3, finishedSessions.distinctBy { it.sessionId }.size) assertEquals(2, onIsPlayingSessions.distinctBy { it.sessionId }.size) - - assertEquals(mediaItems, createdSessions.map { it.mediaItem }) - assertEquals(mediaItems, currentSessions.map { it.mediaItem }) - assertEquals(mediaItems, finishedSessions.map { it.mediaItem }) assertEquals(listOf(true, false, true, false), onIsPlayingValue) } @@ -268,33 +183,16 @@ class QoSEventsDispatcherTest { TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player) - val createdSessions = mutableListOf() - val currentSessions = mutableListOf() - val finishedSessions = mutableListOf() - val onIsPlayingSessions = mutableListOf() + val onIsPlayingSessions = mutableListOf() val onIsPlayingValue = mutableListOf() verify { - eventsDispatcherListener.onSessionCreated(capture(createdSessions)) - eventsDispatcherListener.onCurrentSession(capture(currentSessions)) - eventsDispatcherListener.onSessionFinished(capture(finishedSessions)) eventsDispatcherListener.onIsPlaying(capture(onIsPlayingSessions), capture(onIsPlayingValue)) } confirmVerified(eventsDispatcherListener) - assertEquals(3, createdSessions.size) - assertEquals(3, currentSessions.size) - assertEquals(3, finishedSessions.size) assertEquals(6, onIsPlayingValue.size) - - assertEquals(3, createdSessions.distinctBy { it.sessionId }.size) - assertEquals(3, currentSessions.distinctBy { it.sessionId }.size) - assertEquals(3, finishedSessions.distinctBy { it.sessionId }.size) assertEquals(3, onIsPlayingSessions.distinctBy { it.sessionId }.size) - - assertEquals(mediaItems, createdSessions.map { it.mediaItem }) - assertEquals(mediaItems, currentSessions.map { it.mediaItem }) - assertEquals(mediaItems, finishedSessions.map { it.mediaItem }) assertEquals(listOf(true, false, true, false, true, false), onIsPlayingValue) } diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/StartupTimesTrackerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/StartupTimesTrackerTest.kt index a2da6937c..a12eb39f5 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/StartupTimesTrackerTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/StartupTimesTrackerTest.kt @@ -13,6 +13,7 @@ import androidx.media3.test.utils.robolectric.TestPlayerRunHelper import androidx.test.core.app.ApplicationProvider import ch.srgssr.pillarbox.player.PillarboxExoPlayer import ch.srgssr.pillarbox.player.analytics.MetricsCollector +import ch.srgssr.pillarbox.player.analytics.PlaybackSessionManager import org.junit.runner.RunWith import org.robolectric.ParameterizedRobolectricTestRunner import org.robolectric.ParameterizedRobolectricTestRunner.Parameters @@ -71,9 +72,10 @@ class StartupTimesTrackerTest( coroutineContext = coroutineContext, ).apply { val mediaItems = mediaUrls.map(MediaItem::fromUri) - val eventsDispatcher = PillarboxEventsDispatcher() - eventsDispatcher.addListener(object : QoSEventsDispatcher.Listener { - override fun onSessionCreated(session: QoSEventsDispatcher.Session) { + val sessionManager = PlaybackSessionManager() + sessionManager.registerPlayer(this) + sessionManager.addListener(object : PlaybackSessionManager.Listener { + override fun onSessionCreated(session: PlaybackSessionManager.Session) { sessionId = session.sessionId } }) @@ -81,10 +83,11 @@ class StartupTimesTrackerTest( QoSCoordinator( context = context, player = this, - eventsDispatcher = eventsDispatcher, + eventsDispatcher = PillarboxEventsDispatcher(sessionManager), startupTimesTracker = startupTimesTracker, metricsCollector = MetricsCollector(this), messageHandler = DummyQoSHandler, + sessionManager = sessionManager, coroutineContext = coroutineContext, )