Add video autoplay to media gallery (#4499)

* Add video autoplay when opening the video from either the timeline or the media gallery

* Only autoplay when the video is first displayed
This commit is contained in:
Jorge Martin Espinosa
2025-04-01 09:44:06 +02:00
committed by GitHub
parent bb4933e4c3
commit 1eff15d6e4
11 changed files with 52 additions and 11 deletions

View File

@@ -19,6 +19,7 @@ import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
@@ -85,11 +86,13 @@ class TimelineMediaGalleryDataSource @Inject constructor(
}
}.flatMapLatest {
timelineMediaItemsFactory.timelineItems
}.map { timelineItems ->
mediaItemsPostProcessor.process(mediaItems = timelineItems)
}.map {
mediaTimeline.orCache(it)
}.onEach { groupedMediaItems ->
}
.distinctUntilChanged()
.map { timelineItems ->
val groupedItems = mediaItemsPostProcessor.process(mediaItems = timelineItems)
mediaTimeline.orCache(groupedItems)
}
.onEach { groupedMediaItems ->
groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems))
}
.onCompletion {

View File

@@ -31,6 +31,7 @@ fun LocalMediaView(
textFileViewer: TextFileViewer,
modifier: Modifier = Modifier,
isDisplayed: Boolean = true,
isUserSelected: Boolean = false,
localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(),
mediaInfo: MediaInfo? = localMedia?.info,
) {
@@ -47,6 +48,7 @@ fun LocalMediaView(
localMediaViewState = localMediaViewState,
bottomPaddingInPixels = bottomPaddingInPixels,
localMedia = localMedia,
autoplay = isUserSelected,
modifier = modifier,
)
mimeType == MimeTypes.PlainText -> TextFileView(

View File

@@ -111,6 +111,7 @@ private fun ExoPlayerMediaAudioView(
MediaPlayerControllerState(
isVisible = true,
isPlaying = false,
isReady = false,
progressInMillis = 0,
durationInMillis = 0,
canMute = false,

View File

@@ -12,6 +12,7 @@ import androidx.annotation.FloatRange
data class MediaPlayerControllerState(
val isVisible: Boolean,
val isPlaying: Boolean,
val isReady: Boolean,
val progressInMillis: Long,
val durationInMillis: Long,
val canMute: Boolean,

View File

@@ -27,6 +27,7 @@ open class MediaPlayerControllerStateProvider : PreviewParameterProvider<MediaPl
private fun aMediaPlayerControllerState(
isVisible: Boolean = true,
isPlaying: Boolean = false,
isReady: Boolean = false,
progressInMillis: Long = 0,
// Default to 1 minute and 23 seconds
durationInMillis: Long = 83_000,
@@ -35,6 +36,7 @@ private fun aMediaPlayerControllerState(
) = MediaPlayerControllerState(
isVisible = isVisible,
isPlaying = isPlaying,
isReady = isReady,
progressInMillis = progressInMillis,
durationInMillis = durationInMillis,
canMute = canMute,

View File

@@ -31,6 +31,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Player.STATE_READY
import androidx.media3.common.Timeline
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.AspectRatioFrameLayout
@@ -61,6 +62,7 @@ fun MediaVideoView(
localMediaViewState: LocalMediaViewState,
bottomPaddingInPixels: Int,
localMedia: LocalMedia?,
autoplay: Boolean,
modifier: Modifier = Modifier,
) {
val exoPlayer = rememberExoPlayer()
@@ -70,6 +72,7 @@ fun MediaVideoView(
bottomPaddingInPixels = bottomPaddingInPixels,
exoPlayer = exoPlayer,
localMedia = localMedia,
autoplay = autoplay,
modifier = modifier,
)
}
@@ -82,6 +85,7 @@ private fun ExoPlayerMediaVideoView(
bottomPaddingInPixels: Int,
exoPlayer: ExoPlayer,
localMedia: LocalMedia?,
autoplay: Boolean,
modifier: Modifier = Modifier,
) {
var mediaPlayerControllerState: MediaPlayerControllerState by remember {
@@ -89,6 +93,7 @@ private fun ExoPlayerMediaVideoView(
MediaPlayerControllerState(
isVisible = true,
isPlaying = false,
isReady = false,
progressInMillis = 0,
durationInMillis = 0,
canMute = true,
@@ -135,6 +140,12 @@ private fun ExoPlayerMediaVideoView(
}
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
mediaPlayerControllerState = mediaPlayerControllerState.copy(
isReady = playbackState == STATE_READY,
)
}
}
}
@@ -164,9 +175,17 @@ private fun ExoPlayerMediaVideoView(
)
}
}
LaunchedEffect(isDisplayed) {
// If not displayed, make sure to pause the video
if (!isDisplayed) {
var needsAutoPlay by remember { mutableStateOf(autoplay) }
LaunchedEffect(needsAutoPlay, isDisplayed, mediaPlayerControllerState.isReady) {
val isReadyAndNotPlaying = mediaPlayerControllerState.isReady && !mediaPlayerControllerState.isPlaying
if (needsAutoPlay && isDisplayed && isReadyAndNotPlaying) {
// When displayed, start autoplaying
exoPlayer.play()
needsAutoPlay = false
} else if (!isDisplayed && mediaPlayerControllerState.isPlaying) {
// If not displayed, make sure to pause the video
exoPlayer.pause()
}
}
@@ -259,5 +278,6 @@ internal fun MediaVideoViewPreview() = ElementPreview {
bottomPaddingInPixels = 0,
localMediaViewState = rememberLocalMediaViewState(),
localMedia = null,
autoplay = false,
)
}

View File

@@ -76,7 +76,7 @@ class MediaViewerDataSource(
}
@VisibleForTesting
fun dataFlow(): Flow<PersistentList<MediaViewerPageData>> {
internal fun dataFlow(): Flow<PersistentList<MediaViewerPageData>> {
return galleryDataSource.groupedMediaItemsFlow()
.map { groupedItems ->
when (groupedItems) {

View File

@@ -148,6 +148,7 @@ class MediaViewerPresenter @AssistedInject constructor(
}
return MediaViewerState(
initiallySelectedEventId = inputs.eventId,
listData = data.value,
currentIndex = currentIndex.intValue,
snackbarMessage = snackbarMessage,

View File

@@ -19,6 +19,7 @@ import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetSta
import kotlinx.collections.immutable.ImmutableList
data class MediaViewerState(
val initiallySelectedEventId: EventId?,
val listData: ImmutableList<MediaViewerPageData>,
val currentIndex: Int,
val snackbarMessage: SnackbarMessage?,

View File

@@ -12,6 +12,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.media.aWaveForm
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.MediaInfo
@@ -202,6 +203,7 @@ fun aMediaViewerState(
mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
eventSink: (MediaViewerEvents) -> Unit = {},
) = MediaViewerState(
initiallySelectedEventId = EventId("\$a:b"),
listData = listData.toPersistentList(),
currentIndex = currentIndex,
snackbarMessage = null,

View File

@@ -142,8 +142,13 @@ fun MediaViewerView(
Box(
modifier = Modifier.fillMaxSize()
) {
val isDisplayed = remember(pagerState.settledPage) {
// This 'item provider' lambda will be called when the data source changes with an outdated `settlePage` value
// So we need to update this value only when the `settledPage` value changes. It seems like a bug that needs to be fixed in Compose.
page == pagerState.settledPage
}
MediaViewerPage(
isDisplayed = page == pagerState.settledPage,
isDisplayed = isDisplayed,
showOverlay = showOverlay,
bottomPaddingInPixels = bottomPaddingInPixels,
data = dataForPage,
@@ -157,7 +162,8 @@ fun MediaViewerView(
},
onShowOverlayChange = {
showOverlay = it
}
},
isUserSelected = (state.listData[page] as? MediaViewerPageData.MediaViewerData)?.eventId == state.initiallySelectedEventId,
)
// Bottom bar
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
@@ -273,6 +279,7 @@ private fun MediaViewerPage(
bottomPaddingInPixels: Int,
data: MediaViewerPageData.MediaViewerData,
textFileViewer: TextFileViewer,
isUserSelected: Boolean,
onDismiss: () -> Unit,
onRetry: () -> Unit,
onDismissError: () -> Unit,
@@ -328,6 +335,7 @@ private fun MediaViewerPage(
currentOnShowOverlayChange(!currentShowOverlay)
}
},
isUserSelected = isUserSelected,
)
ThumbnailView(
mediaInfo = data.mediaInfo,