diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/LocalTimeFormatter.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/LocalTimeFormatter.kt new file mode 100644 index 000000000..2c463f17f --- /dev/null +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/LocalTimeFormatter.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.shared.ui + +import kotlinx.datetime.LocalTime +import kotlinx.datetime.format.Padding +import kotlinx.datetime.format.char + +/** + * Local time formatter that format [LocalTime] to HH:mm:ss. + */ +val localTimeFormatter by lazy { + LocalTime.Format { + hour(Padding.ZERO) + char(':') + minute(Padding.ZERO) + char(':') + second(Padding.ZERO) + } +} diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt index 87870f358..dcfde7e01 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt @@ -80,4 +80,7 @@ sealed interface NavigationRoutes { @Serializable data object ThumbnailShowcase : NavigationRoutes + + @Serializable + data object TimeBasedContent : NavigationRoutes } diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt index 94e4b83a6..f958a86bc 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.res.stringResource import androidx.media3.common.C import androidx.media3.common.Player +import androidx.media3.common.Timeline.Window import androidx.tv.material3.Button import androidx.tv.material3.DrawerValue import androidx.tv.material3.Icon @@ -51,6 +52,7 @@ import ch.srgssr.pillarbox.demo.shared.R import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent import ch.srgssr.pillarbox.demo.shared.ui.components.PillarboxSlider import ch.srgssr.pillarbox.demo.shared.ui.getFormatter +import ch.srgssr.pillarbox.demo.shared.ui.localTimeFormatter import ch.srgssr.pillarbox.demo.shared.ui.player.metrics.MetricsOverlay import ch.srgssr.pillarbox.demo.shared.ui.settings.MetricsOverlayOptions import ch.srgssr.pillarbox.demo.tv.ui.player.compose.controls.PlayerError @@ -60,12 +62,14 @@ import ch.srgssr.pillarbox.demo.tv.ui.theme.paddings import ch.srgssr.pillarbox.player.PillarboxExoPlayer import ch.srgssr.pillarbox.player.currentPositionAsFlow import ch.srgssr.pillarbox.player.extension.canSeek +import ch.srgssr.pillarbox.player.extension.getUnixTimeMs import ch.srgssr.pillarbox.ui.extension.availableCommandsAsState import ch.srgssr.pillarbox.ui.extension.currentMediaMetadataAsState import ch.srgssr.pillarbox.ui.extension.currentPositionAsState import ch.srgssr.pillarbox.ui.extension.durationAsState import ch.srgssr.pillarbox.ui.extension.getCurrentChapterAsState import ch.srgssr.pillarbox.ui.extension.getCurrentCreditAsState +import ch.srgssr.pillarbox.ui.extension.isCurrentMediaItemLiveAsState import ch.srgssr.pillarbox.ui.extension.playerErrorAsState import ch.srgssr.pillarbox.ui.widget.DelayedVisibilityState import ch.srgssr.pillarbox.ui.widget.maintainVisibleOnFocus @@ -73,6 +77,9 @@ import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface import ch.srgssr.pillarbox.ui.widget.rememberDelayedVisibilityState import kotlinx.coroutines.delay import kotlinx.coroutines.flow.map +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -306,9 +313,20 @@ private fun PlayerTimeRow( var compactMode by remember { mutableStateOf(true) } + val isLive by player.isCurrentMediaItemLiveAsState() + val window = remember { Window() } + val positionTime = if (isLive) player.getUnixTimeMs(positionMs, window) else C.TIME_UNSET + val positionLabel = when (positionTime) { + C.TIME_UNSET -> formatter(positionMs.milliseconds) + + else -> { + val localTime = Instant.fromEpochMilliseconds(positionTime).toLocalDateTime(TimeZone.currentSystemDefault()).time + localTimeFormatter.format(localTime) + } + } Text( - text = "${formatter(positionMs.milliseconds)} / ${formatter(duration)}", + text = "$positionLabel / ${formatter(duration)}", modifier = Modifier.padding( top = MaterialTheme.paddings.baseline, bottom = MaterialTheme.paddings.small, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/CountdownView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/CountdownView.kt index 0e0d5c1b2..0cf0b79af 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/CountdownView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/CountdownView.kt @@ -11,11 +11,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -23,11 +21,9 @@ import androidx.compose.ui.tooling.preview.Preview import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme import kotlinx.coroutines.delay import kotlinx.datetime.LocalTime -import kotlinx.datetime.format -import kotlinx.datetime.format.Padding -import kotlinx.datetime.format.char import kotlin.time.Duration import kotlin.time.Duration.Companion.ZERO +import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -52,19 +48,17 @@ fun rememberCountdownState(duration: Duration): CountdownState { * @param duration The countdown duration. */ class CountdownState internal constructor(duration: Duration) { - private var countdown by mutableStateOf(duration) + private var _countdown = mutableStateOf(duration.inWholeSeconds.seconds) /** * Remaining time [LocalTime]. */ - val remainingTime: State = derivedStateOf { - LocalTime.fromMillisecondOfDay(countdown.inWholeMilliseconds.toInt()) - } + val countdown: State = _countdown internal suspend fun start() { - while (countdown > ZERO) { + while (_countdown.value > ZERO) { delay(step) - countdown -= step + _countdown.value -= step } } @@ -73,16 +67,6 @@ class CountdownState internal constructor(duration: Duration) { } } -private val formatHms by lazy { - LocalTime.Format { - hour(Padding.ZERO) - char(':') - minute(Padding.ZERO) - char(':') - second(Padding.ZERO) - } -} - /** * Countdown * @@ -92,9 +76,8 @@ private val formatHms by lazy { @Composable fun Countdown(countdownDuration: Duration, modifier: Modifier = Modifier) { val countdownState = rememberCountdownState(countdownDuration) - val remainingTime by countdownState.remainingTime - val text = remainingTime.format(formatHms) - Text(text, modifier = modifier, color = Color.White) + val remainingTime by countdownState.countdown + Text("$remainingTime", modifier = modifier, color = Color.White) } @Preview(showBackground = true) @@ -107,7 +90,7 @@ private fun CountdownPreview() { .background(Color.Black) ) { Countdown( - countdownDuration = 1.minutes, + countdownDuration = 40.hours + 1.minutes + 32.seconds, modifier = Modifier.align(Alignment.Center), ) } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt index 2cac94cda..53b322864 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt @@ -23,18 +23,25 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.media3.common.C import androidx.media3.common.Player +import androidx.media3.common.Timeline.Window import ch.srgssr.pillarbox.demo.shared.ui.components.PillarboxSlider import ch.srgssr.pillarbox.demo.shared.ui.getFormatter +import ch.srgssr.pillarbox.demo.shared.ui.localTimeFormatter import ch.srgssr.pillarbox.demo.ui.theme.paddings import ch.srgssr.pillarbox.player.PillarboxExoPlayer import ch.srgssr.pillarbox.player.extension.canSeek +import ch.srgssr.pillarbox.player.extension.getUnixTimeMs import ch.srgssr.pillarbox.ui.ProgressTrackerState import ch.srgssr.pillarbox.ui.SimpleProgressTrackerState import ch.srgssr.pillarbox.ui.SmoothProgressTrackerState import ch.srgssr.pillarbox.ui.extension.availableCommandsAsState import ch.srgssr.pillarbox.ui.extension.currentBufferedPercentageAsState import ch.srgssr.pillarbox.ui.extension.durationAsState +import ch.srgssr.pillarbox.ui.extension.isCurrentMediaItemLiveAsState import kotlinx.coroutines.CoroutineScope +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.milliseconds @@ -76,6 +83,7 @@ fun PlayerTimeSlider( progressTracker: ProgressTrackerState = rememberProgressTrackerState(player = player, smoothTracker = true), interactionSource: MutableInteractionSource? = null, ) { + val window = remember { Window() } val rememberedProgressTracker by rememberUpdatedState(progressTracker) val durationMs by player.durationAsState() val duration = remember(durationMs) { @@ -95,7 +103,20 @@ fun PlayerTimeSlider( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.mini) ) { - Text(text = formatter(currentProgress), color = Color.White) + val isLive by player.isCurrentMediaItemLiveAsState() + val timePosition = if (isLive) player.getUnixTimeMs(currentProgress.inWholeMilliseconds, window) else C.TIME_UNSET + // We choose to display local time only when it is live, but it is possible to have timestamp inside VoD. + val positionLabel = + when (timePosition) { + C.TIME_UNSET -> formatter(currentProgress) + + else -> { + val localTime = Instant.fromEpochMilliseconds(timePosition).toLocalDateTime(TimeZone.currentSystemDefault()).time + localTimeFormatter.format(localTime) + } + } + + Text(text = positionLabel, color = Color.White) PillarboxSlider( value = currentProgressPercent, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt index 4a6684066..1e49f7cec 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt @@ -157,6 +157,14 @@ fun ShowcasesHome(navController: NavController) { HorizontalDivider() + DemoListItemView( + title = stringResource(R.string.showcase_time_based_content), + modifier = itemModifier, + onClick = { navController.navigate(NavigationRoutes.TimeBasedContent) } + ) + + HorizontalDivider() + DemoListItemView( title = stringResource(R.string.adaptive), modifier = itemModifier, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt index f0af5928d..08485630f 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt @@ -21,6 +21,7 @@ import ch.srgssr.pillarbox.demo.ui.showcases.misc.ResizablePlayerShowcase import ch.srgssr.pillarbox.demo.ui.showcases.misc.SmoothSeekingShowcase import ch.srgssr.pillarbox.demo.ui.showcases.misc.SphericalSurfaceShowcase import ch.srgssr.pillarbox.demo.ui.showcases.misc.StartAtGivenTimeShowcase +import ch.srgssr.pillarbox.demo.ui.showcases.misc.TimeBasedContent import ch.srgssr.pillarbox.demo.ui.showcases.misc.TrackingToggleShowcase import ch.srgssr.pillarbox.demo.ui.showcases.misc.UpdatableMediaItemShowcase import ch.srgssr.pillarbox.demo.ui.showcases.playlists.CustomPlaybackSettingsShowcase @@ -74,6 +75,9 @@ fun NavGraphBuilder.showcasesNavGraph(navController: NavController) { composable(DemoPageView("ThumbnailShowcase", Levels)) { ThumbnailView() } + composable(DemoPageView("TimeBasedContent", Levels)) { + TimeBasedContent() + } } private val Levels = listOf("app", "pillarbox", "showcase") diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ContentNotYetAvailableViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ContentNotYetAvailableViewModel.kt index 751a5c77a..6491b25a7 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ContentNotYetAvailableViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ContentNotYetAvailableViewModel.kt @@ -17,6 +17,8 @@ import ch.srgssr.pillarbox.player.PillarboxExoPlayer import ch.srgssr.pillarbox.player.asset.Asset import ch.srgssr.pillarbox.player.asset.AssetLoader import kotlinx.datetime.Clock +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes /** @@ -27,7 +29,7 @@ import kotlin.time.Duration.Companion.minutes class ContentNotYetAvailableViewModel(application: Application) : AndroidViewModel(application) { private class AlwaysStartDateBlockedAssetLoader(context: Context) : AssetLoader(DefaultMediaSourceFactory(context)) { private val srgAssetLoader = SRGAssetLoader(context) - private val validFrom = Clock.System.now().plus(1.minutes) + private val validFrom = Clock.System.now().plus(2.days + 1.hours + 34.minutes) override fun canLoadAsset(mediaItem: MediaItem): Boolean { return srgAssetLoader.canLoadAsset(mediaItem) } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TimeBasedContent.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TimeBasedContent.kt new file mode 100644 index 000000000..cb118f10b --- /dev/null +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TimeBasedContent.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.ui.showcases.misc + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.LifecycleStartEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import ch.srgssr.pillarbox.demo.ui.components.DemoListHeaderView +import ch.srgssr.pillarbox.demo.ui.components.DemoListItemView +import ch.srgssr.pillarbox.demo.ui.components.DemoListSectionView +import ch.srgssr.pillarbox.demo.ui.player.DemoPlayerView +import ch.srgssr.pillarbox.demo.ui.theme.paddings +import ch.srgssr.pillarbox.player.extension.seekToUnixTimeMs +import kotlinx.datetime.Clock + +/** + * Time-based content that demonstrates how to use timestamp-based api. + */ +@Composable +fun TimeBasedContent() { + val viewModel: TimeBasedContentViewModel = viewModel() + val player = viewModel.player + val timedEvents by viewModel.deltaTimeEvents.collectAsStateWithLifecycle() + + LifecycleStartEffect(player) { + player.play() + onStopOrDispose { + player.pause() + } + } + Column { + DemoPlayerView(player = player, modifier = Modifier.weight(1f)) + LazyColumn( + modifier = Modifier + .padding(horizontal = MaterialTheme.paddings.baseline) + .weight(1f), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.small) + ) { + item { + DemoListHeaderView("Timed events") + } + item { + DemoListSectionView { + timedEvents.forEachIndexed { index, timedEvent -> + DemoListItemView( + title = timedEvent.name, + subtitle = "Delta time ${timedEvent.delta}", + modifier = Modifier + .fillMaxWidth() + .minimumInteractiveComponentSize() + ) { + val now = Clock.System.now() + player.seekToUnixTimeMs((now + timedEvent.delta).toEpochMilliseconds()) + } + + if (index < timedEvents.lastIndex) { + HorizontalDivider() + } + } + } + } + } + } +} diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TimeBasedContentViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TimeBasedContentViewModel.kt new file mode 100644 index 000000000..fb7400bd2 --- /dev/null +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TimeBasedContentViewModel.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.ui.showcases.misc + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import ch.srgssr.pillarbox.core.business.PillarboxExoPlayer +import ch.srgssr.pillarbox.demo.shared.data.DemoItem +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.seconds + +/** + * A view model that exposes some timed events. + * + * @param application The [Application]. + */ +class TimeBasedContentViewModel(application: Application) : AndroidViewModel(application) { + + /** + * Player + */ + val player = PillarboxExoPlayer(application) + + /** + * Timed events + */ + val deltaTimeEvents: StateFlow> = flow { + emit( + listOf( + DeltaTimeEvent(name = "Now", Duration.ZERO), + DeltaTimeEvent(name = "2 hours in the past", (-2).hours), + DeltaTimeEvent(name = "1 hour in the past", (-1).hours), + DeltaTimeEvent(name = "Near future", 30.seconds), + DeltaTimeEvent(name = "In 1 hour", 1.hours), + DeltaTimeEvent(name = "4 hours in the past", (-4).hours), + ) + ) + }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + + init { + player.setMediaItem(DemoItem.LiveTimestampVideoHLS.toMediaItem()) + player.prepare() + } + + override fun onCleared() { + player.release() + } + + /** + * @property name Name of the event. + * @property delta The delta [Duration] of the event from now. + */ + data class DeltaTimeEvent( + val name: String, + val delta: Duration, + ) +} diff --git a/pillarbox-demo/src/main/res/values/strings.xml b/pillarbox-demo/src/main/res/values/strings.xml index d7afe8e43..a17c0cfdf 100644 --- a/pillarbox-demo/src/main/res/values/strings.xml +++ b/pillarbox-demo/src/main/res/values/strings.xml @@ -43,4 +43,5 @@ Display an overlay on top of the video surface to show useful information. Enable metrics overlay Content always not yet available + Content with timestamps diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt index c6d8ba428..1094455b5 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt @@ -2,6 +2,8 @@ * Copyright (c) SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ +@file:Suppress("TooManyFunctions") + package ch.srgssr.pillarbox.player.extension import androidx.media3.common.C @@ -125,22 +127,35 @@ fun Player.isAtLiveEdge(positionMs: Long = currentPosition, window: Window = Win } /** - * Get the player's position timestamp of the media being played, or `null` if not available. - * - * @param window A reusable [Window] instance. + * Calculates the unix time corresponding to the given position in the current media item in milliseconds. * - * @return The player's position timestamp of the media being played, in milliseconds, or `null` if not available. + * @param positionMs The position in milliseconds within the current media item. Defaults to the current playback position. + * @param window A [Window] object to store the window information. A new instance will be created if not provided. + * @return The unix time corresponding to the given position, or [C.TIME_UNSET] if the timeline is empty or the window start time is unset. */ -internal fun Player.getPositionTimestamp(window: Window = Window()): Long? { - if (currentTimeline.isEmpty) { - return null - } - +@Suppress("ReturnCount") +fun Player.getUnixTimeMs(positionMs: Long = currentPosition, window: Window = Window()): Long { + if (currentTimeline.isEmpty) return C.TIME_UNSET currentTimeline.getWindow(currentMediaItemIndex, window) + if (window.windowStartTimeMs == C.TIME_UNSET) return C.TIME_UNSET + return window.windowStartTimeMs + if (positionMs != C.TIME_UNSET) positionMs else window.durationMs +} - return if (window.elapsedRealtimeEpochOffsetMs != C.TIME_UNSET) { - window.windowStartTimeMs + currentPosition - } else { - null +/** + * Seeks the player to the specified unix time in milliseconds within the current media item's window. + * + * This function calculates the seek position relative to the window's start time + * and uses it to seek the player. If the provided unix time or the window's start time + * is unset ([C.TIME_UNSET]), or if the current timeline is empty, the function does nothing. + * + * @param unixTimeMs The target unix time to seek to, in milliseconds. + * @param window A [Window] object to store the current window information. + * If not provided, a new [Window] object will be created. + */ +fun Player.seekToUnixTimeMs(unixTimeMs: Long, window: Window = Window()) { + if (unixTimeMs == C.TIME_UNSET || currentTimeline.isEmpty) return + currentTimeline.getWindow(currentMediaItemIndex, window) + if (window.windowStartTimeMs != C.TIME_UNSET) { + seekTo(unixTimeMs - window.windowStartTimeMs) } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/monitoring/models/ErrorMessageData.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/monitoring/models/ErrorMessageData.kt index 2fcb58206..742c225ac 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/monitoring/models/ErrorMessageData.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/monitoring/models/ErrorMessageData.kt @@ -4,8 +4,9 @@ */ package ch.srgssr.pillarbox.player.monitoring.models +import androidx.media3.common.C import androidx.media3.common.Player -import ch.srgssr.pillarbox.player.extension.getPositionTimestamp +import ch.srgssr.pillarbox.player.extension.getUnixTimeMs import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -40,7 +41,7 @@ data class ErrorMessageData( message = throwable.message.orEmpty(), name = throwable::class.simpleName.orEmpty(), position = player.currentPosition, - positionTimestamp = player.getPositionTimestamp(), + positionTimestamp = player.getUnixTimeMs().takeIf { it != C.TIME_UNSET }, url = url, ) }