Skip to content

Commit 7bfecc3

Browse files
StaehliJMGaetan89
andauthored
Improve live edge detection and provide Player extension (#505)
Co-authored-by: Gaëtan Muller <[email protected]>
1 parent 12ee176 commit 7bfecc3

File tree

3 files changed

+35
-27
lines changed
  • pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls
  • pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension
  • pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension

3 files changed

+35
-27
lines changed

pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerControls.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,13 @@ import androidx.media3.common.Player
2525
import ch.srgssr.pillarbox.demo.ui.player.LiveIndicator
2626
import ch.srgssr.pillarbox.demo.ui.theme.paddings
2727
import ch.srgssr.pillarbox.player.extension.canSeek
28+
import ch.srgssr.pillarbox.player.extension.isAtLiveEdge
2829
import ch.srgssr.pillarbox.ui.extension.availableCommandsAsState
2930
import ch.srgssr.pillarbox.ui.extension.currentMediaMetadataAsState
3031
import ch.srgssr.pillarbox.ui.extension.currentPositionAsState
31-
import ch.srgssr.pillarbox.ui.extension.getCurrentDefaultPositionAsState
3232
import ch.srgssr.pillarbox.ui.extension.isCurrentMediaItemLiveAsState
3333
import ch.srgssr.pillarbox.ui.extension.playWhenReadyAsState
3434

35-
private const val LiveEdgeThreshold = 5_000L
36-
3735
/**
3836
* Player controls
3937
*
@@ -54,10 +52,6 @@ fun PlayerControls(
5452
) {
5553
val mediaMetadata by player.currentMediaMetadataAsState()
5654
val isCurrentItemLive by player.isCurrentMediaItemLiveAsState()
57-
val currentPlaybackPosition by player.currentPositionAsState()
58-
val currentDefaultPosition by player.getCurrentDefaultPositionAsState()
59-
val playWhenReady by player.playWhenReadyAsState()
60-
val isAtLiveEdge = playWhenReady && currentPlaybackPosition >= (currentDefaultPosition - LiveEdgeThreshold)
6155
val availableCommand by player.availableCommandsAsState()
6256
Box(
6357
modifier = modifier.background(color = backgroundColor),
@@ -93,6 +87,9 @@ fun PlayerControls(
9387
}
9488

9589
if (isCurrentItemLive) {
90+
val currentPlaybackPosition by player.currentPositionAsState()
91+
val isPlayWhenReady by player.playWhenReadyAsState()
92+
val isAtLiveEdge = isPlayWhenReady && player.isAtLiveEdge(currentPlaybackPosition)
9693
LiveIndicator(
9794
modifier = Modifier
9895
.padding(MaterialTheme.paddings.mini),

pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Player.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ package ch.srgssr.pillarbox.player.extension
66

77
import androidx.media3.common.MediaItem
88
import androidx.media3.common.Player
9+
import androidx.media3.common.Timeline.Window
10+
import androidx.media3.exoplayer.dash.manifest.DashManifest
11+
import androidx.media3.exoplayer.hls.HlsManifest
12+
import kotlin.time.Duration.Companion.microseconds
13+
import kotlin.time.Duration.Companion.milliseconds
914

1015
/**
1116
* Get a snapshot of the current media items
@@ -47,3 +52,29 @@ fun Player.currentPositionPercentage(): Float {
4752
fun Player.setHandleAudioFocus(handleAudioFocus: Boolean) {
4853
setAudioAttributes(audioAttributes, handleAudioFocus)
4954
}
55+
56+
/**
57+
* Is at live edge
58+
*
59+
* @param positionMs The position in milliseconds.
60+
* @param window The optional Window.
61+
* @return if [positionMs] is at live edge.
62+
*/
63+
fun Player.isAtLiveEdge(positionMs: Long = currentPosition, window: Window = Window()): Boolean {
64+
if (!isCurrentMediaItemLive) return false
65+
currentTimeline.getWindow(currentMediaItemIndex, window)
66+
val offsetSeconds = when (val manifest = currentManifest) {
67+
is HlsManifest -> {
68+
manifest.mediaPlaylist.targetDurationUs.microseconds.inWholeSeconds
69+
}
70+
71+
is DashManifest -> {
72+
manifest.minBufferTimeMs.milliseconds.inWholeSeconds
73+
}
74+
75+
else -> {
76+
0L
77+
}
78+
}
79+
return playWhenReady && positionMs.milliseconds.inWholeSeconds >= window.defaultPositionMs.milliseconds.inWholeSeconds - offsetSeconds
80+
}

pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/extension/ComposablePlayer.kt

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@ import androidx.compose.runtime.collectAsState
1616
import androidx.compose.runtime.derivedStateOf
1717
import androidx.compose.runtime.getValue
1818
import androidx.compose.runtime.remember
19-
import androidx.media3.common.C
2019
import androidx.media3.common.MediaItem
2120
import androidx.media3.common.MediaMetadata
2221
import androidx.media3.common.PlaybackException
2322
import androidx.media3.common.Player
2423
import androidx.media3.common.Player.Commands
25-
import androidx.media3.common.Timeline
26-
import androidx.media3.common.Timeline.Window
2724
import androidx.media3.common.VideoSize
2825
import ch.srgssr.pillarbox.player.DefaultUpdateInterval
2926
import ch.srgssr.pillarbox.player.availableCommandsAsFlow
@@ -34,7 +31,6 @@ import ch.srgssr.pillarbox.player.durationAsFlow
3431
import ch.srgssr.pillarbox.player.extension.getCurrentMediaItems
3532
import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed
3633
import ch.srgssr.pillarbox.player.getAspectRatioAsFlow
37-
import ch.srgssr.pillarbox.player.getCurrentDefaultPositionAsFlow
3834
import ch.srgssr.pillarbox.player.getCurrentMediaItemIndexAsFlow
3935
import ch.srgssr.pillarbox.player.getCurrentMediaItemsAsFlow
4036
import ch.srgssr.pillarbox.player.getPlaybackSpeedAsFlow
@@ -252,19 +248,3 @@ fun Player.isCurrentMediaItemLiveAsState(): State<Boolean> {
252248
}
253249
return flow.collectAsState(initial = isCurrentMediaItemLive)
254250
}
255-
256-
/**
257-
* @return The current default position as state.
258-
* @see Timeline.Window.getDefaultPositionMs
259-
*/
260-
@Composable
261-
fun Player.getCurrentDefaultPositionAsState(): LongState {
262-
val flow = remember(this) {
263-
getCurrentDefaultPositionAsFlow()
264-
}
265-
val window = remember {
266-
Window()
267-
}
268-
val initialValue = if (!currentTimeline.isEmpty) currentTimeline.getWindow(currentMediaItemIndex, window).defaultPositionMs else C.TIME_UNSET
269-
return flow.collectAsState(initial = initialValue).asLongState()
270-
}

0 commit comments

Comments
 (0)