Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import ch.srgssr.pillarbox.ui.extension.isCurrentMediaItemLiveAsState
import ch.srgssr.pillarbox.ui.extension.isPlayingAsState
import ch.srgssr.pillarbox.ui.extension.playerErrorAsState
import ch.srgssr.pillarbox.ui.state.rememberCreditState
import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface
import ch.srgssr.pillarbox.ui.widget.player.PlayerFrame
import coil3.compose.AsyncImage
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -183,7 +183,7 @@ fun PlayerView(
onRetry = player::prepare,
)
} else {
PlayerSurface(
PlayerFrame(
player = player,
modifier = Modifier
.fillMaxSize()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.common.Player
Expand All @@ -47,7 +48,6 @@ import ch.srgssr.pillarbox.demo.ui.player.playlist.PlaylistView
import ch.srgssr.pillarbox.demo.ui.player.settings.PlaybackSettingsContent
import ch.srgssr.pillarbox.demo.ui.player.state.rememberFullscreenButtonState
import ch.srgssr.pillarbox.player.PillarboxPlayer
import ch.srgssr.pillarbox.ui.ScaleMode

/**
* Demo player
Expand Down Expand Up @@ -137,15 +137,15 @@ private fun PlayerContent(
val appSettings by appSettingsViewModel.currentAppSettings.collectAsStateWithLifecycle()

Column(modifier = modifier) {
var pinchScaleMode by remember(fullscreenButtonState.isInFullscreen) {
mutableStateOf(ScaleMode.Fit)
var pinchContentScale by remember(fullscreenButtonState.isInFullscreen) {
mutableStateOf(ContentScale.Fit)
}
val scalableModifier = if (fullscreenButtonState.isInFullscreen) {
Modifier.pointerInput(pinchScaleMode) {
Modifier.pointerInput(pinchContentScale) {
var lastZoomValue = 1f
detectTransformGestures(true) { _, _, zoom, _ ->
lastZoomValue *= zoom
pinchScaleMode = if (lastZoomValue < 1f) ScaleMode.Fit else ScaleMode.Crop
pinchContentScale = if (lastZoomValue < 1f) ContentScale.Fit else ContentScale.Crop
}
}
} else {
Expand All @@ -159,7 +159,7 @@ private fun PlayerContent(
player = player,
controlsToggleable = !isInPictureInPicture,
controlsVisible = !isInPictureInPicture,
scaleMode = pinchScaleMode,
contentScale = pinchContentScale,
overlayEnabled = appSettings.metricsOverlayEnabled,
overlayOptions = MetricsOverlayOptions(
textColor = appSettings.metricsOverlayTextColor.color,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
*/
package ch.srgssr.pillarbox.demo.ui.player

import android.net.Uri
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
Expand All @@ -26,21 +24,20 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.media3.common.DeviceInfo
import androidx.media3.common.Player
import androidx.media3.ui.compose.state.rememberPresentationState
import ch.srgssr.pillarbox.demo.shared.R
import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent
import ch.srgssr.pillarbox.demo.shared.ui.player.DefaultVisibilityDelay
import ch.srgssr.pillarbox.demo.shared.ui.player.metrics.MetricsOverlay
import ch.srgssr.pillarbox.demo.shared.ui.player.rememberDelayedControlsVisibility
import ch.srgssr.pillarbox.demo.shared.ui.player.rememberProgressTrackerState
import ch.srgssr.pillarbox.demo.shared.ui.player.shouldDisplayArtworkAsState
import ch.srgssr.pillarbox.demo.shared.ui.rememberIsTalkBackEnabled
import ch.srgssr.pillarbox.demo.shared.ui.settings.MetricsOverlayOptions
import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerControls
Expand All @@ -50,8 +47,6 @@ import ch.srgssr.pillarbox.demo.ui.player.controls.SkipButton
import ch.srgssr.pillarbox.demo.ui.theme.paddings
import ch.srgssr.pillarbox.player.PillarboxPlayer
import ch.srgssr.pillarbox.ui.ProgressTrackerState
import ch.srgssr.pillarbox.ui.ScaleMode
import ch.srgssr.pillarbox.ui.exoplayer.ExoPlayerSubtitleView
import ch.srgssr.pillarbox.ui.extension.currentMediaMetadataAsState
import ch.srgssr.pillarbox.ui.extension.getDeviceInfoAsState
import ch.srgssr.pillarbox.ui.extension.getPeriodicallyCurrentMetricsAsState
Expand All @@ -62,7 +57,7 @@ import ch.srgssr.pillarbox.ui.extension.playerErrorAsState
import ch.srgssr.pillarbox.ui.state.CreditState
import ch.srgssr.pillarbox.ui.state.rememberCreditState
import ch.srgssr.pillarbox.ui.widget.keepScreenOn
import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface
import ch.srgssr.pillarbox.ui.widget.player.PlayerFrame
import coil3.compose.AsyncImage
import kotlin.time.Duration.Companion.ZERO
import kotlin.time.Duration.Companion.milliseconds
Expand All @@ -72,7 +67,7 @@ import kotlin.time.Duration.Companion.milliseconds
*
* @param player The [Player] to observe.
* @param modifier The modifier to be applied to the layout.
* @param scaleMode The surface scale mode.
* @param contentScale The surface [ContentScale].
* @param controlsVisible The control visibility.
* @param controlsToggleable The controls are toggleable.
* @param progressTracker The progress tracker.
Expand All @@ -84,34 +79,107 @@ import kotlin.time.Duration.Companion.milliseconds
fun PlayerView(
player: PillarboxPlayer,
modifier: Modifier = Modifier,
scaleMode: ScaleMode = ScaleMode.Fit,
contentScale: ContentScale = ContentScale.Fit,
controlsVisible: Boolean = true,
controlsToggleable: Boolean = true,
progressTracker: ProgressTrackerState = rememberProgressTrackerState(player = player),
overlayOptions: MetricsOverlayOptions = MetricsOverlayOptions(),
overlayEnabled: Boolean = false,
content: @Composable ColumnScope.() -> Unit = {},
) {
val playerError by player.playerErrorAsState()
playerError?.let {
val sessionId = remember {
player.getCurrentPlaybackSessionId()
val presentationState = rememberPresentationState(player, keepContentOnReset = false)
PlayerFrame(
modifier = modifier,
player = player,
contentScale = contentScale,
presentationState = presentationState,
shutter = {
val deviceInfo by player.getDeviceInfoAsState()
val mediaMetadata by player.currentMediaMetadataAsState()
val placeholder = if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE) {
androidx.media3.cast.R.drawable.ic_mr_button_disconnected_dark
} else {
R.drawable.placeholder
}
val placeHolderPainter = painterResource(placeholder)
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Black)
) {
AsyncImage(
modifier = Modifier
.matchParentSize()
.background(color = Color.Black)
.align(Alignment.Center),
model = mediaMetadata.artworkUri,
contentDescription = null,
contentScale = ContentScale.Fit,
placeholder = placeHolderPainter,
error = placeHolderPainter,
)
}
}
) {
val playerError by player.playerErrorAsState()
playerError?.let {
val sessionId = remember {
player.getCurrentPlaybackSessionId()
}
PlayerError(
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center),
playerError = it,
sessionId = sessionId,
onRetry = player::prepare
)
return@PlayerFrame
}
PlayerError(
modifier = modifier,
playerError = it,
sessionId = sessionId,
onRetry = player::prepare

val hasMediaItem by player.hasMediaItemsAsState()
if (!hasMediaItem) {
PlayerNoContent(
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center)
)
return@PlayerFrame
}

if (overlayEnabled) {
val currentMetrics by player.getPeriodicallyCurrentMetricsAsState(500.milliseconds)
currentMetrics?.let {
MetricsOverlay(
modifier = Modifier
.fillMaxSize()
.align(Alignment.TopStart),
playbackMetrics = it,
overlayOptions = overlayOptions,
)
}
}

PlayerOverlay(
player = player,
controlsVisible = controlsVisible,
controlsToggleable = controlsToggleable,
progressTracker = progressTracker,
controlsContent = content,
)
return
}
}

val hasMediaItem by player.hasMediaItemsAsState()
if (!hasMediaItem) {
PlayerNoContent(modifier = modifier)
return
}
@Composable
private fun PlayerOverlay(
player: PillarboxPlayer,
controlsVisible: Boolean,
controlsToggleable: Boolean,
progressTracker: ProgressTrackerState,
controlsContent: @Composable ColumnScope.() -> Unit,
) {
player.keepScreenOn()

val interactionSource = remember {
MutableInteractionSource()
}
Expand All @@ -120,15 +188,14 @@ fun PlayerView(
val isPlaying by player.isPlayingAsState()
val keepControlDelay = if (!talkBackEnabled && !isSliderDragged && isPlaying) DefaultVisibilityDelay else ZERO
val controlsVisibility = rememberDelayedControlsVisibility(initialVisible = controlsVisible, initialDelay = keepControlDelay)
val playbackState by player.playbackStateAsState()
val isBuffering = playbackState == Player.STATE_BUFFERING
val controlsStateDescription = if (controlsVisibility.visible) {
stringResource(R.string.controls_visible)
} else {
stringResource(R.string.controls_hidden)
}
Box(
modifier = modifier
modifier = Modifier
.fillMaxSize()
.toggleable(
value = controlsVisibility.visible,
enabled = controlsToggleable,
Expand All @@ -141,33 +208,6 @@ fun PlayerView(
}
) {
val creditState = rememberCreditState(player)
val shouldDisplayArtwork by player.shouldDisplayArtworkAsState()
val deviceInfo by player.getDeviceInfoAsState()
val mediaMetadata by player.currentMediaMetadataAsState()
val placeholder = if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE) {
androidx.media3.cast.R.drawable.ic_mr_button_disconnected_dark
} else {
R.drawable.placeholder
}

PlayerSurface(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Black),
player = player,
scaleMode = scaleMode
) {
SurfaceOverlay(
player = player,
displayBuffering = isBuffering && !isSliderDragged,
overlayEnabled = overlayEnabled,
overlayOptions = overlayOptions,
shouldDisplayArtwork = shouldDisplayArtwork,
artworkUri = mediaMetadata.artworkUri,
placeholder = painterResource(placeholder)
)
}

AnimatedVisibility(
visible = creditState.isInCredit && !controlsVisibility.visible,
modifier = Modifier
Expand All @@ -179,6 +219,8 @@ fun PlayerView(
SkipButton(onClick = creditState::onClick)
}

ProgressIndicator(player, isSliderDragged)

DemoControls(
modifier = Modifier
.matchParentSize()
Expand All @@ -192,56 +234,24 @@ fun PlayerView(
progressTracker = progressTracker,
interactionSource = interactionSource,
creditState = creditState,
content = content,
content = controlsContent,
)
}
}

@Composable
private fun BoxScope.SurfaceOverlay(
player: Player,
displayBuffering: Boolean,
overlayEnabled: Boolean,
overlayOptions: MetricsOverlayOptions,
shouldDisplayArtwork: Boolean,
artworkUri: Uri?,
placeholder: Painter?
) {
if (shouldDisplayArtwork) {
AsyncImage(
modifier = Modifier
.matchParentSize()
.background(color = Color.Black)
.align(Alignment.Center),
model = artworkUri,
contentDescription = null,
contentScale = ContentScale.Fit,
placeholder = placeholder,
error = placeholder,
)
}
private fun ProgressIndicator(player: PillarboxPlayer, isSliderDragging: Boolean) {
val playbackState by player.playbackStateAsState()
val isBuffering = playbackState == Player.STATE_BUFFERING
AnimatedVisibility(
displayBuffering,
isBuffering && !isSliderDragging,
enter = fadeIn(),
exit = fadeOut(),
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center), color = Color.White)
}
}
ExoPlayerSubtitleView(player = player)
if (overlayEnabled && player is PillarboxPlayer) {
val currentMetrics by player.getPeriodicallyCurrentMetricsAsState(500.milliseconds)
currentMetrics?.let {
MetricsOverlay(
modifier = Modifier
.fillMaxSize()
.align(Alignment.TopStart),
playbackMetrics = it,
overlayOptions = overlayOptions,
)
}
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ fun PlayerControls(
) {
if (availableCommand.canSeek()) {
PlayerTimeSlider(
modifier = Modifier.weight(1f),
player = player,
progressTracker = progressTracker,
interactionSource = interactionSource
Expand Down
Loading
Loading