Skip to content

Commit

Permalink
Update models for QoS (#654)
Browse files Browse the repository at this point in the history
Co-authored-by: Joaquim Stähli <[email protected]>
  • Loading branch information
MGaetan89 and StaehliJ committed Sep 11, 2024
1 parent 73c39f9 commit faf0751
Show file tree
Hide file tree
Showing 16 changed files with 698 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
)
}
Expand Down Expand Up @@ -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(),
)
}
Expand Down Expand Up @@ -166,6 +175,7 @@ internal class QoSCoordinator(
throwable = it,
playerPosition = player.currentPosition,
severity = QoSError.Severity.FATAL,
url = url,
),
)
}
Expand All @@ -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,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}
}
Original file line number Diff line number Diff line change
@@ -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,
)
}
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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(),
)
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Loading

0 comments on commit faf0751

Please sign in to comment.