From 6e875d304aac2ff30e5350d3249d77553e5b3b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 27 Nov 2024 10:53:18 +0100 Subject: [PATCH 01/11] Add player UTC timestamp api --- .../pillarbox/player/extension/Player.kt | 41 +++++++++++++------ .../monitoring/models/ErrorMessageData.kt | 5 ++- 2 files changed, 31 insertions(+), 15 deletions(-) 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 9ce042d6d..c4d343df5 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 @@ -118,22 +120,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 UTC time corresponding to the given position in the current media item. * - * @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 UTC 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.getPositionTimeUtc(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 UTC time 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 UTC time or the window's start time + * is unset (C.TIME_UNSET), or if the current timeline is empty, the function does nothing. + * + * @param utcTime The target UTC 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.seekToUtcTime(utcTime: Long, window: Window = Window()) { + if (utcTime == C.TIME_UNSET || currentTimeline.isEmpty) return + currentTimeline.getWindow(currentMediaItemIndex, window) + if (window.windowStartTimeMs != C.TIME_UNSET) { + seekTo(utcTime - 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 53bc4c504..d0006125b 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.getPositionTimeUtc 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.getPositionTimeUtc().let { if (it == C.TIME_UNSET) null else it }, url = url, ) } From 11e5b848cf37f1c33b70eed4de34defe5ab6f969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 27 Nov 2024 10:53:59 +0100 Subject: [PATCH 02/11] Improve demo to display current player time when possible --- .../demo/shared/ui/LocalTimeFormatter.kt | 22 +++++++++++++++++++ .../ui/player/controls/PlayerTimeSlider.kt | 19 +++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/LocalTimeFormatter.kt 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/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..8b0ba32a5 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,11 +23,14 @@ 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.getPositionTimeUtc import ch.srgssr.pillarbox.ui.ProgressTrackerState import ch.srgssr.pillarbox.ui.SimpleProgressTrackerState import ch.srgssr.pillarbox.ui.SmoothProgressTrackerState @@ -35,6 +38,9 @@ import ch.srgssr.pillarbox.ui.extension.availableCommandsAsState import ch.srgssr.pillarbox.ui.extension.currentBufferedPercentageAsState import ch.srgssr.pillarbox.ui.extension.durationAsState 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 +82,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 +102,17 @@ fun PlayerTimeSlider( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.mini) ) { - Text(text = formatter(currentProgress), color = Color.White) + val positionLabel = when (val timePosition = player.getPositionTimeUtc(currentProgress.inWholeMilliseconds, window)) { + 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, From f6115a1fadf02c39b19e0a66e221da5efe10e2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 27 Nov 2024 10:54:11 +0100 Subject: [PATCH 03/11] Add a time based showcase --- .../demo/shared/ui/NavigationRoutes.kt | 3 + .../demo/ui/showcases/ShowcasesHome.kt | 8 +++ .../demo/ui/showcases/ShowcasesNavigation.kt | 4 ++ .../ui/showcases/misc/TimeBasedContent.kt | 71 +++++++++++++++++++ .../misc/TimeBasedContentViewModel.kt | 65 +++++++++++++++++ .../src/main/res/values/strings.xml | 1 + 6 files changed, 152 insertions(+) create mode 100644 pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TimeBasedContent.kt create mode 100644 pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TimeBasedContentViewModel.kt 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/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/TimeBasedContent.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TimeBasedContent.kt new file mode 100644 index 000000000..039f93bde --- /dev/null +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TimeBasedContent.kt @@ -0,0 +1,71 @@ +/* + * 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.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +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.seekToUtcTime +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() + + LaunchedEffect(player) { + player.play() + } + 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 { + for (it in timedEvents) { + DemoListItemView( + title = it.name, + subtitle = "Delta time ${it.delta}", + modifier = Modifier + .fillMaxWidth() + .minimumInteractiveComponentSize() + ) { + val now = Clock.System.now() + player.seekToUtcTime((now + it.delta).toEpochMilliseconds()) + } + 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..28dcf5460 --- /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 hour 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 hour 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 From b0a56561773a2b4d96721107b28fd550dd83fd6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 27 Nov 2024 11:08:47 +0100 Subject: [PATCH 04/11] Demo display current time only when it is a Live stream. --- .../ui/player/controls/PlayerTimeSlider.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) 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 8b0ba32a5..0e2d1a4f4 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 @@ -37,6 +37,7 @@ 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 @@ -102,16 +103,19 @@ fun PlayerTimeSlider( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.mini) ) { - val positionLabel = when (val timePosition = player.getPositionTimeUtc(currentProgress.inWholeMilliseconds, window)) { - C.TIME_UNSET -> { - formatter(currentProgress) - } + val isLive by player.isCurrentMediaItemLiveAsState() + // We choose to display local time only when it is live, but it is possible to have timestamp inside VoD. + val positionLabel = + when (val timePosition = if (isLive) player.getPositionTimeUtc(currentProgress.inWholeMilliseconds, window) else C.TIME_UNSET) { + C.TIME_UNSET -> { + formatter(currentProgress) + } - else -> { - val localTime = Instant.fromEpochMilliseconds(timePosition).toLocalDateTime(TimeZone.currentSystemDefault()).time - localTimeFormatter.format(localTime) + else -> { + val localTime = Instant.fromEpochMilliseconds(timePosition).toLocalDateTime(TimeZone.currentSystemDefault()).time + localTimeFormatter.format(localTime) + } } - } Text(text = positionLabel, color = Color.White) PillarboxSlider( From 0b2c607e4447c937156b8b0415b3305d3f812288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 27 Nov 2024 11:45:13 +0100 Subject: [PATCH 05/11] Add timestamp to demo tv --- .../demo/tv/ui/player/compose/PlayerView.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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..f1cace53f 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 @@ -51,6 +51,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 +61,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.getPositionTimeUtc 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 +76,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 +312,15 @@ private fun PlayerTimeRow( var compactMode by remember { mutableStateOf(true) } + val isLive by player.isCurrentMediaItemLiveAsState() + val positionTime = if (isLive) player.getPositionTimeUtc(positionMs) else C.TIME_UNSET + val positionLabel = when (positionTime) { + C.TIME_UNSET -> formatter(positionMs.milliseconds) + else -> localTimeFormatter.format(Instant.fromEpochMilliseconds(positionTime).toLocalDateTime(TimeZone.currentSystemDefault()).time) + } Text( - text = "${formatter(positionMs.milliseconds)} / ${formatter(duration)}", + text = " $positionLabel/ ${formatter(duration)}", modifier = Modifier.padding( top = MaterialTheme.paddings.baseline, bottom = MaterialTheme.paddings.small, From e0efad9de2cdb882bd698c2ba328cc6d20cb6c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Thu, 28 Nov 2024 10:57:57 +0100 Subject: [PATCH 06/11] Rename methods --- .../demo/tv/ui/player/compose/PlayerView.kt | 4 ++-- .../ui/player/controls/PlayerTimeSlider.kt | 4 ++-- .../demo/ui/showcases/misc/TimeBasedContent.kt | 4 ++-- .../pillarbox/player/extension/Player.kt | 18 +++++++++--------- .../monitoring/models/ErrorMessageData.kt | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) 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 f1cace53f..ccccfe3df 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 @@ -61,7 +61,7 @@ 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.getPositionTimeUtc +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 @@ -313,7 +313,7 @@ private fun PlayerTimeRow( mutableStateOf(true) } val isLive by player.isCurrentMediaItemLiveAsState() - val positionTime = if (isLive) player.getPositionTimeUtc(positionMs) else C.TIME_UNSET + val positionTime = if (isLive) player.getUnixTimeMs(positionMs) else C.TIME_UNSET val positionLabel = when (positionTime) { C.TIME_UNSET -> formatter(positionMs.milliseconds) else -> localTimeFormatter.format(Instant.fromEpochMilliseconds(positionTime).toLocalDateTime(TimeZone.currentSystemDefault()).time) 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 0e2d1a4f4..0658fae45 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 @@ -30,7 +30,7 @@ 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.getPositionTimeUtc +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 @@ -106,7 +106,7 @@ fun PlayerTimeSlider( val isLive by player.isCurrentMediaItemLiveAsState() // We choose to display local time only when it is live, but it is possible to have timestamp inside VoD. val positionLabel = - when (val timePosition = if (isLive) player.getPositionTimeUtc(currentProgress.inWholeMilliseconds, window) else C.TIME_UNSET) { + when (val timePosition = if (isLive) player.getUnixTimeMs(currentProgress.inWholeMilliseconds, window) else C.TIME_UNSET) { C.TIME_UNSET -> { formatter(currentProgress) } 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 index 039f93bde..dd58d0ed6 100644 --- 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 @@ -23,7 +23,7 @@ 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.seekToUtcTime +import ch.srgssr.pillarbox.player.extension.seekToUnixTimeMs import kotlinx.datetime.Clock /** @@ -60,7 +60,7 @@ fun TimeBasedContent() { .minimumInteractiveComponentSize() ) { val now = Clock.System.now() - player.seekToUtcTime((now + it.delta).toEpochMilliseconds()) + player.seekToUnixTimeMs((now + it.delta).toEpochMilliseconds()) } HorizontalDivider() } 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 c4d343df5..beee0a06e 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 @@ -120,14 +120,14 @@ fun Player.isAtLiveEdge(positionMs: Long = currentPosition, window: Window = Win } /** - * Calculates the UTC time corresponding to the given position in the current media item. + * Calculates the unix time corresponding to the given position in the current media item in milliseconds. * * @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 UTC time corresponding to the given position, or [C.TIME_UNSET] if the timeline is empty or the window start time is unset. + * @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. */ @Suppress("ReturnCount") -fun Player.getPositionTimeUtc(positionMs: Long = currentPosition, window: Window = Window()): Long { +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 @@ -135,20 +135,20 @@ fun Player.getPositionTimeUtc(positionMs: Long = currentPosition, window: Window } /** - * Seeks the player to the specified UTC time within the current media item's window. + * 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 UTC time or 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 utcTime The target UTC time to seek to, in milliseconds. + * @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.seekToUtcTime(utcTime: Long, window: Window = Window()) { - if (utcTime == C.TIME_UNSET || currentTimeline.isEmpty) return +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(utcTime - window.windowStartTimeMs) + 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 d0006125b..2e2b6f829 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 @@ -6,7 +6,7 @@ package ch.srgssr.pillarbox.player.monitoring.models import androidx.media3.common.C import androidx.media3.common.Player -import ch.srgssr.pillarbox.player.extension.getPositionTimeUtc +import ch.srgssr.pillarbox.player.extension.getUnixTimeMs import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -41,7 +41,7 @@ data class ErrorMessageData( message = throwable.message.orEmpty(), name = throwable::class.simpleName.orEmpty(), position = player.currentPosition, - positionTimestamp = player.getPositionTimeUtc().let { if (it == C.TIME_UNSET) null else it }, + positionTimestamp = player.getUnixTimeMs().let { if (it == C.TIME_UNSET) null else it }, url = url, ) } From fcc8786f5ac13fc5c42c18d9942330e562c412c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Thu, 28 Nov 2024 17:07:15 +0100 Subject: [PATCH 07/11] Update documentation --- .../srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt | 2 +- .../pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt | 1 + .../demo/ui/showcases/misc/TimeBasedContentViewModel.kt | 4 ++-- .../main/java/ch/srgssr/pillarbox/player/extension/Player.kt | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) 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 ccccfe3df..bb8e16136 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 @@ -320,7 +320,7 @@ private fun PlayerTimeRow( } Text( - text = " $positionLabel/ ${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/controls/PlayerTimeSlider.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt index 0658fae45..2ff96d6bf 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 @@ -116,6 +116,7 @@ fun PlayerTimeSlider( localTimeFormatter.format(localTime) } } + Text(text = positionLabel, color = Color.White) PillarboxSlider( 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 index 28dcf5460..fb7400bd2 100644 --- 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 @@ -36,11 +36,11 @@ class TimeBasedContentViewModel(application: Application) : AndroidViewModel(app emit( listOf( DeltaTimeEvent(name = "Now", Duration.ZERO), - DeltaTimeEvent(name = "2 hour in the past", (-2).hours), + 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 hour in the past", (-4).hours), + DeltaTimeEvent(name = "4 hours in the past", (-4).hours), ) ) }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) 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 beee0a06e..59dac9f41 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 @@ -139,11 +139,11 @@ fun Player.getUnixTimeMs(positionMs: Long = currentPosition, window: Window = Wi * * 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. + * 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. + * 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 From 7f6337b49cbbd1e2ca3d333a3eff18b68f706700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Thu, 28 Nov 2024 17:09:07 +0100 Subject: [PATCH 08/11] Simplify code --- .../demo/tv/ui/player/compose/PlayerView.kt | 6 +++++- .../demo/ui/player/controls/PlayerTimeSlider.kt | 7 +++---- .../demo/ui/showcases/misc/TimeBasedContent.kt | 13 ++++++++----- .../player/monitoring/models/ErrorMessageData.kt | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) 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 bb8e16136..f017b2624 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 @@ -316,7 +316,11 @@ private fun PlayerTimeRow( val positionTime = if (isLive) player.getUnixTimeMs(positionMs) else C.TIME_UNSET val positionLabel = when (positionTime) { C.TIME_UNSET -> formatter(positionMs.milliseconds) - else -> localTimeFormatter.format(Instant.fromEpochMilliseconds(positionTime).toLocalDateTime(TimeZone.currentSystemDefault()).time) + + else -> { + val localTime = Instant.fromEpochMilliseconds(positionTime).toLocalDateTime(TimeZone.currentSystemDefault()).time + localTimeFormatter.format(localTime) + } } Text( 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 2ff96d6bf..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 @@ -104,12 +104,11 @@ fun PlayerTimeSlider( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.mini) ) { 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 (val timePosition = if (isLive) player.getUnixTimeMs(currentProgress.inWholeMilliseconds, window) else C.TIME_UNSET) { - C.TIME_UNSET -> { - formatter(currentProgress) - } + when (timePosition) { + C.TIME_UNSET -> formatter(currentProgress) else -> { val localTime = Instant.fromEpochMilliseconds(timePosition).toLocalDateTime(TimeZone.currentSystemDefault()).time 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 index dd58d0ed6..6de98e338 100644 --- 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 @@ -51,18 +51,21 @@ fun TimeBasedContent() { } item { DemoListSectionView { - for (it in timedEvents) { + timedEvents.forEachIndexed { index, timedEvent -> DemoListItemView( - title = it.name, - subtitle = "Delta time ${it.delta}", + title = timedEvent.name, + subtitle = "Delta time ${timedEvent.delta}", modifier = Modifier .fillMaxWidth() .minimumInteractiveComponentSize() ) { val now = Clock.System.now() - player.seekToUnixTimeMs((now + it.delta).toEpochMilliseconds()) + player.seekToUnixTimeMs((now + timedEvent.delta).toEpochMilliseconds()) + } + + if (index < timedEvents.lastIndex) { + HorizontalDivider() } - HorizontalDivider() } } } 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 2e2b6f829..bde1541f9 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 @@ -41,7 +41,7 @@ data class ErrorMessageData( message = throwable.message.orEmpty(), name = throwable::class.simpleName.orEmpty(), position = player.currentPosition, - positionTimestamp = player.getUnixTimeMs().let { if (it == C.TIME_UNSET) null else it }, + positionTimestamp = player.getUnixTimeMs().takeIf { it != C.TIME_UNSET }, url = url, ) } From 724f932198f0f8304d1b7a1d62cbbccf4e5e61d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Tue, 3 Dec 2024 15:53:20 +0100 Subject: [PATCH 09/11] Fix countdown duration more that 1 days --- .../pillarbox/demo/ui/player/CountdownView.kt | 33 +++++-------------- .../misc/ContentNotYetAvailableViewModel.kt | 4 ++- 2 files changed, 11 insertions(+), 26 deletions(-) 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/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) } From 1056e9f949e6508ff3e1fd044f86eb17b5f8f71f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Tue, 3 Dec 2024 16:04:48 +0100 Subject: [PATCH 10/11] Pass Window instead of creating one each time --- .../srgssr/pillarbox/demo/tv/ui/player/compose/PlayerView.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 f017b2624..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 @@ -313,7 +314,8 @@ private fun PlayerTimeRow( mutableStateOf(true) } val isLive by player.isCurrentMediaItemLiveAsState() - val positionTime = if (isLive) player.getUnixTimeMs(positionMs) else C.TIME_UNSET + 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) From 8827960f88297144746cb43330be2c0ea89d627d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Tue, 3 Dec 2024 16:05:28 +0100 Subject: [PATCH 11/11] Use Lifecyle effet to play/pause the player. --- .../pillarbox/demo/ui/showcases/misc/TimeBasedContent.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 index 6de98e338..cb118f10b 100644 --- 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 @@ -13,9 +13,9 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect 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 @@ -35,8 +35,11 @@ fun TimeBasedContent() { val player = viewModel.player val timedEvents by viewModel.deltaTimeEvents.collectAsStateWithLifecycle() - LaunchedEffect(player) { + LifecycleStartEffect(player) { player.play() + onStopOrDispose { + player.pause() + } } Column { DemoPlayerView(player = player, modifier = Modifier.weight(1f))