Skip to content

Commit

Permalink
Dispaly metrics inside the demo apps
Browse files Browse the repository at this point in the history
  • Loading branch information
MGaetan89 committed Aug 8, 2024
1 parent 0926634 commit cca67a2
Show file tree
Hide file tree
Showing 17 changed files with 1,701 additions and 47 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-unit = { module = "androidx.compose.ui:ui-unit" }
androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class" }
androidx-compose-material-icons-core = { module = "androidx.compose.material:material-icons-core" }
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ fun LineChart(
lineWidth: Dp = 2.dp,
lineCornerRadius: Dp = 6.dp,
stretchChartToPointsCount: Int? = null,
scaleItemsCount: Int = 4,
scaleItemsCount: Int = 5,
scaleTextFormatter: NumberFormat = NumberFormat.getIntegerInstance(),
scaleTextStyle: TextStyle = TextStyle.Default,
scaleTextHorizontalPadding: Dp = 8.dp,
Expand Down Expand Up @@ -123,7 +123,7 @@ fun BarChart(
barColor: Color = Color.Blue,
barSpacing: Dp = 1.dp,
stretchChartToPointsCount: Int? = null,
scaleItemsCount: Int = 4,
scaleItemsCount: Int = 5,
scaleTextFormatter: NumberFormat = NumberFormat.getIntegerInstance(),
scaleTextStyle: TextStyle = TextStyle.Default,
scaleTextHorizontalPadding: Dp = 8.dp,
Expand Down Expand Up @@ -154,13 +154,13 @@ fun BarChart(
@Composable
private fun Chart(
data: List<Float>,
modifier: Modifier = Modifier,
stretchChartToPointsCount: Int? = null,
scaleItemsCount: Int = 4,
modifier: Modifier,
stretchChartToPointsCount: Int?,
scaleItemsCount: Int,
scaleTextFormatter: NumberFormat,
scaleTextStyle: TextStyle = TextStyle.Default,
scaleTextHorizontalPadding: Dp = 8.dp,
scaleLineColor: Color = Color.LightGray,
scaleTextStyle: TextStyle,
scaleTextHorizontalPadding: Dp,
scaleLineColor: Color,
drawChart: DrawScope.(points: List<Float>, maxValue: Int, bounds: Rect) -> Unit,
) {
val trimmedData = if (stretchChartToPointsCount != null) data.takeLast(stretchChartToPointsCount) else data
Expand Down Expand Up @@ -286,7 +286,7 @@ private fun DrawScope.drawScale(
val textX = lineXEnd + scaleTextHorizontalPadding.toPx()
val textY = (lineY - textSize.center.y).coerceIn(
minimumValue = 0f,
maximumValue = size.height - textSize.height,
maximumValue = (size.height - textSize.height).coerceAtLeast(0f),
)

drawLine(
Expand Down
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.demo.shared.ui.player.metrics

/**
* Information about bit rates.
*
* @property data The list of recorded bit rates.
*/
data class BitRates(
val data: List<Float>,
) {
/**
* The unit in which the bit rates are expressed.
*/
val unit = "Mbps"

/**
* The current bit rate.
*/
val current: Float
get() = data.last()

/**
* The biggest recorded bit rate.
*/
val max: Float
get() = data.max()

/**
* The smallest recorded bit rate.
*/
val min: Float
get() = data.min()

companion object {
/**
* Empty [BitRates].
*/
val Empty = BitRates(
data = emptyList(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.demo.shared.ui.player.metrics

/**
* Information about data volumes.
*
* @property data The list of recorded data volumes.
* @property total The formatted total volume.
*/
data class DataVolumes(
val data: List<Float>,
val total: String,
) {
/**
* The unit in which the volumes are expressed.
*/
val unit = "MByte"

companion object {
/**
* Empty [DataVolumes].
*/
val Empty = DataVolumes(
data = emptyList(),
total = "",
)
}
}
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.demo.shared.ui.player.metrics

/**
* Information about stalls.
*
* @property data The list of recorded stalls.
* @property total The formatted total of stalls.
*/
data class Stalls(
val data: List<Float>,
val total: String,
) {
companion object {
/**
* Empty [Stalls].
*/
val Empty = Stalls(
data = emptyList(),
total = "",
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.demo.shared.ui.player.metrics

import android.app.Application
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel
import androidx.media3.common.VideoSize
import ch.srgssr.pillarbox.demo.shared.R
import ch.srgssr.pillarbox.player.analytics.metrics.PlaybackMetrics
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import java.text.NumberFormat
import kotlin.collections.sum
import kotlin.time.Duration

/**
* [ViewModel][androidx.lifecycle.ViewModel] for the "Stats for Nerds" screen.
*/
class StatsForNerdsViewModel(application: Application) : AndroidViewModel(application) {
private val _indicatedBitRates = MutableStateFlow(BitRates.Empty)

/**
* Provides information about the indicated bit rates.
*/
val indicatedBitRates: StateFlow<BitRates> = _indicatedBitRates

private val _information = MutableStateFlow(emptyMap<String, String>())

/**
* Provides information about the current session.
*/
val information: StateFlow<Map<String, String>> = _information

private val _observedBitRates = MutableStateFlow(BitRates.Empty)

/**
* Provides information about the observed bit rates.
*/
val observedBitRates: StateFlow<BitRates> = _observedBitRates

private val _stalls = MutableStateFlow(Stalls.Empty)

/**
* Provides information about stalls.
*/
val stalls: StateFlow<Stalls> = _stalls

private val _startupTimes = MutableStateFlow(emptyMap<String, String>())

/**
* Provides information about the startup times.
*/
val startupTimes: StateFlow<Map<String, String>> = _startupTimes

private val _volumes = MutableStateFlow(DataVolumes.Empty)

/**
* Provides information about volumes.
*/
val volumes: StateFlow<DataVolumes> = _volumes

/**
* The latest playback metrics.
*/
var playbackMetrics: PlaybackMetrics? = null
set(value) {
if (field == value) {
return
}

field = value

if (value == null) {
return
}

_indicatedBitRates.update {
BitRates(
data = it.data + (value.indicatedBitrate / TO_MEGA),
)
}

_information.update {
listOfNotNull(
getSessionInformation(R.string.session_id, value.sessionId),
getSessionInformation(R.string.media_uri, value.url?.toString()),
getSessionInformation(R.string.playback_duration, value.playbackDuration.toString()),
getSessionInformation(R.string.data_volume, value.totalBytesLoaded.toFloat().toFormattedBytes(includeUnit = true)),
getSessionInformation(R.string.buffering, value.bufferingDuration.toString()),
getSessionInformation(
labelRes = R.string.video_size,
value = if (value.videoSize != VideoSize.UNKNOWN) {
"${value.videoSize.width}x${value.videoSize.height}"
} else {
null
}
),
).toMap()
}

_observedBitRates.update {
BitRates(
data = it.data + (value.bandwidth / TO_MEGA),
)
}

_stalls.update {
val stallCount = value.stallCount.toFloat()
val stall = if (it.data.isEmpty()) {
stallCount
} else {
stallCount - it.data.sum()
}

Stalls(
data = it.data + stall.coerceAtLeast(0f),
total = value.stallCount.toFloat().toFormattedBytes(includeUnit = false)
)
}

_startupTimes.update {
listOfNotNull(
getLoadDuration(R.string.asset_loading, value.loadDuration.asset),
getLoadDuration(R.string.manifest_loading, value.loadDuration.manifest),
getLoadDuration(R.string.drm_loading, value.loadDuration.drm),
getLoadDuration(R.string.resource_loading, value.loadDuration.source),
getLoadDuration(R.string.total_load_time, value.loadDuration.timeToReady)
).toMap()
}

_volumes.update {
val totalBytesLoaded = value.totalBytesLoaded.toFloat()
val volume = if (it.data.isEmpty()) {
totalBytesLoaded / TO_MEGA
} else {
totalBytesLoaded / TO_MEGA - it.data.sum()
}

DataVolumes(
data = it.data + volume.coerceAtLeast(0f),
total = totalBytesLoaded.toFormattedBytes(includeUnit = true),
)
}
}

private fun getLoadDuration(
@StringRes labelRes: Int,
duration: Duration?,
): Pair<String, String>? {
return if (duration != null) {
getApplication<Application>().getString(labelRes) to duration.toString()
} else {
null
}
}

private fun getSessionInformation(
@StringRes labelRes: Int,
value: String?,
): Pair<String, String>? {
return if (value != null) {
getApplication<Application>().getString(labelRes) to value
} else {
null
}
}

private fun Float.toFormattedBytes(
includeUnit: Boolean,
): String {
val units = listOf("B", "KB", "MB", "GB", "TB")
val numberFormat = NumberFormat.getNumberInstance()

var remaining = this
var unitIndex = 0
while (remaining >= TO_NEXT_UNIT && unitIndex < units.lastIndex) {
remaining /= TO_NEXT_UNIT
unitIndex++
}

return if (includeUnit) {
"${numberFormat.format(remaining)} ${units[unitIndex]}"
} else {
numberFormat.format(remaining)
}
}

companion object {
private const val TO_MEGA = 1_000_000f
private const val TO_NEXT_UNIT = 1_000f

/**
* The aspect ratio to use for the charts.
*/
const val CHART_ASPECT_RATIO = 16 / 9f

/**
* The maximum number of points to display on a chart.
*/
const val CHART_MAX_POINTS = 90
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package ch.srgssr.pillarbox.demo.shared.ui.player.settings
import android.app.Application
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ClosedCaption
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.RecordVoiceOver
import androidx.compose.material.icons.filled.SlowMotionVideo
import androidx.compose.material.icons.filled.Tune
Expand Down Expand Up @@ -180,6 +181,15 @@ class PlayerSettingsViewModel(
)
)
}

add(
SettingItem(
title = application.getString(R.string.stats_for_nerds),
subtitle = null,
icon = Icons.Default.Info,
destination = SettingsRoutes.StatsForNerds,
)
)
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())

Expand Down
Loading

0 comments on commit cca67a2

Please sign in to comment.