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:
committed by
GitHub
parent
bb4933e4c3
commit
1eff15d6e4
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -111,6 +111,7 @@ private fun ExoPlayerMediaAudioView(
|
||||
MediaPlayerControllerState(
|
||||
isVisible = true,
|
||||
isPlaying = false,
|
||||
isReady = false,
|
||||
progressInMillis = 0,
|
||||
durationInMillis = 0,
|
||||
canMute = false,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -148,6 +148,7 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||
}
|
||||
|
||||
return MediaViewerState(
|
||||
initiallySelectedEventId = inputs.eventId,
|
||||
listData = data.value,
|
||||
currentIndex = currentIndex.intValue,
|
||||
snackbarMessage = snackbarMessage,
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user