Add some previews.

This commit is contained in:
Benoit Marty 2024-11-29 11:23:02 +01:00
parent 6e624cc198
commit 6e5d7f8f51
5 changed files with 367 additions and 40 deletions

View file

@ -29,16 +29,20 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
@Composable
fun MediaFileView(
@ -101,3 +105,16 @@ fun MediaFileView(
}
}
}
@PreviewsDayNight
@Composable
internal fun MediaFileViewPreview(
@PreviewParameter(MediaInfoFileProvider::class) info: MediaInfo
) = ElementPreview {
MediaFileView(
localMediaViewState = rememberLocalMediaViewState(),
uri = null,
info = info,
onClick = {},
)
}

View file

@ -0,0 +1,21 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.mediaviewer.impl.local.file
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
open class MediaInfoFileProvider : PreviewParameterProvider<MediaInfo> {
override val values: Sequence<MediaInfo>
get() = sequenceOf(
aPdfMediaInfo(),
anAudioMediaInfo(),
)
}

View file

@ -14,9 +14,12 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
import io.element.android.libraries.ui.strings.CommonStrings
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
import me.saket.telephoto.zoomable.rememberZoomableImageState
@ -47,3 +50,13 @@ fun MediaImageView(
)
}
}
@PreviewsDayNight
@Composable
internal fun MediaImageViewPreview() = ElementPreview {
MediaImageView(
localMediaViewState = rememberLocalMediaViewState(),
localMedia = null,
onClick = {},
)
}

View file

@ -0,0 +1,252 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
@file:Suppress(
"OVERRIDE_DEPRECATION",
"RedundantNullableReturnType",
"DEPRECATION",
)
package io.element.android.libraries.mediaviewer.impl.local.video
import android.annotation.SuppressLint
import android.media.AudioDeviceInfo
import android.os.Looper
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.TextureView
import androidx.media3.common.AudioAttributes
import androidx.media3.common.AuxEffectInfo
import androidx.media3.common.DeviceInfo
import androidx.media3.common.Effect
import androidx.media3.common.Format
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
import androidx.media3.common.PriorityTaskManager
import androidx.media3.common.Timeline
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.Tracks
import androidx.media3.common.VideoSize
import androidx.media3.common.text.CueGroup
import androidx.media3.common.util.Clock
import androidx.media3.common.util.Size
import androidx.media3.exoplayer.DecoderCounters
import androidx.media3.exoplayer.ExoPlaybackException
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.PlayerMessage
import androidx.media3.exoplayer.Renderer
import androidx.media3.exoplayer.SeekParameters
import androidx.media3.exoplayer.analytics.AnalyticsCollector
import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.exoplayer.image.ImageOutput
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ShuffleOrder
import androidx.media3.exoplayer.source.TrackGroupArray
import androidx.media3.exoplayer.trackselection.TrackSelectionArray
import androidx.media3.exoplayer.trackselection.TrackSelector
import androidx.media3.exoplayer.video.VideoFrameMetadataListener
import androidx.media3.exoplayer.video.spherical.CameraMotionListener
@SuppressLint("UnsafeOptInUsageError")
class ExoPlayerForPreview(
private val isPlaying: Boolean = false,
) : ExoPlayer {
override fun getApplicationLooper(): Looper = throw NotImplementedError()
override fun addListener(listener: Player.Listener) {}
override fun removeListener(listener: Player.Listener) {}
override fun setMediaItems(mediaItems: MutableList<MediaItem>) {}
override fun setMediaItems(mediaItems: MutableList<MediaItem>, resetPosition: Boolean) {}
override fun setMediaItems(mediaItems: MutableList<MediaItem>, startIndex: Int, startPositionMs: Long) {}
override fun setMediaItem(mediaItem: MediaItem) {}
override fun setMediaItem(mediaItem: MediaItem, startPositionMs: Long) {}
override fun setMediaItem(mediaItem: MediaItem, resetPosition: Boolean) {}
override fun addMediaItem(mediaItem: MediaItem) {}
override fun addMediaItem(index: Int, mediaItem: MediaItem) {}
override fun addMediaItems(mediaItems: MutableList<MediaItem>) {}
override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) {}
override fun moveMediaItem(currentIndex: Int, newIndex: Int) {}
override fun moveMediaItems(fromIndex: Int, toIndex: Int, newIndex: Int) {}
override fun replaceMediaItem(index: Int, mediaItem: MediaItem) {}
override fun replaceMediaItems(fromIndex: Int, toIndex: Int, mediaItems: MutableList<MediaItem>) {}
override fun removeMediaItem(index: Int) {}
override fun removeMediaItems(fromIndex: Int, toIndex: Int) {}
override fun clearMediaItems() {}
override fun isCommandAvailable(command: Int): Boolean = throw NotImplementedError()
override fun canAdvertiseSession(): Boolean = throw NotImplementedError()
override fun getAvailableCommands(): Player.Commands = throw NotImplementedError()
override fun prepare(mediaSource: MediaSource) {}
override fun prepare(mediaSource: MediaSource, resetPosition: Boolean, resetState: Boolean) {}
override fun prepare() {}
override fun getPlaybackState(): Int = throw NotImplementedError()
override fun getPlaybackSuppressionReason(): Int = throw NotImplementedError()
override fun isPlaying() = isPlaying
override fun getPlayerError(): ExoPlaybackException? = null
override fun play() {}
override fun pause() {}
override fun setPlayWhenReady(playWhenReady: Boolean) {}
override fun getPlayWhenReady(): Boolean = throw NotImplementedError()
override fun setRepeatMode(repeatMode: Int) {}
override fun getRepeatMode(): Int = throw NotImplementedError()
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
override fun getShuffleModeEnabled(): Boolean = throw NotImplementedError()
override fun isLoading(): Boolean = throw NotImplementedError()
override fun seekToDefaultPosition() {}
override fun seekToDefaultPosition(mediaItemIndex: Int) {}
override fun seekTo(positionMs: Long) {}
override fun seekTo(mediaItemIndex: Int, positionMs: Long) {}
override fun getSeekBackIncrement(): Long = throw NotImplementedError()
override fun seekBack() {}
override fun getSeekForwardIncrement(): Long = throw NotImplementedError()
override fun seekForward() {}
override fun hasPreviousMediaItem(): Boolean = throw NotImplementedError()
override fun seekToPreviousWindow() {}
override fun seekToPreviousMediaItem() {}
override fun getMaxSeekToPreviousPosition(): Long = throw NotImplementedError()
override fun seekToPrevious() {}
override fun hasNext(): Boolean = throw NotImplementedError()
override fun hasNextWindow(): Boolean = throw NotImplementedError()
override fun hasNextMediaItem(): Boolean = throw NotImplementedError()
override fun next() {}
override fun seekToNextWindow() {}
override fun seekToNextMediaItem() {}
override fun seekToNext() {}
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {}
override fun setPlaybackSpeed(speed: Float) {}
override fun getPlaybackParameters(): PlaybackParameters = throw NotImplementedError()
override fun stop() {}
override fun release() {}
override fun getCurrentTracks(): Tracks = throw NotImplementedError()
override fun getTrackSelectionParameters(): TrackSelectionParameters = throw NotImplementedError()
override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) {}
override fun getMediaMetadata(): MediaMetadata = throw NotImplementedError()
override fun getPlaylistMetadata(): MediaMetadata = throw NotImplementedError()
override fun setPlaylistMetadata(mediaMetadata: MediaMetadata) {}
override fun getCurrentManifest(): Any? = throw NotImplementedError()
override fun getCurrentTimeline(): Timeline = throw NotImplementedError()
override fun getCurrentPeriodIndex(): Int = throw NotImplementedError()
override fun getCurrentWindowIndex(): Int = throw NotImplementedError()
override fun getCurrentMediaItemIndex(): Int = throw NotImplementedError()
override fun getNextWindowIndex(): Int = throw NotImplementedError()
override fun getNextMediaItemIndex(): Int = throw NotImplementedError()
override fun getPreviousWindowIndex(): Int = throw NotImplementedError()
override fun getPreviousMediaItemIndex(): Int = throw NotImplementedError()
override fun getCurrentMediaItem(): MediaItem? = throw NotImplementedError()
override fun getMediaItemCount(): Int = throw NotImplementedError()
override fun getMediaItemAt(index: Int): MediaItem = throw NotImplementedError()
override fun getDuration(): Long = throw NotImplementedError()
override fun getCurrentPosition(): Long = throw NotImplementedError()
override fun getBufferedPosition(): Long = throw NotImplementedError()
override fun getBufferedPercentage(): Int = throw NotImplementedError()
override fun getTotalBufferedDuration(): Long = throw NotImplementedError()
override fun isCurrentWindowDynamic(): Boolean = throw NotImplementedError()
override fun isCurrentMediaItemDynamic(): Boolean = throw NotImplementedError()
override fun isCurrentWindowLive(): Boolean = throw NotImplementedError()
override fun isCurrentMediaItemLive(): Boolean = throw NotImplementedError()
override fun getCurrentLiveOffset(): Long = throw NotImplementedError()
override fun isCurrentWindowSeekable(): Boolean = throw NotImplementedError()
override fun isCurrentMediaItemSeekable(): Boolean = throw NotImplementedError()
override fun isPlayingAd(): Boolean = throw NotImplementedError()
override fun getCurrentAdGroupIndex(): Int = throw NotImplementedError()
override fun getCurrentAdIndexInAdGroup(): Int = throw NotImplementedError()
override fun getContentDuration(): Long = throw NotImplementedError()
override fun getContentPosition(): Long = throw NotImplementedError()
override fun getContentBufferedPosition(): Long = throw NotImplementedError()
override fun getAudioAttributes(): AudioAttributes = throw NotImplementedError()
override fun setVolume(volume: Float) = throw NotImplementedError()
override fun getVolume(): Float = throw NotImplementedError()
override fun clearVideoSurface() {}
override fun clearVideoSurface(surface: Surface?) {}
override fun setVideoSurface(surface: Surface?) {}
override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {}
override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {}
override fun setVideoSurfaceView(surfaceView: SurfaceView?) {}
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {}
override fun setVideoTextureView(textureView: TextureView?) {}
override fun clearVideoTextureView(textureView: TextureView?) {}
override fun getVideoSize(): VideoSize = throw NotImplementedError()
override fun getSurfaceSize(): Size = throw NotImplementedError()
override fun getCurrentCues(): CueGroup = throw NotImplementedError()
override fun getDeviceInfo(): DeviceInfo = throw NotImplementedError()
override fun getDeviceVolume(): Int = throw NotImplementedError()
override fun isDeviceMuted(): Boolean = throw NotImplementedError()
override fun setDeviceVolume(volume: Int) {}
override fun setDeviceVolume(volume: Int, flags: Int) {}
override fun increaseDeviceVolume() {}
override fun increaseDeviceVolume(flags: Int) {}
override fun decreaseDeviceVolume() {}
override fun decreaseDeviceVolume(flags: Int) {}
override fun setDeviceMuted(muted: Boolean) {}
override fun setDeviceMuted(muted: Boolean, flags: Int) {}
override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) {}
override fun getAudioComponent(): ExoPlayer.AudioComponent? = throw NotImplementedError()
override fun getVideoComponent(): ExoPlayer.VideoComponent? = throw NotImplementedError()
override fun getTextComponent(): ExoPlayer.TextComponent? = throw NotImplementedError()
override fun getDeviceComponent(): ExoPlayer.DeviceComponent? = throw NotImplementedError()
override fun addAudioOffloadListener(listener: ExoPlayer.AudioOffloadListener) {}
override fun removeAudioOffloadListener(listener: ExoPlayer.AudioOffloadListener) {}
override fun getAnalyticsCollector(): AnalyticsCollector = throw NotImplementedError()
override fun addAnalyticsListener(listener: AnalyticsListener) {}
override fun removeAnalyticsListener(listener: AnalyticsListener) {}
override fun getRendererCount(): Int = throw NotImplementedError()
override fun getRendererType(index: Int): Int = throw NotImplementedError()
override fun getRenderer(index: Int): Renderer = throw NotImplementedError()
override fun getTrackSelector(): TrackSelector? = throw NotImplementedError()
override fun getCurrentTrackGroups(): TrackGroupArray = throw NotImplementedError()
override fun getCurrentTrackSelections(): TrackSelectionArray = throw NotImplementedError()
override fun getPlaybackLooper(): Looper = throw NotImplementedError()
override fun getClock(): Clock = throw NotImplementedError()
override fun setMediaSources(mediaSources: MutableList<MediaSource>) {}
override fun setMediaSources(mediaSources: MutableList<MediaSource>, resetPosition: Boolean) {}
override fun setMediaSources(mediaSources: MutableList<MediaSource>, startMediaItemIndex: Int, startPositionMs: Long) {}
override fun setMediaSource(mediaSource: MediaSource) {}
override fun setMediaSource(mediaSource: MediaSource, startPositionMs: Long) {}
override fun setMediaSource(mediaSource: MediaSource, resetPosition: Boolean) {}
override fun addMediaSource(mediaSource: MediaSource) {}
override fun addMediaSource(index: Int, mediaSource: MediaSource) {}
override fun addMediaSources(mediaSources: MutableList<MediaSource>) {}
override fun addMediaSources(index: Int, mediaSources: MutableList<MediaSource>) {}
override fun setShuffleOrder(shuffleOrder: ShuffleOrder) {}
override fun setPreloadConfiguration(preloadConfiguration: ExoPlayer.PreloadConfiguration) {}
override fun getPreloadConfiguration(): ExoPlayer.PreloadConfiguration = throw NotImplementedError()
override fun setAudioSessionId(audioSessionId: Int) {}
override fun getAudioSessionId(): Int = throw NotImplementedError()
override fun setAuxEffectInfo(auxEffectInfo: AuxEffectInfo) {}
override fun clearAuxEffectInfo() {}
override fun setPreferredAudioDevice(audioDeviceInfo: AudioDeviceInfo?) {}
override fun setSkipSilenceEnabled(skipSilenceEnabled: Boolean) {}
override fun getSkipSilenceEnabled(): Boolean = throw NotImplementedError()
override fun setVideoEffects(videoEffects: MutableList<Effect>) {}
override fun setVideoScalingMode(videoScalingMode: Int) {}
override fun getVideoScalingMode(): Int = throw NotImplementedError()
override fun setVideoChangeFrameRateStrategy(videoChangeFrameRateStrategy: Int) {}
override fun getVideoChangeFrameRateStrategy(): Int = throw NotImplementedError()
override fun setVideoFrameMetadataListener(listener: VideoFrameMetadataListener) {}
override fun clearVideoFrameMetadataListener(listener: VideoFrameMetadataListener) {}
override fun setCameraMotionListener(listener: CameraMotionListener) {}
override fun clearCameraMotionListener(listener: CameraMotionListener) {}
override fun createMessage(target: PlayerMessage.Target): PlayerMessage = throw NotImplementedError()
override fun setSeekParameters(seekParameters: SeekParameters?) {}
override fun getSeekParameters(): SeekParameters = throw NotImplementedError()
override fun setForegroundMode(foregroundMode: Boolean) {}
override fun setPauseAtEndOfMediaItems(pauseAtEndOfMediaItems: Boolean) {}
override fun getPauseAtEndOfMediaItems(): Boolean = throw NotImplementedError()
override fun getAudioFormat(): Format? = throw NotImplementedError()
override fun getVideoFormat(): Format? = throw NotImplementedError()
override fun getAudioDecoderCounters(): DecoderCounters? = throw NotImplementedError()
override fun getVideoDecoderCounters(): DecoderCounters? = throw NotImplementedError()
override fun setHandleAudioBecomingNoisy(handleAudioBecomingNoisy: Boolean) {}
override fun setWakeMode(wakeMode: Int) {}
override fun setPriority(priority: Int) {}
override fun setPriorityTaskManager(priorityTaskManager: PriorityTaskManager?) {}
override fun isSleepingForOffload(): Boolean = throw NotImplementedError()
override fun isTunnelingEnabled(): Boolean = throw NotImplementedError()
override fun isReleased(): Boolean = throw NotImplementedError()
override fun setImageOutput(imageOutput: ImageOutput?) {}
}

View file

@ -32,51 +32,60 @@ import androidx.lifecycle.Lifecycle
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.KeepScreenOn
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState
import io.element.android.libraries.mediaviewer.impl.local.PlayableState
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
@SuppressLint("UnsafeOptInUsageError")
@Composable
fun MediaVideoView(
localMediaViewState: LocalMediaViewState,
localMedia: LocalMedia?,
modifier: Modifier = Modifier,
) {
if (LocalInspectionMode.current) {
Text(
modifier = modifier
.background(ElementTheme.colors.bgSubtlePrimary)
.wrapContentSize(),
text = "A Video Player will render here",
)
val exoPlayer = if (LocalInspectionMode.current) {
remember {
ExoPlayerForPreview()
}
} else {
ExoPlayerMediaVideoView(
localMediaViewState = localMediaViewState,
localMedia = localMedia,
modifier = modifier,
)
val context = LocalContext.current
remember {
ExoPlayerWrapper.create(context)
}
}
ExoPlayerMediaVideoView(
localMediaViewState = localMediaViewState,
exoPlayer = exoPlayer,
localMedia = localMedia,
modifier = modifier,
)
}
@SuppressLint("UnsafeOptInUsageError")
@Composable
private fun ExoPlayerMediaVideoView(
localMediaViewState: LocalMediaViewState,
exoPlayer: ExoPlayer,
localMedia: LocalMedia?,
modifier: Modifier = Modifier,
) {
val isControllerVisibleByDefault = LocalInspectionMode.current
var mediaPlayerControllerState: MediaPlayerControllerState by remember {
mutableStateOf(
MediaPlayerControllerState(
isVisible = false,
isVisible = isControllerVisibleByDefault,
isPlaying = false,
progressInMillis = 0,
durationInMillis = 0,
@ -95,10 +104,6 @@ private fun ExoPlayerMediaVideoView(
localMediaViewState.playableState = playableState
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayerWrapper.create(context)
}
val playerListener = object : Player.Listener {
override fun onRenderedFirstFrame() {
localMediaViewState.isReady = true
@ -167,31 +172,40 @@ private fun ExoPlayerMediaVideoView(
KeepScreenOn(mediaPlayerControllerState.isPlaying)
Box(
modifier = modifier
.background(ElementTheme.colors.bgSubtlePrimary)
.wrapContentSize(),
.background(ElementTheme.colors.bgSubtlePrimary),
) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = {
PlayerView(context).apply {
player = exoPlayer
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
setOnClickListener {
autoHideController++
mediaPlayerControllerState = mediaPlayerControllerState.copy(
isVisible = !mediaPlayerControllerState.isVisible,
)
val context = LocalContext.current
if (LocalInspectionMode.current) {
Text(
modifier = Modifier
.background(ElementTheme.colors.bgSubtlePrimary)
.wrapContentSize(),
text = "A Video Player will render here",
)
} else {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = {
PlayerView(context).apply {
player = exoPlayer
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
setOnClickListener {
autoHideController++
mediaPlayerControllerState = mediaPlayerControllerState.copy(
isVisible = !mediaPlayerControllerState.isVisible,
)
}
useController = false
}
useController = false
}
},
onRelease = { playerView ->
playerView.setOnClickListener(null)
playerView.setControllerVisibilityListener(null as PlayerView.ControllerVisibilityListener?)
playerView.player = null
},
)
},
onRelease = { playerView ->
playerView.setOnClickListener(null)
playerView.setControllerVisibilityListener(null as PlayerView.ControllerVisibilityListener?)
playerView.player = null
},
)
}
MediaPlayerControllerView(
state = mediaPlayerControllerState,
onTogglePlay = {
@ -235,3 +249,13 @@ private fun ExoPlayerMediaVideoView(
}
}
}
@PreviewsDayNight
@Composable
internal fun MediaVideoViewPreview() = ElementPreview {
MediaVideoView(
modifier = Modifier.fillMaxSize(),
localMediaViewState = rememberLocalMediaViewState(),
localMedia = null,
)
}