Skip to content

Commit

Permalink
Merge branch 'qos' into 604-collect-stalls-metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
StaehliJ authored Jun 27, 2024
2 parents c9f083c + 1a41bf2 commit 2d2120a
Show file tree
Hide file tree
Showing 7 changed files with 329 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
*/
Expand Down Expand Up @@ -78,24 +82,19 @@ class PlaybackSessionManager : AnalyticsListener {
private val sessions = HashMap<String, Session>()
private val window = Window()

/**
* Listener
*/
var listener: Listener? = null

/**
* Current session
*/
var currentSession: Session? = null
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)
}
}
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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()
Expand All @@ -161,15 +160,14 @@ class PlaybackSessionManager : AnalyticsListener {
if (matchingItem == null) {
if (session == currentSession) currentSession = null
else {
listener?.onSessionFinished(session)
listener.onSessionFinished(session)
this.sessions.remove(session.sessionId)
}
}
}
}

override fun onLoadStarted(eventTime: EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData) {
if (eventTime.timeline.isEmpty) return
val mediaItem = eventTime.getMediaItem()
if (mediaItem != MediaItem.EMPTY) {
getOrCreateSession(mediaItem)
Expand All @@ -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()
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,51 @@ 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(
private val context: Context,
private val onQoSSessionReady: (qosSession: QoSSession) -> Unit,
) : AnalyticsListener {
private val loadingSessions = mutableSetOf<String>()
private val mediaIdToSessionId = mutableMapOf<String, String>()
private val currentSessionToMediaStart = mutableMapOf<String, Long>()
private val qosSessions = mutableMapOf<String, QoSSession>()
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
}

Expand All @@ -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? {
Expand All @@ -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(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
Loading

0 comments on commit 2d2120a

Please sign in to comment.