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 c7e2b866f..03ad268f2 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 @@ -116,31 +116,33 @@ class PillarboxExoPlayer internal constructor( ) init { - addAnalyticsListener( - PlaybackSessionManager().apply { - this.listener = object : PlaybackSessionManager.Listener { - private val TAG = "SessionManager" - private fun PlaybackSessionManager.Session.prettyString(): String { - return "$sessionId / ${mediaItem.mediaMetadata.title}" - } + val qoSSessionAnalyticsListener = QoSSessionAnalyticsListener(context, ::handleQoSSession) + val sessionManagerListener = object : PlaybackSessionManager.Listener { + private val TAG = "SessionManager" + private fun PlaybackSessionManager.Session.prettyString(): String { + return "$sessionId / ${mediaItem.mediaMetadata.title}" + } - override fun onSessionCreated(session: PlaybackSessionManager.Session) { - Log.i(TAG, "onSessionCreated ${session.prettyString()}") - } + override fun onSessionCreated(session: PlaybackSessionManager.Session) { + Log.i(TAG, "onSessionCreated ${session.prettyString()}") + qoSSessionAnalyticsListener.onSessionCreated(session) + } - override fun onSessionFinished(session: PlaybackSessionManager.Session) { - Log.i(TAG, "onSessionFinished ${session.prettyString()}") - } + override fun onSessionFinished(session: PlaybackSessionManager.Session) { + Log.i(TAG, "onSessionFinished ${session.prettyString()}") + qoSSessionAnalyticsListener.onSessionFinished(session) + } - override fun onCurrentSession(session: PlaybackSessionManager.Session) { - Log.i(TAG, "onCurrentSession ${session.prettyString()}") - } - } + override fun onCurrentSession(session: PlaybackSessionManager.Session) { + Log.i(TAG, "onCurrentSession ${session.prettyString()}") + qoSSessionAnalyticsListener.onCurrentSession(session) } - ) + } + + addAnalyticsListener(PlaybackSessionManager(sessionManagerListener)) addListener(analyticsCollector) exoPlayer.addListener(ComponentListener()) - exoPlayer.addAnalyticsListener(QoSSessionAnalyticsListener(context, ::handleQoSSession)) + exoPlayer.addAnalyticsListener(qoSSessionAnalyticsListener) itemPillarboxDataTracker.addCallback(timeRangeTracker) itemPillarboxDataTracker.addCallback(analyticsTracker) if (BuildConfig.DEBUG) { 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 872d727a9..bfbdbc628 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 @@ -6,7 +6,7 @@ package ch.srgssr.pillarbox.player.analytics import androidx.media3.common.MediaItem import androidx.media3.common.Player -import androidx.media3.common.Timeline +import androidx.media3.common.Player.TimelineChangeReason import androidx.media3.common.Timeline.Window import androidx.media3.exoplayer.analytics.AnalyticsListener import androidx.media3.exoplayer.analytics.AnalyticsListener.EventTime @@ -22,8 +22,12 @@ import java.util.UUID * - Session is created when the player does something with a [MediaItem]. * - Session is current if the media item associated with session is the current [MediaItem]. * - Session is finished when it is no longer the current session or when the session is removed from the player. + * + * @param listener The listener attached to the session manager. */ -class PlaybackSessionManager : AnalyticsListener { +class PlaybackSessionManager( + private val listener: Listener, +) : AnalyticsListener { /** * Listener */ @@ -78,11 +82,6 @@ class PlaybackSessionManager : AnalyticsListener { private val sessions = HashMap() private val window = Window() - /** - * Listener - */ - var listener: Listener? = null - /** * Current session */ @@ -90,12 +89,12 @@ class PlaybackSessionManager : AnalyticsListener { private set(value) { if (field != value) { field?.let { - listener?.onSessionFinished(it) + listener.onSessionFinished(it) sessions.remove(it.sessionId) } field = value field?.let { - listener?.onCurrentSession(it) + listener.onCurrentSession(it) } } } @@ -111,7 +110,7 @@ class PlaybackSessionManager : AnalyticsListener { if (session == null) { val newSession = Session(mediaItem) sessions[newSession.sessionId] = newSession - listener?.onSessionCreated(newSession) + listener.onSessionCreated(newSession) if (currentSession == null) { currentSession = newSession } @@ -143,7 +142,7 @@ class PlaybackSessionManager : AnalyticsListener { currentSession = mediaItem?.let { getOrCreateSession(it) } } - override fun onTimelineChanged(eventTime: EventTime, reason: Int) { + override fun onTimelineChanged(eventTime: EventTime, @TimelineChangeReason reason: Int) { DebugLogger.debug(TAG, "onTimelineChanged ${StringUtil.timelineChangeReasonString(reason)} ${eventTime.getMediaItem().mediaMetadata.title}") if (eventTime.timeline.isEmpty) { finishAllSession() @@ -161,7 +160,7 @@ class PlaybackSessionManager : AnalyticsListener { if (matchingItem == null) { if (session == currentSession) currentSession = null else { - listener?.onSessionFinished(session) + listener.onSessionFinished(session) this.sessions.remove(session.sessionId) } } @@ -169,7 +168,6 @@ class PlaybackSessionManager : AnalyticsListener { } override fun onLoadStarted(eventTime: EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData) { - if (eventTime.timeline.isEmpty) return val mediaItem = eventTime.getMediaItem() if (mediaItem != MediaItem.EMPTY) { getOrCreateSession(mediaItem) @@ -184,7 +182,7 @@ class PlaybackSessionManager : AnalyticsListener { private fun finishAllSession() { currentSession = null for (session in sessions.values) { - listener?.onSessionFinished(session) + listener.onSessionFinished(session) } sessions.clear() } @@ -195,7 +193,7 @@ class PlaybackSessionManager : AnalyticsListener { private fun EventTime.getMediaItem(): MediaItem { if (timeline.isEmpty) return MediaItem.EMPTY - return timeline.getWindow(windowIndex, Timeline.Window()).mediaItem + return timeline.getWindow(windowIndex, Window()).mediaItem } private fun MediaItem.isTheSame(mediaItem: MediaItem): Boolean { diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSSessionAnalyticsListener.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSSessionAnalyticsListener.kt index 5276b97f2..e496bd512 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSSessionAnalyticsListener.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSSessionAnalyticsListener.kt @@ -8,12 +8,11 @@ import android.content.Context import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Timeline -import androidx.media3.common.Tracks 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 java.util.UUID import kotlin.time.Duration.Companion.milliseconds internal class QoSSessionAnalyticsListener( @@ -21,22 +20,39 @@ internal class QoSSessionAnalyticsListener( private val onQoSSessionReady: (qosSession: QoSSession) -> Unit, ) : AnalyticsListener { private val loadingSessions = mutableSetOf() + private val mediaIdToSessionId = mutableMapOf() + private val currentSessionToMediaStart = mutableMapOf() private val qosSessions = mutableMapOf() private val window = Timeline.Window() - @Suppress("ReturnCount") + fun onSessionCreated(session: PlaybackSessionManager.Session) { + loadingSessions.add(session.sessionId) + mediaIdToSessionId[session.mediaItem.mediaId] = session.sessionId + qosSessions[session.sessionId] = QoSSession( + context = context, + mediaId = session.mediaItem.mediaId, + mediaSource = session.mediaItem.localConfiguration?.uri?.toString().orEmpty(), + ) + } + + fun onCurrentSession(session: PlaybackSessionManager.Session) { + currentSessionToMediaStart[session.sessionId] = System.currentTimeMillis() + } + + fun onSessionFinished(session: PlaybackSessionManager.Session) { + loadingSessions.remove(session.sessionId) + mediaIdToSessionId.remove(session.mediaItem.mediaId) + qosSessions.remove(session.sessionId) + } + override fun onLoadCompleted( eventTime: AnalyticsListener.EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData, ) { - val mediaItem = getMediaItem(eventTime) ?: return - val sessionId = getSessionId(mediaItem) - - if (sessionId !in qosSessions) { - loadingSessions.add(sessionId) - qosSessions[sessionId] = createQoSSession(mediaItem) - } else if (sessionId !in loadingSessions) { + val mediaItem = getMediaItem(eventTime) + val sessionId = mediaIdToSessionId[mediaItem?.mediaId] + if (sessionId == null || sessionId !in loadingSessions || sessionId !in qosSessions) { return } @@ -46,32 +62,49 @@ internal class QoSSessionAnalyticsListener( val timings = when (mediaLoadData.dataType) { C.DATA_TYPE_DRM -> initialTimings.copy(drm = initialTimings.drm + loadDuration) - C.DATA_TYPE_MEDIA -> initialTimings.copy(mediaSource = initialTimings.mediaSource + loadDuration) + C.DATA_TYPE_MANIFEST, C.DATA_TYPE_MEDIA -> initialTimings.copy(mediaSource = initialTimings.mediaSource + loadDuration) PillarboxMediaSource.DATA_TYPE_CUSTOM_ASSET -> initialTimings.copy(asset = initialTimings.asset + loadDuration) - else -> return + else -> initialTimings } qosSessions[sessionId] = qosSession.copy(timings = timings) } - override fun onTracksChanged( + override fun onAudioPositionAdvancing( eventTime: AnalyticsListener.EventTime, - tracks: Tracks, + playoutStartSystemTimeMs: Long, ) { - val mediaItem = getMediaItem(eventTime) ?: return - val sessionId = getSessionId(mediaItem) + notifyQoSSessionReady(eventTime) + } - if (loadingSessions.remove(sessionId)) { - qosSessions[sessionId]?.let(onQoSSessionReady) - } + override fun onRenderedFirstFrame( + eventTime: AnalyticsListener.EventTime, + output: Any, + renderTimeMs: Long, + ) { + notifyQoSSessionReady(eventTime) } - private fun getSessionId(mediaItem: MediaItem): String { - val mediaId = mediaItem.mediaId - val mediaUrl = mediaItem.localConfiguration?.uri?.toString().orEmpty() - val name = (mediaId + mediaUrl).toByteArray() + private fun notifyQoSSessionReady(eventTime: AnalyticsListener.EventTime) { + val mediaItem = getMediaItem(eventTime) + val sessionId = mediaIdToSessionId[mediaItem?.mediaId] ?: return + + if (loadingSessions.remove(sessionId)) { + qosSessions[sessionId]?.let { + val qosSession = if (sessionId in currentSessionToMediaStart) { + it.copy( + timings = it.timings.copy( + currentToStart = (System.currentTimeMillis() - currentSessionToMediaStart.getValue(sessionId)).milliseconds, + ), + ) + } else { + it + } - return UUID.nameUUIDFromBytes(name).toString() + currentSessionToMediaStart.remove(sessionId) + onQoSSessionReady(qosSession) + } + } } private fun getMediaItem(eventTime: AnalyticsListener.EventTime): MediaItem? { @@ -81,12 +114,4 @@ internal class QoSSessionAnalyticsListener( eventTime.timeline.getWindow(eventTime.windowIndex, window).mediaItem } } - - private fun createQoSSession(mediaItem: MediaItem): QoSSession { - return QoSSession( - context = context, - mediaId = mediaItem.mediaId, - mediaSource = mediaItem.localConfiguration?.uri?.toString().orEmpty(), - ) - } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSSessionTimings.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSSessionTimings.kt index 5ce1ace30..6db8bf8b1 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSSessionTimings.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSSessionTimings.kt @@ -10,25 +10,24 @@ import kotlin.time.Duration * Represents the timings until the current media started to play. * * @property asset The time spent to load the asset. + * @property currentToStart The time spent to load from the moment the [MediaItem][androidx.media3.common.MediaItem] became the current item until it + * started to play. * @property drm The time spent to load the DRM. * @property mediaSource The time spent to load the media source. */ data class QoSSessionTimings( val asset: Duration, + val currentToStart: Duration, val drm: Duration, val mediaSource: Duration, ) { - /** - * The total time spent to load all the components. - */ - val total = asset + drm + mediaSource - companion object { /** * Default [QoSSessionTimings] where all fields are a duration of zero. */ val Zero = QoSSessionTimings( asset = Duration.ZERO, + currentToStart = Duration.ZERO, drm = Duration.ZERO, mediaSource = Duration.ZERO, ) 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..52ab7500e --- /dev/null +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/analytics/PlaybackSessionManagerTest.kt @@ -0,0 +1,205 @@ +/* + * 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 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 ch.srgssr.pillarbox.player.test.utils.TestPillarboxRunHelper +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 +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +@RunWith(AndroidJUnit4::class) +class PlaybackSessionManagerTest { + private lateinit var clock: FakeClock + private lateinit var player: Player + 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 { + addAnalyticsListener(PlaybackSessionManager(sessionManagerListener)) + prepare() + } + } + + @Test + fun `play single media item`() { + val mediaItem = MediaItem.fromUri(VOD1) + + player.setMediaItem(mediaItem) + player.play() + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + + val sessions = mutableListOf() + + verifyOrder { + sessionManagerListener.onSessionCreated(capture(sessions)) + sessionManagerListener.onCurrentSession(capture(sessions)) + } + confirmVerified(sessionManagerListener) + + assertEquals(2, sessions.size) + assertEquals(1, sessions.distinctBy { it.sessionId }.size) + assertTrue(sessions.all { it.mediaItem == mediaItem }) + } + + @Test + fun `play single media item, remove media item`() { + val mediaItem = MediaItem.fromUri(VOD1) + + player.setMediaItem(mediaItem) + player.play() + + TestPillarboxRunHelper.runUntilPosition(player, 5.seconds, clock) + + player.removeMediaItem(0) + + 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) + + val sessions = mutableListOf() + + verifyOrder { + sessionManagerListener.onSessionCreated(capture(sessions)) // Item 1 + sessionManagerListener.onCurrentSession(capture(sessions)) // Item 1 + sessionManagerListener.onSessionCreated(capture(sessions)) // Item 2 + sessionManagerListener.onSessionFinished(capture(sessions)) // Item 1 + sessionManagerListener.onCurrentSession(capture(sessions)) // Item 2 + sessionManagerListener.onSessionCreated(capture(sessions)) // Item 3 + sessionManagerListener.onSessionFinished(capture(sessions)) // Item 2 + sessionManagerListener.onCurrentSession(capture(sessions)) // Item 3 + } + confirmVerified(sessionManagerListener) + + assertEquals(8, sessions.size) + assertEquals(3, sessions.distinctBy { it.sessionId }.size) + assertEquals( + listOf(mediaItems[0], mediaItems[0], mediaItems[1], mediaItems[0], mediaItems[1], mediaItems[2], mediaItems[1], mediaItems[2]), + sessions.map { it.mediaItem }.reversed(), + ) + } + + @Test + fun `play multiple media items, remove upcoming media item`() { + val mediaItems = listOf(VOD1, VOD2, VOD3).map { MediaItem.fromUri(it) } + + player.setMediaItems(mediaItems) + player.play() + + TestPillarboxRunHelper.runUntilPosition(player, 5.seconds, clock) + + player.removeMediaItem(1) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + + val sessions = mutableListOf() + + verifyOrder { + sessionManagerListener.onSessionCreated(capture(sessions)) // Item 1 + sessionManagerListener.onCurrentSession(capture(sessions)) // Item 1 + sessionManagerListener.onSessionCreated(capture(sessions)) // Item 3 + sessionManagerListener.onSessionFinished(capture(sessions)) // Item 1 + sessionManagerListener.onCurrentSession(capture(sessions)) // Item 3 + } + confirmVerified(sessionManagerListener) + + assertEquals(5, sessions.size) + assertEquals(2, sessions.distinctBy { it.sessionId }.size) + assertEquals( + listOf(mediaItems[0], mediaItems[0], mediaItems[2], mediaItems[0], mediaItems[2]), + sessions.map { it.mediaItem }.reversed(), + ) + } + + @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() + + TestPillarboxRunHelper.runUntilPosition(player, 5.seconds, clock) + + player.removeMediaItem(0) + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED) + + val sessions = mutableListOf() + + verifyOrder { + sessionManagerListener.onSessionCreated(capture(sessions)) // Item 1 + sessionManagerListener.onCurrentSession(capture(sessions)) // Item 1 + sessionManagerListener.onSessionFinished(capture(sessions)) // Item 1 + sessionManagerListener.onSessionCreated(capture(sessions)) // Item 2 + sessionManagerListener.onCurrentSession(capture(sessions)) // Item 2 + sessionManagerListener.onSessionCreated(capture(sessions)) // Item 3 + sessionManagerListener.onSessionFinished(capture(sessions)) // Item 2 + sessionManagerListener.onCurrentSession(capture(sessions)) // Item 3 + } + confirmVerified(sessionManagerListener) + + assertEquals(8, sessions.size) + assertEquals(3, sessions.distinctBy { it.sessionId }.size) + assertEquals( + listOf(mediaItems[0], mediaItems[0], mediaItems[0], mediaItems[1], mediaItems[1], mediaItems[2], mediaItems[1], mediaItems[2]), + sessions.map { it.mediaItem }.reversed(), + ) + } + + @AfterTest + fun tearDown() { + player.release() + } + + 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/QoSSessionAnalyticsListenerTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/QoSSessionAnalyticsListenerTest.kt index acf2af49c..d60d59407 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/QoSSessionAnalyticsListenerTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/QoSSessionAnalyticsListenerTest.kt @@ -6,18 +6,22 @@ package ch.srgssr.pillarbox.player.qos import android.content.Context import android.os.Looper +import android.view.SurfaceView +import android.view.ViewGroup import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.test.utils.FakeClock 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.PlaybackSessionManager import org.junit.runner.RunWith import org.robolectric.ParameterizedRobolectricTestRunner import org.robolectric.ParameterizedRobolectricTestRunner.Parameters import org.robolectric.Shadows.shadowOf import kotlin.test.AfterTest import kotlin.test.BeforeTest +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.Duration.Companion.seconds @@ -35,6 +39,12 @@ class QoSSessionAnalyticsListenerTest( qosSessions.add(it) } + // Attach the Player to a surface + val surfaceView = SurfaceView(ApplicationProvider.getApplicationContext()) + surfaceView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + + player.setVideoSurfaceView(surfaceView) + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY) mediaUrls.forEachIndexed { index, _ -> @@ -44,6 +54,7 @@ class QoSSessionAnalyticsListenerTest( } @Test + @Ignore("SurfaceView/SurfaceHolder not implemented in Robolectric") fun `qos session analytics listener`() { assertEquals(mediaUrls, qosSessions.map { it.mediaSource }) } @@ -61,7 +72,21 @@ class QoSSessionAnalyticsListenerTest( callback: (qosSession: QoSSession) -> Unit, ): Player { val context = ApplicationProvider.getApplicationContext() - val listener = QoSSessionAnalyticsListener(context, callback) + val qosSessionAnalyticsListener = QoSSessionAnalyticsListener(context, callback) + val playbackSessionManagerListener = object : PlaybackSessionManager.Listener { + override fun onSessionCreated(session: PlaybackSessionManager.Session) { + qosSessionAnalyticsListener.onSessionCreated(session) + } + + override fun onCurrentSession(session: PlaybackSessionManager.Session) { + qosSessionAnalyticsListener.onCurrentSession(session) + } + + override fun onSessionFinished(session: PlaybackSessionManager.Session) { + qosSessionAnalyticsListener.onSessionFinished(session) + } + } + val playbackSessionManager = PlaybackSessionManager(playbackSessionManagerListener) return PillarboxExoPlayer( context = context, @@ -70,7 +95,8 @@ class QoSSessionAnalyticsListenerTest( val mediaItems = mediaUrls.map(MediaItem::fromUri) addMediaItems(mediaItems) - addAnalyticsListener(listener) + addAnalyticsListener(qosSessionAnalyticsListener) + addAnalyticsListener(playbackSessionManager) prepare() play() } diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/QoSSessionTimingsTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/QoSSessionTimingsTest.kt index ae4e19e87..4925dbe4c 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/QoSSessionTimingsTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/QoSSessionTimingsTest.kt @@ -7,7 +7,6 @@ package ch.srgssr.pillarbox.player.qos import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds class QoSSessionTimingsTest { @Test @@ -15,19 +14,8 @@ class QoSSessionTimingsTest { val timings = QoSSessionTimings.Zero assertEquals(Duration.ZERO, timings.asset) + assertEquals(Duration.ZERO, timings.currentToStart) assertEquals(Duration.ZERO, timings.drm) assertEquals(Duration.ZERO, timings.mediaSource) - assertEquals(Duration.ZERO, timings.total) - } - - @Test - fun `timings compute total value`() { - val timings = QoSSessionTimings( - asset = 250.milliseconds, - drm = 33.milliseconds, - mediaSource = 100.milliseconds, - ) - - assertEquals(383.milliseconds, timings.total) } }