From ab1dafe1f056c798cf5e854a9449547b18b74b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Tue, 30 Jul 2024 11:23:03 +0200 Subject: [PATCH] Update models for QoS (#654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joaquim Stähli --- .../pillarbox/player/qos/QoSCoordinator.kt | 36 ++- .../pillarbox/player/qos/QoSMessageHandler.kt | 1 + .../pillarbox/player/qos/models/QoSDevice.kt | 30 +++ .../pillarbox/player/qos/models/QoSError.kt | 46 ++++ .../pillarbox/player/qos/models/QoSEvent.kt | 26 ++ .../pillarbox/player/qos/models/QoSMedia.kt | 20 ++ .../pillarbox/player/qos/models/QoSMessage.kt | 20 ++ .../pillarbox/player/qos/models/QoSOS.kt | 16 ++ .../pillarbox/player/qos/models/QoSPlayer.kt | 18 ++ .../pillarbox/player/qos/models/QoSScreen.kt | 16 ++ .../pillarbox/player/qos/models/QoSSession.kt | 110 ++++++++ .../player/qos/models/QoSSessionTimings.kt | 35 +++ .../pillarbox/player/qos/models/QoSStall.kt | 16 ++ .../player/qos/models/QoSErrorTest.kt | 58 +++++ .../player/qos/models/QoSSessionTest.kt | 241 ++++++++++++++++++ .../qos/models/QoSSessionTimingsTest.kt | 20 ++ 16 files changed, 698 insertions(+), 11 deletions(-) create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSDevice.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSError.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSEvent.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSMedia.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSMessage.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSOS.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSPlayer.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSScreen.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSSession.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSSessionTimings.kt create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSStall.kt create mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/models/QoSErrorTest.kt create mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/models/QoSSessionTest.kt create mode 100644 pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/models/QoSSessionTimingsTest.kt 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 ebbeb1ae1..975b73ebc 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 @@ -15,6 +15,13 @@ import ch.srgssr.pillarbox.player.analytics.PillarboxAnalyticsListener import ch.srgssr.pillarbox.player.analytics.PlaybackSessionManager import ch.srgssr.pillarbox.player.analytics.metrics.MetricsCollector import ch.srgssr.pillarbox.player.analytics.metrics.PlaybackMetrics +import ch.srgssr.pillarbox.player.qos.models.QoSError +import ch.srgssr.pillarbox.player.qos.models.QoSEvent +import ch.srgssr.pillarbox.player.qos.models.QoSMedia +import ch.srgssr.pillarbox.player.qos.models.QoSMessage +import ch.srgssr.pillarbox.player.qos.models.QoSSession +import ch.srgssr.pillarbox.player.qos.models.QoSSessionTimings +import ch.srgssr.pillarbox.player.qos.models.QoSStall import ch.srgssr.pillarbox.player.runOnApplicationLooper import ch.srgssr.pillarbox.player.utils.BitrateUtil.toByteRate import ch.srgssr.pillarbox.player.utils.DebugLogger @@ -63,11 +70,11 @@ internal class QoSCoordinator( sessionManager.getSessionById(metrics.sessionId)?.let { sendStartEvent( session = it, - timings = QoSSessionTimings( - asset = metrics.loadDuration.asset, - mediaSource = metrics.loadDuration.source, - currentToStart = metrics.loadDuration.timeToReady, - drm = metrics.loadDuration.drm + timeMetrics = QoSSessionTimings( + asset = metrics.loadDuration.source, + drm = metrics.loadDuration.drm, + metadata = metrics.loadDuration.asset, + total = metrics.loadDuration.timeToReady, ) ) } @@ -126,8 +133,10 @@ internal class QoSCoordinator( bufferDuration = player.totalBufferedDuration, playbackDuration = playbackDuration.inWholeMilliseconds, playerPosition = player.currentPosition, - stallCount = stallCount, - stallDuration = stallDuration.inWholeSeconds, + stall = QoSStall( + count = stallCount, + duration = stallDuration.inWholeMilliseconds, + ), url = url.toString(), ) } @@ -166,6 +175,7 @@ internal class QoSCoordinator( throwable = it, playerPosition = player.currentPosition, severity = QoSError.Severity.FATAL, + url = url, ), ) } @@ -180,16 +190,20 @@ internal class QoSCoordinator( private fun sendStartEvent( session: PlaybackSessionManager.Session, - timings: QoSSessionTimings, + timeMetrics: QoSSessionTimings, ) { sendEvent( eventName = "START", session = session, data = QoSSession( context = context, - mediaId = session.mediaItem.mediaId, - mediaSource = session.mediaItem.localConfiguration?.uri.toString(), - timings = timings, + media = QoSMedia( + assetUrl = url, + id = session.mediaItem.mediaId, + metadataUrl = session.mediaItem.localConfiguration?.uri.toString(), + origin = context.packageName, + ), + timeMetrics = timeMetrics, ), ) } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSMessageHandler.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSMessageHandler.kt index 5e4860225..0d282775d 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSMessageHandler.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/QoSMessageHandler.kt @@ -5,6 +5,7 @@ package ch.srgssr.pillarbox.player.qos import android.util.Log +import ch.srgssr.pillarbox.player.qos.models.QoSMessage /** * QoS message handler diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSDevice.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSDevice.kt new file mode 100644 index 000000000..92a9524a0 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSDevice.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +/** + * Information about the device. + * + * @property id The unique identifier of the device. + * @property model The model of the device. + * @property type The type of device. + */ +data class QoSDevice( + val id: String, + val model: String, + val type: DeviceType, +) { + /** + * The type of device. + */ + enum class DeviceType { + CAR, + DESKTOP, + PHONE, + TABLET, + TV, + UNKNOWN, + } +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSError.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSError.kt new file mode 100644 index 000000000..64bbc9b24 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSError.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +/** + * Represents a [Player][androidx.media3.common.Player] error to send to a QoS server. + * + * @property log The log associated with the error. + * @property message The error message. + * @property name The name of the error. + * @property playerPosition The position of the player when the error occurred, in milliseconds, or `null` if not available. + * @property severity The severity of the error, either [FATAL][Severity.FATAL] or [WARNING][Severity.WARNING]. + * @property url The last loaded url. + */ +data class QoSError( + val log: String, + val message: String, + val name: String, + val playerPosition: Long?, + val severity: Severity, + val url: String, +) { + /** + * Represents a [Player][androidx.media3.common.Player] error severity. + */ + enum class Severity { + FATAL, + WARNING, + } + + constructor( + throwable: Throwable, + playerPosition: Long?, + severity: Severity, + url: String, + ) : this( + log = throwable.stackTraceToString(), + message = throwable.message.orEmpty(), + name = throwable::class.simpleName.orEmpty(), + playerPosition = playerPosition, + severity = severity, + url = url, + ) +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSEvent.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSEvent.kt new file mode 100644 index 000000000..2fc58aef3 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSEvent.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +/** + * Represents a generic event, which contains metrics about the current media stream. + * + * @property bandwidth The device-measured network bandwidth, in bytes per second. + * @property bitrate The bitrate of the current stream, in bytes per second. + * @property bufferDuration The forward duration of the buffer, in milliseconds. + * @property playbackDuration The duration of the playback, in milliseconds. + * @property playerPosition The position of the player, in milliseconds. + * @property stall The information about stalls. + * @property url The URL of the stream. + */ +data class QoSEvent( + val bandwidth: Long, + val bitrate: Int, + val bufferDuration: Long, + val playbackDuration: Long, + val playerPosition: Long, + val stall: QoSStall, + val url: String, +) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSMedia.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSMedia.kt new file mode 100644 index 000000000..d399839ae --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSMedia.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +/** + * Information about the media being played. + * + * @property assetUrl The URL of the asset. + * @property id The id of the media. + * @property metadataUrl The URL of the metadata. + * @property origin The origin of the media. + */ +data class QoSMedia( + val assetUrl: String, + val id: String, + val metadataUrl: String, + val origin: String, +) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSMessage.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSMessage.kt new file mode 100644 index 000000000..666b2d03f --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSMessage.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +/** + * Represents a QoS message. + * + * @property data The data associated with the message. + * @property eventName The name of the event. + * @property sessionId The session id. + * @property timestamp The current timestamp. + */ +data class QoSMessage( + val data: Any, + val eventName: String, + val sessionId: String, + val timestamp: Long = System.currentTimeMillis(), +) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSOS.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSOS.kt new file mode 100644 index 000000000..0347f30eb --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSOS.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +/** + * Information about the operating system. + * + * @property name The name of the operating system. + * @property version The version of the operating system. + */ +data class QoSOS( + val name: String, + val version: String, +) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSPlayer.kt new file mode 100644 index 000000000..8a174ed43 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSPlayer.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +/** + * Information about the player. + * + * @property name The name of the player. + * @property platform The platform of the player. + * @property version The version of the player. + */ +data class QoSPlayer( + val name: String, + val platform: String, + val version: String, +) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSScreen.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSScreen.kt new file mode 100644 index 000000000..be912a787 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSScreen.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +/** + * Information about the device screen. + * + * @property height The height of the screen, in pixels. + * @property width The width of the screen, in pixels. + */ +data class QoSScreen( + val height: Int, + val width: Int, +) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSSession.kt new file mode 100644 index 000000000..b9734a23c --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSSession.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Rect +import android.os.Build +import android.view.WindowManager +import ch.srgssr.pillarbox.player.BuildConfig +import ch.srgssr.pillarbox.player.qos.models.QoSDevice.DeviceType + +/** + * Represents a QoS session, which contains information about the device, current media, and player. + * + * @property device The information about the device. + * @property media The information about the media being played. + * @property operatingSystem The information about the operating system. + * @property player The information about the player. + * @property screen The information about the device screen. + * @property timeMetrics The metrics about the time needed to load the various media components. + */ +data class QoSSession( + val device: QoSDevice, + val media: QoSMedia, + val operatingSystem: QoSOS = QoSOS( + name = PLATFORM_NAME, + version = OPERATING_SYSTEM_VERSION, + ), + val player: QoSPlayer = QoSPlayer( + name = PLAYER_NAME, + platform = PLATFORM_NAME, + version = PLAYER_VERSION, + ), + val screen: QoSScreen, + val timeMetrics: QoSSessionTimings = QoSSessionTimings.Empty, +) { + constructor( + context: Context, + media: QoSMedia, + timeMetrics: QoSSessionTimings, + ) : this( + device = QoSDevice( + id = getDeviceId(), + model = getDeviceModel(), + type = context.getDeviceType(), + ), + media = media, + screen = context.getWindowBounds().let { windowBounds -> + QoSScreen( + height = windowBounds.height(), + width = windowBounds.width(), + ) + }, + timeMetrics = timeMetrics, + ) + + private companion object { + private val OPERATING_SYSTEM_VERSION = Build.VERSION.RELEASE + private const val PHONE_TABLET_WIDTH_THRESHOLD = 600 + private const val PLATFORM_NAME = "android" + private const val PLAYER_NAME = "pillarbox" + private const val PLAYER_VERSION = BuildConfig.VERSION_NAME + + @Suppress("FunctionOnlyReturningConstant") + private fun getDeviceId(): String { + // TODO Define this somehow (maybe use TCPredefinedVariables.getInstance().uniqueIdentifier) + return "" + } + + private fun getDeviceModel(): String { + return Build.MANUFACTURER + " " + Build.MODEL + } + + private fun Context.getDeviceType(): DeviceType { + val configuration = resources.configuration + return when (configuration.uiMode and Configuration.UI_MODE_TYPE_MASK) { + Configuration.UI_MODE_TYPE_CAR -> DeviceType.CAR + Configuration.UI_MODE_TYPE_DESK -> DeviceType.DESKTOP + Configuration.UI_MODE_TYPE_NORMAL -> { + val smallestWidthDp = configuration.smallestScreenWidthDp + + if (smallestWidthDp >= PHONE_TABLET_WIDTH_THRESHOLD) { + DeviceType.TABLET + } else { + DeviceType.PHONE + } + } + + Configuration.UI_MODE_TYPE_TELEVISION -> DeviceType.TV + else -> DeviceType.UNKNOWN + } + } + + private fun Context.getWindowBounds(): Rect { + val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager + + return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + Rect().also { + @Suppress("DEPRECATION") + windowManager.defaultDisplay.getRectSize(it) + } + } else { + windowManager.maximumWindowMetrics.bounds + } + } + } +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSSessionTimings.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSSessionTimings.kt new file mode 100644 index 000000000..360bf8132 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSSessionTimings.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +import kotlin.time.Duration + +/** + * Represents the timings until the current media started to play. + * + * @property asset The time spent to load the asset. + * @property drm The time spent to load the DRM. + * @property metadata The time spent to load the media source. + * @property total The time spent to load from the moment the [MediaItem][androidx.media3.common.MediaItem] became the current item until it + * started to play. + */ +data class QoSSessionTimings( + val asset: Duration? = null, + val drm: Duration? = null, + val metadata: Duration? = null, + val total: Duration? = null, +) { + companion object { + /** + * Default [QoSSessionTimings] where all fields are set to `null`. + */ + val Empty = QoSSessionTimings( + asset = null, + drm = null, + metadata = null, + total = null, + ) + } +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSStall.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSStall.kt new file mode 100644 index 000000000..943e5c52b --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/qos/models/QoSStall.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +/** + * Information about stalls. + * + * @property count The number of stalls that have occurred, not as a result of a seek. + * @property duration The total duration of the stalls, in milliseconds. + */ +data class QoSStall( + val count: Int, + val duration: Long, +) diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/models/QoSErrorTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/models/QoSErrorTest.kt new file mode 100644 index 000000000..3871b05b9 --- /dev/null +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/models/QoSErrorTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +import java.lang.RuntimeException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +class QoSErrorTest { + @Test + fun `throwableConstructor with empty exception`() { + val throwable = IllegalStateException() + val qosError = QoSError( + throwable = throwable, + playerPosition = 5.minutes.inWholeMilliseconds, + severity = QoSError.Severity.WARNING, + url = "", + ) + + val logLines = qosError.log.lineSequence() + + assertTrue(logLines.count() > 1, "Expected log to contain the stacktrace") + assertEquals("java.lang.IllegalStateException", logLines.first()) + assertTrue(logLines.none { it.startsWith("Caused by: ") }, "Expected log to not contain a cause") + assertEquals("", qosError.message) + assertEquals("IllegalStateException", qosError.name) + assertEquals(QoSError.Severity.WARNING, qosError.severity) + } + + @Test + fun `throwableConstructor with detailed exception`() { + val cause = NullPointerException("Expected 'foo' to be not null") + val throwable = RuntimeException("Something bad happened", cause) + val qosError = QoSError( + throwable = throwable, + playerPosition = 30.seconds.inWholeMilliseconds, + severity = QoSError.Severity.FATAL, + url = "", + ) + + val logLines = qosError.log.lineSequence() + + assertTrue(logLines.count() > 1, "Expected log to contain the stacktrace") + assertEquals("java.lang.RuntimeException: ${throwable.message}", logLines.first()) + assertTrue( + logLines.any { it == "Caused by: java.lang.NullPointerException: ${cause.message}" }, + "Expected log to contain a cause", + ) + assertEquals(throwable.message, qosError.message) + assertEquals("RuntimeException", qosError.name) + assertEquals(QoSError.Severity.FATAL, qosError.severity) + } +} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/models/QoSSessionTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/models/QoSSessionTest.kt new file mode 100644 index 000000000..4896ff0b6 --- /dev/null +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/models/QoSSessionTest.kt @@ -0,0 +1,241 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import kotlin.test.Test +import kotlin.test.assertEquals + +@RunWith(AndroidJUnit4::class) +class QoSSessionTest { + @Test + fun `contextConstructor provides correct default values`() { + val qosSession = createQoSSession() + + assertEquals("", qosSession.device.id) + assertEquals("unknown robolectric", qosSession.device.model) + assertEquals(QoSDevice.DeviceType.PHONE, qosSession.device.type) + assertEquals(ASSET_URL, qosSession.media.assetUrl) + assertEquals(MEDIA_ID, qosSession.media.id) + assertEquals(METADATA_URL, qosSession.media.metadataUrl) + assertEquals(ORIGIN, qosSession.media.origin) + assertEquals(OPERATING_SYSTEM_NAME, qosSession.operatingSystem.name) + assertEquals("5.0.2", qosSession.operatingSystem.version) + assertEquals(PLAYER_NAME, qosSession.player.name) + assertEquals(PLAYER_PLATFORM, qosSession.player.platform) + assertEquals(PLAYER_VERSION, qosSession.player.version) + assertEquals(SCREEN_HEIGHT, qosSession.screen.height) + assertEquals(SCREEN_WIDTH, qosSession.screen.width) + assertEquals(QoSSessionTimings.Empty, qosSession.timeMetrics) + } + + @Test + @Config(sdk = [30]) + fun `contextConstructor provides correct default values (API 30)`() { + val qosSession = createQoSSession() + + assertEquals("", qosSession.device.id) + assertEquals("robolectric robolectric", qosSession.device.model) + assertEquals(QoSDevice.DeviceType.PHONE, qosSession.device.type) + assertEquals(ASSET_URL, qosSession.media.assetUrl) + assertEquals(MEDIA_ID, qosSession.media.id) + assertEquals(METADATA_URL, qosSession.media.metadataUrl) + assertEquals(ORIGIN, qosSession.media.origin) + assertEquals(OPERATING_SYSTEM_NAME, qosSession.operatingSystem.name) + assertEquals("11", qosSession.operatingSystem.version) + assertEquals(PLAYER_NAME, qosSession.player.name) + assertEquals(PLAYER_PLATFORM, qosSession.player.platform) + assertEquals(PLAYER_VERSION, qosSession.player.version) + assertEquals(SCREEN_HEIGHT, qosSession.screen.height) + assertEquals(SCREEN_WIDTH, qosSession.screen.width) + assertEquals(QoSSessionTimings.Empty, qosSession.timeMetrics) + } + + @Test + @Config(qualifiers = "car") + fun `contextConstructor provides correct default values for car`() { + val qosSession = createQoSSession() + + assertEquals("", qosSession.device.id) + assertEquals("unknown robolectric", qosSession.device.model) + assertEquals(QoSDevice.DeviceType.CAR, qosSession.device.type) + assertEquals(ASSET_URL, qosSession.media.assetUrl) + assertEquals(MEDIA_ID, qosSession.media.id) + assertEquals(METADATA_URL, qosSession.media.metadataUrl) + assertEquals(ORIGIN, qosSession.media.origin) + assertEquals(OPERATING_SYSTEM_NAME, qosSession.operatingSystem.name) + assertEquals("5.0.2", qosSession.operatingSystem.version) + assertEquals(PLAYER_NAME, qosSession.player.name) + assertEquals(PLAYER_PLATFORM, qosSession.player.platform) + assertEquals(PLAYER_VERSION, qosSession.player.version) + assertEquals(SCREEN_HEIGHT, qosSession.screen.height) + assertEquals(SCREEN_WIDTH, qosSession.screen.width) + assertEquals(QoSSessionTimings.Empty, qosSession.timeMetrics) + } + + @Test + @Config(qualifiers = "desk") + fun `contextConstructor provides correct default values for desktop`() { + val qosSession = createQoSSession() + + assertEquals("", qosSession.device.id) + assertEquals("unknown robolectric", qosSession.device.model) + assertEquals(QoSDevice.DeviceType.DESKTOP, qosSession.device.type) + assertEquals(ASSET_URL, qosSession.media.assetUrl) + assertEquals(MEDIA_ID, qosSession.media.id) + assertEquals(METADATA_URL, qosSession.media.metadataUrl) + assertEquals(ORIGIN, qosSession.media.origin) + assertEquals(OPERATING_SYSTEM_NAME, qosSession.operatingSystem.name) + assertEquals("5.0.2", qosSession.operatingSystem.version) + assertEquals(PLAYER_NAME, qosSession.player.name) + assertEquals(PLAYER_PLATFORM, qosSession.player.platform) + assertEquals(PLAYER_VERSION, qosSession.player.version) + assertEquals(SCREEN_HEIGHT, qosSession.screen.height) + assertEquals(SCREEN_WIDTH, qosSession.screen.width) + assertEquals(QoSSessionTimings.Empty, qosSession.timeMetrics) + } + + @Test + @Config(qualifiers = "sw320dp") + fun `contextConstructor provides correct default values for phone (sw320dp)`() { + val qosSession = createQoSSession() + + assertEquals("", qosSession.device.id) + assertEquals("unknown robolectric", qosSession.device.model) + assertEquals(QoSDevice.DeviceType.PHONE, qosSession.device.type) + assertEquals(ASSET_URL, qosSession.media.assetUrl) + assertEquals(MEDIA_ID, qosSession.media.id) + assertEquals(METADATA_URL, qosSession.media.metadataUrl) + assertEquals(ORIGIN, qosSession.media.origin) + assertEquals(OPERATING_SYSTEM_NAME, qosSession.operatingSystem.name) + assertEquals("5.0.2", qosSession.operatingSystem.version) + assertEquals(PLAYER_NAME, qosSession.player.name) + assertEquals(PLAYER_PLATFORM, qosSession.player.platform) + assertEquals(PLAYER_VERSION, qosSession.player.version) + assertEquals(SCREEN_HEIGHT, qosSession.screen.height) + assertEquals(SCREEN_WIDTH, qosSession.screen.width) + assertEquals(QoSSessionTimings.Empty, qosSession.timeMetrics) + } + + @Test + @Config(qualifiers = "sw600dp") + fun `contextConstructor provides correct default values for tablet (sw600dp)`() { + val qosSession = createQoSSession() + + assertEquals("", qosSession.device.id) + assertEquals("unknown robolectric", qosSession.device.model) + assertEquals(QoSDevice.DeviceType.TABLET, qosSession.device.type) + assertEquals(ASSET_URL, qosSession.media.assetUrl) + assertEquals(MEDIA_ID, qosSession.media.id) + assertEquals(METADATA_URL, qosSession.media.metadataUrl) + assertEquals(ORIGIN, qosSession.media.origin) + assertEquals(OPERATING_SYSTEM_NAME, qosSession.operatingSystem.name) + assertEquals("5.0.2", qosSession.operatingSystem.version) + assertEquals(PLAYER_NAME, qosSession.player.name) + assertEquals(PLAYER_PLATFORM, qosSession.player.platform) + assertEquals(PLAYER_VERSION, qosSession.player.version) + assertEquals(SCREEN_HEIGHT, qosSession.screen.height) + assertEquals(SCREEN_WIDTH, qosSession.screen.width) + assertEquals(QoSSessionTimings.Empty, qosSession.timeMetrics) + } + + @Test + @Config(qualifiers = "sw720dp") + fun `contextConstructor provides correct default values for tablet (sw720dp)`() { + val qosSession = createQoSSession() + + assertEquals("", qosSession.device.id) + assertEquals("unknown robolectric", qosSession.device.model) + assertEquals(QoSDevice.DeviceType.TABLET, qosSession.device.type) + assertEquals(ASSET_URL, qosSession.media.assetUrl) + assertEquals(MEDIA_ID, qosSession.media.id) + assertEquals(METADATA_URL, qosSession.media.metadataUrl) + assertEquals(ORIGIN, qosSession.media.origin) + assertEquals(OPERATING_SYSTEM_NAME, qosSession.operatingSystem.name) + assertEquals("5.0.2", qosSession.operatingSystem.version) + assertEquals(PLAYER_NAME, qosSession.player.name) + assertEquals(PLAYER_PLATFORM, qosSession.player.platform) + assertEquals(PLAYER_VERSION, qosSession.player.version) + assertEquals(SCREEN_HEIGHT, qosSession.screen.height) + assertEquals(SCREEN_WIDTH, qosSession.screen.width) + assertEquals(QoSSessionTimings.Empty, qosSession.timeMetrics) + } + + @Test + @Config(qualifiers = "television") + fun `contextConstructor provides correct default values for TV`() { + val qosSession = createQoSSession() + + assertEquals("", qosSession.device.id) + assertEquals("unknown robolectric", qosSession.device.model) + assertEquals(QoSDevice.DeviceType.TV, qosSession.device.type) + assertEquals(ASSET_URL, qosSession.media.assetUrl) + assertEquals(MEDIA_ID, qosSession.media.id) + assertEquals(METADATA_URL, qosSession.media.metadataUrl) + assertEquals(ORIGIN, qosSession.media.origin) + assertEquals(OPERATING_SYSTEM_NAME, qosSession.operatingSystem.name) + assertEquals("5.0.2", qosSession.operatingSystem.version) + assertEquals(PLAYER_NAME, qosSession.player.name) + assertEquals(PLAYER_PLATFORM, qosSession.player.platform) + assertEquals(PLAYER_VERSION, qosSession.player.version) + assertEquals(SCREEN_HEIGHT, qosSession.screen.height) + assertEquals(SCREEN_WIDTH, qosSession.screen.width) + assertEquals(QoSSessionTimings.Empty, qosSession.timeMetrics) + } + + @Test + @Config(qualifiers = "watch") + fun `contextConstructor provides correct default values for watch`() { + val qosSession = createQoSSession() + + assertEquals("", qosSession.device.id) + assertEquals("unknown robolectric", qosSession.device.model) + assertEquals(QoSDevice.DeviceType.UNKNOWN, qosSession.device.type) + assertEquals(ASSET_URL, qosSession.media.assetUrl) + assertEquals(MEDIA_ID, qosSession.media.id) + assertEquals(METADATA_URL, qosSession.media.metadataUrl) + assertEquals(ORIGIN, qosSession.media.origin) + assertEquals(OPERATING_SYSTEM_NAME, qosSession.operatingSystem.name) + assertEquals("5.0.2", qosSession.operatingSystem.version) + assertEquals(PLAYER_NAME, qosSession.player.name) + assertEquals(PLAYER_PLATFORM, qosSession.player.platform) + assertEquals(PLAYER_VERSION, qosSession.player.version) + assertEquals(SCREEN_HEIGHT, qosSession.screen.height) + assertEquals(SCREEN_WIDTH, qosSession.screen.width) + assertEquals(QoSSessionTimings.Empty, qosSession.timeMetrics) + } + + private fun createQoSSession(): QoSSession { + val context = ApplicationProvider.getApplicationContext() + + return QoSSession( + context = context, + media = QoSMedia( + assetUrl = ASSET_URL, + id = MEDIA_ID, + metadataUrl = METADATA_URL, + origin = ORIGIN, + ), + timeMetrics = QoSSessionTimings.Empty, + ) + } + + private companion object { + private const val ASSET_URL = "https://rts-vod-amd.akamaized.net/ww/12345/3037738d-fe91-32e3-93f2-4dbb62a0f9bd/master.m3u8" + private const val MEDIA_ID = "urn:rts:video:12345" + private const val METADATA_URL = "https://il-stage.srgssr.ch/integrationlayer/2.1/mediaComposition/byUrn/urn:rts:video:12345?vector=APPPLAY" + private const val OPERATING_SYSTEM_NAME = "android" + private const val ORIGIN = "ch.srgssr.pillarbox.player.test" + private const val PLAYER_NAME = "pillarbox" + private const val PLAYER_PLATFORM = "android" + private const val PLAYER_VERSION = "Local" + private const val SCREEN_HEIGHT = 470 + private const val SCREEN_WIDTH = 320 + } +} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/models/QoSSessionTimingsTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/models/QoSSessionTimingsTest.kt new file mode 100644 index 000000000..0f6ec0487 --- /dev/null +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/qos/models/QoSSessionTimingsTest.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player.qos.models + +import kotlin.test.Test +import kotlin.test.assertNull + +class QoSSessionTimingsTest { + @Test + fun `empty timings`() { + val timings = QoSSessionTimings.Empty + + assertNull(timings.asset) + assertNull(timings.drm) + assertNull(timings.metadata) + assertNull(timings.total) + } +}