Skip to content

Commit 2fd69ec

Browse files
CopilotRaival-e
andcommitted
Implement clean Material 3 video player with custom controls
Co-authored-by: Raival-e <[email protected]>
1 parent 6244551 commit 2fd69ec

File tree

4 files changed

+525
-34
lines changed

4 files changed

+525
-34
lines changed

app/src/main/java/com/raival/compose/file/explorer/screen/viewer/media/MediaViewerActivity.kt

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,7 @@ package com.raival.compose.file.explorer.screen.viewer.media
22

33
import android.net.Uri
44
import androidx.activity.compose.setContent
5-
import androidx.compose.foundation.layout.Box
6-
import androidx.compose.foundation.layout.fillMaxSize
75
import androidx.compose.runtime.LaunchedEffect
8-
import androidx.compose.ui.Alignment
9-
import androidx.compose.ui.Modifier
10-
import androidx.compose.ui.viewinterop.AndroidView
11-
import androidx.media3.common.Player
12-
import androidx.media3.ui.PlayerView
136
import com.raival.compose.file.explorer.App.Companion.globalClass
147
import com.raival.compose.file.explorer.R
158
import com.raival.compose.file.explorer.common.ui.KeepScreenOn
@@ -19,6 +12,7 @@ import com.raival.compose.file.explorer.screen.viewer.ViewerInstance
1912
import com.raival.compose.file.explorer.screen.viewer.media.instance.MediaViewerInstance
2013
import com.raival.compose.file.explorer.screen.viewer.media.misc.MediaSource
2114
import com.raival.compose.file.explorer.screen.viewer.media.ui.AudioPlayer
15+
import com.raival.compose.file.explorer.screen.viewer.media.ui.VideoPlayer
2216
import com.raival.compose.file.explorer.theme.FileExplorerTheme
2317

2418
class MediaViewerActivity : ViewerActivity() {
@@ -32,35 +26,22 @@ class MediaViewerActivity : ViewerActivity() {
3226
KeepScreenOn()
3327
FileExplorerTheme {
3428
SafeSurface {
35-
if (instance.mediaSource is MediaSource.AudioSource) {
36-
AudioPlayer(instance)
37-
} else {
38-
Box(
39-
Modifier
40-
.fillMaxSize(),
41-
contentAlignment = Alignment.Center
42-
) {
29+
when (instance.mediaSource) {
30+
is MediaSource.AudioSource -> {
31+
AudioPlayer(instance)
32+
}
33+
is MediaSource.VideoSource -> {
34+
VideoPlayer(
35+
instance = instance,
36+
onBackClick = { finish() }
37+
)
38+
}
39+
else -> {
40+
// Handle unknown media type - show error and finish
4341
LaunchedEffect(Unit) {
44-
instance.player.play()
42+
globalClass.showMsg(getString(R.string.invalid_media_file))
43+
finish()
4544
}
46-
47-
AndroidView(
48-
modifier = Modifier
49-
.fillMaxSize(),
50-
factory = { context ->
51-
PlayerView(context).apply {
52-
useController =
53-
instance.mediaSource is MediaSource.VideoSource
54-
player = instance.player.apply {
55-
repeatMode = Player.REPEAT_MODE_ONE
56-
}
57-
}
58-
},
59-
update = { },
60-
onRelease = {
61-
it.player?.release()
62-
}
63-
)
6445
}
6546
}
6647
}

app/src/main/java/com/raival/compose/file/explorer/screen/viewer/media/instance/MediaViewerInstance.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,24 @@ import com.raival.compose.file.explorer.App.Companion.globalClass
88
import com.raival.compose.file.explorer.screen.viewer.ViewerInstance
99
import com.raival.compose.file.explorer.screen.viewer.media.misc.AudioPlayerManager
1010
import com.raival.compose.file.explorer.screen.viewer.media.misc.MediaSource
11+
import com.raival.compose.file.explorer.screen.viewer.media.misc.VideoPlayerManager
1112

1213
class MediaViewerInstance(override val uri: Uri, override val id: String) : ViewerInstance {
1314
val player = ExoPlayer.Builder(globalClass).build()
1415
val mediaItem = MediaItem.fromUri(uri)
1516
val mediaSource = getMediaSource(globalClass, uri)
1617

1718
val audioManager = AudioPlayerManager(globalClass)
19+
val videoManager = VideoPlayerManager(globalClass)
1820

1921
init {
2022
player.setMediaItem(mediaItem)
2123
player.prepare()
2224

2325
if (mediaSource is MediaSource.AudioSource) {
2426
audioManager.prepare(uri)
27+
} else if (mediaSource is MediaSource.VideoSource) {
28+
videoManager.prepare(uri)
2529
}
2630
}
2731

@@ -47,5 +51,6 @@ class MediaViewerInstance(override val uri: Uri, override val id: String) : View
4751
override fun onClose() {
4852
player.release()
4953
audioManager.release()
54+
videoManager.release()
5055
}
5156
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.raival.compose.file.explorer.screen.viewer.media.misc
2+
3+
import android.content.Context
4+
import android.net.Uri
5+
import android.os.Handler
6+
import android.os.Looper
7+
import androidx.media3.common.MediaItem
8+
import androidx.media3.common.MediaMetadata
9+
import androidx.media3.common.Player
10+
import androidx.media3.exoplayer.ExoPlayer
11+
12+
/**
13+
* A video player manager using Media3 ExoPlayer.
14+
* Provides video playback controls and state management.
15+
*/
16+
class VideoPlayerManager(context: Context) {
17+
18+
private val exoPlayer: ExoPlayer = ExoPlayer.Builder(context).build()
19+
private var listener: VideoPlayerManagerListener? = null
20+
21+
// Handler to update playback progress every second.
22+
private val updateHandler = Handler(Looper.getMainLooper())
23+
private val updateRunnable = object : Runnable {
24+
override fun run() {
25+
val currentPosition = exoPlayer.currentPosition
26+
val duration = exoPlayer.duration.takeIf { it > 0 } ?: 0L
27+
val remaining = if (duration > currentPosition) duration - currentPosition else 0L
28+
listener?.onProgressUpdated(currentPosition, remaining)
29+
updateHandler.postDelayed(this, 1000)
30+
}
31+
}
32+
33+
/**
34+
* Set a listener to receive callbacks.
35+
*/
36+
fun setListener(listener: VideoPlayerManagerListener) {
37+
this.listener = listener
38+
}
39+
40+
/**
41+
* Prepares the player with the given video URI.
42+
*/
43+
fun prepare(uri: Uri) {
44+
val mediaItem = MediaItem.fromUri(uri)
45+
exoPlayer.setMediaItem(mediaItem)
46+
exoPlayer.repeatMode = Player.REPEAT_MODE_ONE
47+
exoPlayer.prepare()
48+
49+
// Listen for playback and metadata events.
50+
exoPlayer.addListener(object : Player.Listener {
51+
override fun onPlaybackStateChanged(state: Int) {
52+
val isPlaying = exoPlayer.playWhenReady && state == Player.STATE_READY
53+
val isLoading = state == Player.STATE_BUFFERING
54+
listener?.onPlaybackStateChanged(isPlaying, isLoading)
55+
}
56+
57+
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
58+
val title = mediaMetadata.title?.toString()
59+
val duration = exoPlayer.duration.takeIf { it > 0 } ?: 0L
60+
61+
val metadata = VideoMetadata(title, duration)
62+
listener?.onMetadataChanged(metadata)
63+
}
64+
})
65+
}
66+
67+
/**
68+
* Gets the ExoPlayer instance for video rendering.
69+
*/
70+
fun getPlayer(): ExoPlayer = exoPlayer
71+
72+
/**
73+
* Starts playback and begins periodic progress updates.
74+
*/
75+
fun play() {
76+
exoPlayer.playWhenReady = true
77+
updateHandler.post(updateRunnable)
78+
}
79+
80+
/**
81+
* Pauses playback and stops progress updates.
82+
*/
83+
fun pause() {
84+
exoPlayer.playWhenReady = false
85+
updateHandler.removeCallbacks(updateRunnable)
86+
}
87+
88+
/**
89+
* Stops playback completely.
90+
*/
91+
fun stop() {
92+
exoPlayer.stop()
93+
updateHandler.removeCallbacks(updateRunnable)
94+
}
95+
96+
/**
97+
* Fast-forwards playback by 10 seconds.
98+
*/
99+
fun forward() {
100+
val newPosition = exoPlayer.currentPosition + 10000L
101+
exoPlayer.seekTo(newPosition)
102+
}
103+
104+
/**
105+
* Rewinds playback by 10 seconds.
106+
*/
107+
fun backward() {
108+
val newPosition = (exoPlayer.currentPosition - 10000L).coerceAtLeast(0L)
109+
exoPlayer.seekTo(newPosition)
110+
}
111+
112+
/**
113+
* Seek to a new position
114+
*/
115+
fun seekTo(position: Long) {
116+
val newPosition = position.coerceIn(0, exoPlayer.duration)
117+
exoPlayer.seekTo(newPosition)
118+
}
119+
120+
/**
121+
* Releases player resources.
122+
*/
123+
fun release() {
124+
updateHandler.removeCallbacks(updateRunnable)
125+
exoPlayer.release()
126+
}
127+
128+
/**
129+
* Callback interface for playback state, progress, and metadata updates.
130+
*/
131+
interface VideoPlayerManagerListener {
132+
/**
133+
* Called when playback state changes.
134+
* @param isPlaying true if the video is playing.
135+
* @param isLoading true if the video is buffering/loading.
136+
*/
137+
fun onPlaybackStateChanged(isPlaying: Boolean, isLoading: Boolean)
138+
139+
/**
140+
* Called periodically with the current position and remaining time (in milliseconds).
141+
*/
142+
fun onProgressUpdated(currentPosition: Long, remainingTime: Long)
143+
144+
/**
145+
* Called when metadata for the current media item is available.
146+
*/
147+
fun onMetadataChanged(metadata: VideoMetadata)
148+
}
149+
150+
/**
151+
* Data class to hold basic video metadata.
152+
*/
153+
data class VideoMetadata(
154+
val title: String?,
155+
val duration: Long
156+
)
157+
}

0 commit comments

Comments
 (0)