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:
parent
bb4933e4c3
commit
1eff15d6e4
11 changed files with 52 additions and 11 deletions
|
|
@ -19,6 +19,7 @@ import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -85,11 +86,13 @@ class TimelineMediaGalleryDataSource @Inject constructor(
|
||||||
}
|
}
|
||||||
}.flatMapLatest {
|
}.flatMapLatest {
|
||||||
timelineMediaItemsFactory.timelineItems
|
timelineMediaItemsFactory.timelineItems
|
||||||
}.map { timelineItems ->
|
}
|
||||||
mediaItemsPostProcessor.process(mediaItems = timelineItems)
|
.distinctUntilChanged()
|
||||||
}.map {
|
.map { timelineItems ->
|
||||||
mediaTimeline.orCache(it)
|
val groupedItems = mediaItemsPostProcessor.process(mediaItems = timelineItems)
|
||||||
}.onEach { groupedMediaItems ->
|
mediaTimeline.orCache(groupedItems)
|
||||||
|
}
|
||||||
|
.onEach { groupedMediaItems ->
|
||||||
groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems))
|
groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems))
|
||||||
}
|
}
|
||||||
.onCompletion {
|
.onCompletion {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ fun LocalMediaView(
|
||||||
textFileViewer: TextFileViewer,
|
textFileViewer: TextFileViewer,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
isDisplayed: Boolean = true,
|
isDisplayed: Boolean = true,
|
||||||
|
isUserSelected: Boolean = false,
|
||||||
localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(),
|
localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(),
|
||||||
mediaInfo: MediaInfo? = localMedia?.info,
|
mediaInfo: MediaInfo? = localMedia?.info,
|
||||||
) {
|
) {
|
||||||
|
|
@ -47,6 +48,7 @@ fun LocalMediaView(
|
||||||
localMediaViewState = localMediaViewState,
|
localMediaViewState = localMediaViewState,
|
||||||
bottomPaddingInPixels = bottomPaddingInPixels,
|
bottomPaddingInPixels = bottomPaddingInPixels,
|
||||||
localMedia = localMedia,
|
localMedia = localMedia,
|
||||||
|
autoplay = isUserSelected,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
mimeType == MimeTypes.PlainText -> TextFileView(
|
mimeType == MimeTypes.PlainText -> TextFileView(
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ private fun ExoPlayerMediaAudioView(
|
||||||
MediaPlayerControllerState(
|
MediaPlayerControllerState(
|
||||||
isVisible = true,
|
isVisible = true,
|
||||||
isPlaying = false,
|
isPlaying = false,
|
||||||
|
isReady = false,
|
||||||
progressInMillis = 0,
|
progressInMillis = 0,
|
||||||
durationInMillis = 0,
|
durationInMillis = 0,
|
||||||
canMute = false,
|
canMute = false,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.annotation.FloatRange
|
||||||
data class MediaPlayerControllerState(
|
data class MediaPlayerControllerState(
|
||||||
val isVisible: Boolean,
|
val isVisible: Boolean,
|
||||||
val isPlaying: Boolean,
|
val isPlaying: Boolean,
|
||||||
|
val isReady: Boolean,
|
||||||
val progressInMillis: Long,
|
val progressInMillis: Long,
|
||||||
val durationInMillis: Long,
|
val durationInMillis: Long,
|
||||||
val canMute: Boolean,
|
val canMute: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ open class MediaPlayerControllerStateProvider : PreviewParameterProvider<MediaPl
|
||||||
private fun aMediaPlayerControllerState(
|
private fun aMediaPlayerControllerState(
|
||||||
isVisible: Boolean = true,
|
isVisible: Boolean = true,
|
||||||
isPlaying: Boolean = false,
|
isPlaying: Boolean = false,
|
||||||
|
isReady: Boolean = false,
|
||||||
progressInMillis: Long = 0,
|
progressInMillis: Long = 0,
|
||||||
// Default to 1 minute and 23 seconds
|
// Default to 1 minute and 23 seconds
|
||||||
durationInMillis: Long = 83_000,
|
durationInMillis: Long = 83_000,
|
||||||
|
|
@ -35,6 +36,7 @@ private fun aMediaPlayerControllerState(
|
||||||
) = MediaPlayerControllerState(
|
) = MediaPlayerControllerState(
|
||||||
isVisible = isVisible,
|
isVisible = isVisible,
|
||||||
isPlaying = isPlaying,
|
isPlaying = isPlaying,
|
||||||
|
isReady = isReady,
|
||||||
progressInMillis = progressInMillis,
|
progressInMillis = progressInMillis,
|
||||||
durationInMillis = durationInMillis,
|
durationInMillis = durationInMillis,
|
||||||
canMute = canMute,
|
canMute = canMute,
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.Player.STATE_READY
|
||||||
import androidx.media3.common.Timeline
|
import androidx.media3.common.Timeline
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.ui.AspectRatioFrameLayout
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
|
|
@ -61,6 +62,7 @@ fun MediaVideoView(
|
||||||
localMediaViewState: LocalMediaViewState,
|
localMediaViewState: LocalMediaViewState,
|
||||||
bottomPaddingInPixels: Int,
|
bottomPaddingInPixels: Int,
|
||||||
localMedia: LocalMedia?,
|
localMedia: LocalMedia?,
|
||||||
|
autoplay: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val exoPlayer = rememberExoPlayer()
|
val exoPlayer = rememberExoPlayer()
|
||||||
|
|
@ -70,6 +72,7 @@ fun MediaVideoView(
|
||||||
bottomPaddingInPixels = bottomPaddingInPixels,
|
bottomPaddingInPixels = bottomPaddingInPixels,
|
||||||
exoPlayer = exoPlayer,
|
exoPlayer = exoPlayer,
|
||||||
localMedia = localMedia,
|
localMedia = localMedia,
|
||||||
|
autoplay = autoplay,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -82,6 +85,7 @@ private fun ExoPlayerMediaVideoView(
|
||||||
bottomPaddingInPixels: Int,
|
bottomPaddingInPixels: Int,
|
||||||
exoPlayer: ExoPlayer,
|
exoPlayer: ExoPlayer,
|
||||||
localMedia: LocalMedia?,
|
localMedia: LocalMedia?,
|
||||||
|
autoplay: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var mediaPlayerControllerState: MediaPlayerControllerState by remember {
|
var mediaPlayerControllerState: MediaPlayerControllerState by remember {
|
||||||
|
|
@ -89,6 +93,7 @@ private fun ExoPlayerMediaVideoView(
|
||||||
MediaPlayerControllerState(
|
MediaPlayerControllerState(
|
||||||
isVisible = true,
|
isVisible = true,
|
||||||
isPlaying = false,
|
isPlaying = false,
|
||||||
|
isReady = false,
|
||||||
progressInMillis = 0,
|
progressInMillis = 0,
|
||||||
durationInMillis = 0,
|
durationInMillis = 0,
|
||||||
canMute = true,
|
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
|
var needsAutoPlay by remember { mutableStateOf(autoplay) }
|
||||||
if (!isDisplayed) {
|
|
||||||
|
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()
|
exoPlayer.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -259,5 +278,6 @@ internal fun MediaVideoViewPreview() = ElementPreview {
|
||||||
bottomPaddingInPixels = 0,
|
bottomPaddingInPixels = 0,
|
||||||
localMediaViewState = rememberLocalMediaViewState(),
|
localMediaViewState = rememberLocalMediaViewState(),
|
||||||
localMedia = null,
|
localMedia = null,
|
||||||
|
autoplay = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ class MediaViewerDataSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
fun dataFlow(): Flow<PersistentList<MediaViewerPageData>> {
|
internal fun dataFlow(): Flow<PersistentList<MediaViewerPageData>> {
|
||||||
return galleryDataSource.groupedMediaItemsFlow()
|
return galleryDataSource.groupedMediaItemsFlow()
|
||||||
.map { groupedItems ->
|
.map { groupedItems ->
|
||||||
when (groupedItems) {
|
when (groupedItems) {
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,7 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
return MediaViewerState(
|
return MediaViewerState(
|
||||||
|
initiallySelectedEventId = inputs.eventId,
|
||||||
listData = data.value,
|
listData = data.value,
|
||||||
currentIndex = currentIndex.intValue,
|
currentIndex = currentIndex.intValue,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetSta
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class MediaViewerState(
|
data class MediaViewerState(
|
||||||
|
val initiallySelectedEventId: EventId?,
|
||||||
val listData: ImmutableList<MediaViewerPageData>,
|
val listData: ImmutableList<MediaViewerPageData>,
|
||||||
val currentIndex: Int,
|
val currentIndex: Int,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.media.aWaveForm
|
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.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||||
|
|
@ -202,6 +203,7 @@ fun aMediaViewerState(
|
||||||
mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
|
mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
|
||||||
eventSink: (MediaViewerEvents) -> Unit = {},
|
eventSink: (MediaViewerEvents) -> Unit = {},
|
||||||
) = MediaViewerState(
|
) = MediaViewerState(
|
||||||
|
initiallySelectedEventId = EventId("\$a:b"),
|
||||||
listData = listData.toPersistentList(),
|
listData = listData.toPersistentList(),
|
||||||
currentIndex = currentIndex,
|
currentIndex = currentIndex,
|
||||||
snackbarMessage = null,
|
snackbarMessage = null,
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,13 @@ fun MediaViewerView(
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize()
|
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(
|
MediaViewerPage(
|
||||||
isDisplayed = page == pagerState.settledPage,
|
isDisplayed = isDisplayed,
|
||||||
showOverlay = showOverlay,
|
showOverlay = showOverlay,
|
||||||
bottomPaddingInPixels = bottomPaddingInPixels,
|
bottomPaddingInPixels = bottomPaddingInPixels,
|
||||||
data = dataForPage,
|
data = dataForPage,
|
||||||
|
|
@ -157,7 +162,8 @@ fun MediaViewerView(
|
||||||
},
|
},
|
||||||
onShowOverlayChange = {
|
onShowOverlayChange = {
|
||||||
showOverlay = it
|
showOverlay = it
|
||||||
}
|
},
|
||||||
|
isUserSelected = (state.listData[page] as? MediaViewerPageData.MediaViewerData)?.eventId == state.initiallySelectedEventId,
|
||||||
)
|
)
|
||||||
// Bottom bar
|
// Bottom bar
|
||||||
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
|
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
|
||||||
|
|
@ -273,6 +279,7 @@ private fun MediaViewerPage(
|
||||||
bottomPaddingInPixels: Int,
|
bottomPaddingInPixels: Int,
|
||||||
data: MediaViewerPageData.MediaViewerData,
|
data: MediaViewerPageData.MediaViewerData,
|
||||||
textFileViewer: TextFileViewer,
|
textFileViewer: TextFileViewer,
|
||||||
|
isUserSelected: Boolean,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onDismissError: () -> Unit,
|
onDismissError: () -> Unit,
|
||||||
|
|
@ -328,6 +335,7 @@ private fun MediaViewerPage(
|
||||||
currentOnShowOverlayChange(!currentShowOverlay)
|
currentOnShowOverlayChange(!currentShowOverlay)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isUserSelected = isUserSelected,
|
||||||
)
|
)
|
||||||
ThumbnailView(
|
ThumbnailView(
|
||||||
mediaInfo = data.mediaInfo,
|
mediaInfo = data.mediaInfo,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue