mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
[ui-compose] Add PresentationState
for first-frame info
It captures some information needed for the UI logic related to rendering of the video track (and later images) to the surface. Supporting `EVENT_RENDERED_FIRST_FRAME` helps improve the UX by covering the surface with an overlay (scrim/shutter) until the first frame is ready. This helps avoid sudden flickering during MediaItem transitions. PiperOrigin-RevId: 712889568
This commit is contained in:
parent
31e5142b72
commit
2dc6af1fae
@ -41,6 +41,8 @@
|
||||
* IMA extension:
|
||||
* Session:
|
||||
* UI:
|
||||
* Add `PresentationState` state holder class and the corresponding
|
||||
`rememberPresentationState` Composable to `media3-ui-compose`.
|
||||
* Downloads:
|
||||
* OkHttp Extension:
|
||||
* Cronet Extension:
|
||||
@ -63,6 +65,7 @@
|
||||
* Cast Extension:
|
||||
* Test Utilities:
|
||||
* Demo app:
|
||||
* Use `PresentationState` to cover the `PlayerSurface` with an overlay.
|
||||
* Remove deprecated symbols:
|
||||
|
||||
## 1.6
|
||||
|
@ -46,6 +46,7 @@ import androidx.media3.demo.compose.layout.noRippleClickable
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.compose.PlayerSurface
|
||||
import androidx.media3.ui.compose.SURFACE_TYPE_SURFACE_VIEW
|
||||
import androidx.media3.ui.compose.state.rememberPresentationState
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@ -100,13 +101,22 @@ private fun initializePlayer(context: Context): Player =
|
||||
@Composable
|
||||
private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) {
|
||||
var showControls by remember { mutableStateOf(true) }
|
||||
val presentationState = rememberPresentationState(player)
|
||||
|
||||
Box(modifier) {
|
||||
PlayerSurface(
|
||||
player = player,
|
||||
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
|
||||
modifier = modifier.noRippleClickable { showControls = !showControls },
|
||||
)
|
||||
|
||||
if (!presentationState.showSurface) {
|
||||
// hide the surface that is being prepared behind a shutter
|
||||
Box(modifier.background(Color.Black))
|
||||
}
|
||||
|
||||
if (showControls) {
|
||||
// drawn on top of a potential shutter
|
||||
MinimalControls(player, Modifier.align(Alignment.Center))
|
||||
ExtraControls(
|
||||
player,
|
||||
|
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.media3.ui.compose.state
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.listen
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
/**
|
||||
* Remembers the value of [PresentationState] created based on the passed [Player] and launches a
|
||||
* coroutine to listen to [Player]'s changes. If the [Player] instance changes between compositions,
|
||||
* produces and remembers a new value.
|
||||
*/
|
||||
@UnstableApi
|
||||
@Composable
|
||||
fun rememberPresentationState(player: Player): PresentationState {
|
||||
val presentationState = remember(player) { PresentationState(player) }
|
||||
LaunchedEffect(player) { presentationState.observe() }
|
||||
return presentationState
|
||||
}
|
||||
|
||||
/**
|
||||
* State that holds information to correctly deal with UI components related to the rendering of
|
||||
* frames to a surface.
|
||||
*
|
||||
* @property[showSurface] set to true when the Player emits [Player.EVENT_RENDERED_FIRST_FRAME] and
|
||||
* reset to false on [Player.EVENT_TRACKS_CHANGED] depending on the number and type of tracks.
|
||||
* @property[keepContentOnReset] whether the currently displayed video frame or media artwork is
|
||||
* kept visible when tracks change. Defaults to false.
|
||||
*/
|
||||
@UnstableApi
|
||||
class PresentationState(private val player: Player) {
|
||||
var showSurface by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var keepContentOnReset: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
maybeHideSurface(player)
|
||||
}
|
||||
|
||||
private var lastPeriodUidWithTracks: Any? = null
|
||||
|
||||
suspend fun observe(): Nothing =
|
||||
player.listen { events ->
|
||||
if (events.contains(Player.EVENT_RENDERED_FIRST_FRAME)) {
|
||||
showSurface = true
|
||||
}
|
||||
if (events.contains(Player.EVENT_TRACKS_CHANGED)) {
|
||||
maybeHideSurface(player)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeHideSurface(player: Player) {
|
||||
val hasTracks =
|
||||
player.isCommandAvailable(Player.COMMAND_GET_TRACKS) && !player.currentTracks.isEmpty
|
||||
if (!shouldKeepSurfaceVisible(player)) {
|
||||
if (!keepContentOnReset && !hasTracks) {
|
||||
showSurface = false
|
||||
}
|
||||
if (hasTracks && !hasSelectedVideoTrack()) {
|
||||
showSurface = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldKeepSurfaceVisible(player: Player): Boolean {
|
||||
// Suppress the shutter if transitioning to an unprepared period within the same window. This
|
||||
// is necessary to avoid closing the shutter (i.e hiding the surface) when such a transition
|
||||
// occurs. See: https://github.com/google/ExoPlayer/issues/5507.
|
||||
val timeline =
|
||||
if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) player.currentTimeline
|
||||
else Timeline.EMPTY
|
||||
|
||||
if (timeline.isEmpty) {
|
||||
lastPeriodUidWithTracks = null
|
||||
return false
|
||||
}
|
||||
|
||||
val period = Timeline.Period()
|
||||
if (player.isCommandAvailable(Player.COMMAND_GET_TRACKS) && !player.currentTracks.isEmpty) {
|
||||
lastPeriodUidWithTracks =
|
||||
timeline.getPeriod(player.currentPeriodIndex, period, /* setIds= */ true).uid
|
||||
} else
|
||||
lastPeriodUidWithTracks?.let {
|
||||
val lastPeriodIndexWithTracks = timeline.getIndexOfPeriod(it)
|
||||
if (lastPeriodIndexWithTracks != C.INDEX_UNSET) {
|
||||
val lastWindowIndexWithTracks =
|
||||
timeline.getPeriod(lastPeriodIndexWithTracks, period).windowIndex
|
||||
if (player.currentMediaItemIndex == lastWindowIndexWithTracks) {
|
||||
// We're in the same media item, keep the surface visible, don't show the shutter.
|
||||
return true
|
||||
}
|
||||
}
|
||||
lastPeriodUidWithTracks = null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun hasSelectedVideoTrack(): Boolean {
|
||||
return player.isCommandAvailable(Player.COMMAND_GET_TRACKS) &&
|
||||
player.currentTracks.isTypeSelected(C.TRACK_TYPE_VIDEO)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user