Skip to content

Commit c5ac47a

Browse files
committed
refactor video playback components and unify under InlineVideoPlayer
- introduce `InlineVideoPlayer` as a shared component for efficient inline video and GIF playback - refactor `VideoNoteBubble` to use `InlineVideoPlayer`, replacing manual `ExoPlayer` management and reducing boilerplate logic - simplify `AvatarPlayer` by delegating playback and thumbnail loading to `InlineVideoPlayer` - extend `VideoStickerPlayer` and `AlphaVideoPlayer` with additional callbacks for `onFirstFrameRendered` and `onPlaybackError` - improve state handling in `VideoNoteBubble` by using a `canRenderInlineVideo` check to toggle between player and thumbnail states - ensure consistent video scaling and progress reporting across chat bubble and avatar components
1 parent 5912cb1 commit c5ac47a

4 files changed

Lines changed: 83 additions & 207 deletions

File tree

presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AlphaVideoPlayer.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ fun VideoStickerPlayer(
110110
onProgressUpdate: (Long) -> Unit = {},
111111
onDurationKnown: (Long) -> Unit = {},
112112
onPlaybackEnded: () -> Unit = {},
113+
onFirstFrameRendered: () -> Unit = {},
114+
onPlaybackError: (PlaybackException) -> Unit = {},
113115
reportProgress: Boolean = false,
114116
fileId: Int = 0,
115117
thumbnailData: Any? = null
@@ -262,6 +264,7 @@ fun VideoStickerPlayer(
262264
override fun onRenderedFirstFrame() {
263265
if (!isDisposed.get()) {
264266
isVideoFrameReady = true
267+
onFirstFrameRendered()
265268
if (isGif) {
266269
Log.d(
267270
GIF_PLAYER_TAG,
@@ -275,6 +278,7 @@ fun VideoStickerPlayer(
275278
if (error.cause is FileNotFoundException) {
276279
isVideoFrameReady = false
277280
}
281+
onPlaybackError(error)
278282
if (isGif) {
279283
Log.e(
280284
GIF_PLAYER_TAG,
Lines changed: 8 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,19 @@
11
package org.monogram.presentation.features.chats.currentChat.components
22

3-
import android.net.Uri
4-
import android.view.ViewGroup
5-
import android.widget.FrameLayout
6-
import androidx.annotation.OptIn
7-
import androidx.compose.animation.AnimatedVisibility
8-
import androidx.compose.animation.core.tween
9-
import androidx.compose.animation.fadeIn
10-
import androidx.compose.animation.fadeOut
11-
import androidx.compose.foundation.layout.Box
12-
import androidx.compose.foundation.layout.fillMaxSize
13-
import androidx.compose.runtime.*
3+
import androidx.compose.runtime.Composable
144
import androidx.compose.ui.Modifier
155
import androidx.compose.ui.layout.ContentScale
16-
import androidx.compose.ui.platform.LocalContext
17-
import androidx.compose.ui.platform.LocalInspectionMode
18-
import androidx.compose.ui.viewinterop.AndroidView
19-
import androidx.core.net.toUri
20-
import androidx.media3.common.MediaItem
21-
import androidx.media3.common.PlaybackException
22-
import androidx.media3.common.Player
23-
import androidx.media3.common.util.UnstableApi
24-
import androidx.media3.ui.AspectRatioFrameLayout
25-
import androidx.media3.ui.PlayerView
26-
import coil3.compose.AsyncImage
27-
import coil3.request.ImageRequest
28-
import coil3.request.crossfade
29-
import coil3.video.VideoFrameDecoder
30-
import coil3.video.videoFrameMillis
31-
import org.monogram.presentation.core.util.LocalVideoPlayerPool
32-
import java.io.File
336

34-
@OptIn(UnstableApi::class)
357
@Composable
368
fun AvatarPlayer(
379
path: String,
3810
modifier: Modifier = Modifier,
3911
contentScale: ContentScale = ContentScale.Crop
4012
) {
41-
if (LocalInspectionMode.current) {
42-
Box(modifier = modifier)
43-
return
44-
}
45-
val videoPlayerPool = LocalVideoPlayerPool.current
46-
47-
val context = LocalContext.current
48-
var isVideoFrameReady by remember { mutableStateOf(false) }
49-
50-
val exoPlayer = remember(path) {
51-
videoPlayerPool.acquire().apply {
52-
val uri = if (path.startsWith("http") || path.startsWith("content") || path.startsWith("file")) {
53-
path.toUri()
54-
} else {
55-
Uri.fromFile(File(path))
56-
}
57-
val mediaSource = videoPlayerPool.getMediaSourceFactory()
58-
.createMediaSource(MediaItem.fromUri(uri))
59-
setMediaSource(mediaSource)
60-
prepare()
61-
repeatMode = Player.REPEAT_MODE_ONE
62-
playWhenReady = true
63-
volume = 0f
64-
}
65-
}
66-
67-
DisposableEffect(exoPlayer) {
68-
val listener = object : Player.Listener {
69-
override fun onRenderedFirstFrame() {
70-
isVideoFrameReady = true
71-
}
72-
73-
override fun onPlayerError(error: PlaybackException) {
74-
isVideoFrameReady = false
75-
}
76-
}
77-
exoPlayer.addListener(listener)
78-
onDispose {
79-
exoPlayer.removeListener(listener)
80-
exoPlayer.setVideoSurface(null)
81-
videoPlayerPool.release(exoPlayer)
82-
}
83-
}
84-
85-
Box(modifier = modifier) {
86-
AndroidView(
87-
factory = { ctx ->
88-
PlayerView(ctx).apply {
89-
player = exoPlayer
90-
useController = false
91-
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
92-
layoutParams = FrameLayout.LayoutParams(
93-
ViewGroup.LayoutParams.MATCH_PARENT,
94-
ViewGroup.LayoutParams.MATCH_PARENT
95-
)
96-
}
97-
},
98-
modifier = Modifier.fillMaxSize()
99-
)
100-
101-
AnimatedVisibility(
102-
visible = !isVideoFrameReady,
103-
enter = fadeIn(tween(100)),
104-
exit = fadeOut(tween(250)),
105-
modifier = Modifier.fillMaxSize()
106-
) {
107-
AsyncImage(
108-
model = ImageRequest.Builder(context)
109-
.data(path)
110-
.decoderFactory(VideoFrameDecoder.Factory())
111-
.videoFrameMillis(0)
112-
.memoryCacheKey(path)
113-
.diskCacheKey(path)
114-
.crossfade(true)
115-
.build(),
116-
contentDescription = null,
117-
contentScale = contentScale,
118-
modifier = Modifier.fillMaxSize()
119-
)
120-
}
121-
}
122-
}
13+
InlineVideoPlayer(
14+
path = path,
15+
modifier = modifier,
16+
contentScale = contentScale,
17+
placeholderData = path
18+
)
19+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.monogram.presentation.features.chats.currentChat.components
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.ui.Modifier
5+
import androidx.compose.ui.layout.ContentScale
6+
import androidx.media3.common.PlaybackException
7+
8+
@Composable
9+
fun InlineVideoPlayer(
10+
path: String,
11+
modifier: Modifier = Modifier,
12+
contentScale: ContentScale = ContentScale.Crop,
13+
animate: Boolean = true,
14+
volume: Float = 0f,
15+
placeholderData: Any? = null,
16+
fileId: Int = 0,
17+
reportProgress: Boolean = false,
18+
onProgressUpdate: (Long) -> Unit = {},
19+
onDurationKnown: (Long) -> Unit = {},
20+
onPlaybackEnded: () -> Unit = {},
21+
onFirstFrameRendered: () -> Unit = {},
22+
onPlaybackError: (PlaybackException) -> Unit = {}
23+
) {
24+
VideoStickerPlayer(
25+
path = path,
26+
type = VideoType.Gif,
27+
modifier = modifier,
28+
animate = animate,
29+
shouldLoop = true,
30+
volume = volume,
31+
contentScale = contentScale,
32+
onProgressUpdate = onProgressUpdate,
33+
onDurationKnown = onDurationKnown,
34+
onPlaybackEnded = onPlaybackEnded,
35+
onFirstFrameRendered = onFirstFrameRendered,
36+
onPlaybackError = onPlaybackError,
37+
reportProgress = reportProgress,
38+
fileId = fileId,
39+
thumbnailData = placeholderData
40+
)
41+
}

0 commit comments

Comments
 (0)