Skip to content

Commit d52d296

Browse files
authored
[Video] Add timeRemainingChange event to player in expo-video (#5013)
1 parent d92731b commit d52d296

File tree

2 files changed

+296
-59
lines changed

2 files changed

+296
-59
lines changed

patches/expo-video+1.2.4.patch

Lines changed: 258 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerEvent.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerEvent.kt
2+
index 473f964..f37aff9 100644
3+
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerEvent.kt
4+
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerEvent.kt
5+
@@ -41,6 +41,11 @@ sealed class PlayerEvent {
6+
override val name = "playToEnd"
7+
}
8+
9+
+ data class PlayerTimeRemainingChanged(val timeRemaining: Double): PlayerEvent() {
10+
+ override val name = "timeRemainingChange"
11+
+ override val arguments = arrayOf(timeRemaining)
12+
+ }
13+
+
14+
fun emit(player: VideoPlayer, listeners: List<VideoPlayerListener>) {
15+
when (this) {
16+
is StatusChanged -> listeners.forEach { it.onStatusChanged(player, status, oldStatus, error) }
17+
@@ -49,6 +54,7 @@ sealed class PlayerEvent {
18+
is SourceChanged -> listeners.forEach { it.onSourceChanged(player, source, oldSource) }
19+
is PlaybackRateChanged -> listeners.forEach { it.onPlaybackRateChanged(player, rate, oldRate) }
20+
is PlayedToEnd -> listeners.forEach { it.onPlayedToEnd(player) }
21+
+ is PlayerTimeRemainingChanged -> listeners.forEach { it.onPlayerTimeRemainingChanged(player, timeRemaining) }
22+
}
23+
}
24+
}
125
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
226
index 9905e13..47342ff 100644
327
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
@@ -8,10 +32,10 @@ index 9905e13..47342ff 100644
832
setTimeBarInteractive(requireLinearPlayback)
933
+ setShowSubtitleButton(true)
1034
}
11-
35+
1236
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
1337
@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
14-
38+
1539
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
1640
internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
1741
- val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
@@ -20,6 +44,42 @@ index 9905e13..47342ff 100644
2044
fullscreenButton?.visibility = if (visible) {
2145
android.view.View.VISIBLE
2246
} else {
47+
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/ProgressTracker.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/ProgressTracker.kt
48+
new file mode 100644
49+
index 0000000..0249e23
50+
--- /dev/null
51+
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/ProgressTracker.kt
52+
@@ -0,0 +1,29 @@
53+
+import android.os.Handler
54+
+import android.os.Looper
55+
+import androidx.annotation.OptIn
56+
+import androidx.media3.common.util.UnstableApi
57+
+import expo.modules.video.PlayerEvent
58+
+import expo.modules.video.VideoPlayer
59+
+import kotlin.math.floor
60+
+
61+
+@OptIn(UnstableApi::class)
62+
+class ProgressTracker(private val videoPlayer: VideoPlayer) : Runnable {
63+
+ private val handler: Handler = Handler(Looper.getMainLooper())
64+
+ private val player = videoPlayer.player
65+
+
66+
+ init {
67+
+ handler.post(this)
68+
+ }
69+
+
70+
+ override fun run() {
71+
+ val currentPosition = player.currentPosition
72+
+ val duration = player.duration
73+
+ val timeRemaining = floor(((duration - currentPosition) / 1000).toDouble())
74+
+ videoPlayer.sendEvent(PlayerEvent.PlayerTimeRemainingChanged(timeRemaining))
75+
+ handler.postDelayed(this, 1000 /* ms */)
76+
+ }
77+
+
78+
+ fun remove() {
79+
+ handler.removeCallbacks(this)
80+
+ }
81+
+}
82+
\ No newline at end of file
2383
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt
2484
index ec3da2a..5a1397a 100644
2585
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt
@@ -33,8 +93,76 @@ index ec3da2a..5a1397a 100644
3393
+ "onEnterFullscreen",
3494
+ "onExitFullscreen"
3595
)
36-
96+
3797
Prop("player") { view: VideoView, player: VideoPlayer ->
98+
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoPlayer.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoPlayer.kt
99+
index 58f00af..5ad8237 100644
100+
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoPlayer.kt
101+
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoPlayer.kt
102+
@@ -1,5 +1,6 @@
103+
package expo.modules.video
104+
105+
+import ProgressTracker
106+
import android.content.Context
107+
import android.view.SurfaceView
108+
import androidx.media3.common.MediaItem
109+
@@ -35,11 +36,13 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
110+
.Builder(context, renderersFactory)
111+
.setLooper(context.mainLooper)
112+
.build()
113+
+ var progressTracker: ProgressTracker? = null
114+
115+
val serviceConnection = PlaybackServiceConnection(WeakReference(player))
116+
117+
var playing by IgnoreSameSet(false) { new, old ->
118+
sendEvent(PlayerEvent.IsPlayingChanged(new, old))
119+
+ addOrRemoveProgressTracker()
120+
}
121+
122+
var uncommittedSource: VideoSource? = source
123+
@@ -141,6 +144,9 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
124+
}
125+
126+
override fun close() {
127+
+ this.progressTracker?.remove()
128+
+ this.progressTracker = null
129+
+
130+
appContext?.reactContext?.unbindService(serviceConnection)
131+
serviceConnection.playbackServiceBinder?.service?.unregisterPlayer(player)
132+
VideoManager.unregisterVideoPlayer(this@VideoPlayer)
133+
@@ -228,7 +234,7 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
134+
listeners.removeAll { it.get() == videoPlayerListener }
135+
}
136+
137+
- private fun sendEvent(event: PlayerEvent) {
138+
+ fun sendEvent(event: PlayerEvent) {
139+
// Emits to the native listeners
140+
event.emit(this, listeners.mapNotNull { it.get() })
141+
// Emits to the JS side
142+
@@ -240,4 +246,13 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
143+
sendEvent(eventName, *args)
144+
}
145+
}
146+
+
147+
+ private fun addOrRemoveProgressTracker() {
148+
+ this.progressTracker?.remove()
149+
+ if (this.playing) {
150+
+ this.progressTracker = ProgressTracker(this)
151+
+ } else {
152+
+ this.progressTracker = null
153+
+ }
154+
+ }
155+
}
156+
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoPlayerListener.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoPlayerListener.kt
157+
index f654254..dcfe3f0 100644
158+
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoPlayerListener.kt
159+
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoPlayerListener.kt
160+
@@ -15,4 +15,5 @@ interface VideoPlayerListener {
161+
fun onSourceChanged(player: VideoPlayer, source: VideoSource?, oldSource: VideoSource?) {}
162+
fun onPlaybackRateChanged(player: VideoPlayer, rate: Float, oldRate: Float?) {}
163+
fun onPlayedToEnd(player: VideoPlayer) {}
164+
+ fun onPlayerTimeRemainingChanged(player: VideoPlayer, timeRemaining: Double) {}
165+
}
38166
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt
39167
index a951d80..3932535 100644
40168
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt
@@ -45,7 +173,7 @@ index a951d80..3932535 100644
45173
val onPictureInPictureStop by EventDispatcher<Unit>()
46174
+ val onEnterFullscreen by EventDispatcher()
47175
+ val onExitFullscreen by EventDispatcher()
48-
176+
49177
var willEnterPiP: Boolean = false
50178
var isInFullscreen: Boolean = false
51179
@@ -154,6 +156,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
@@ -55,17 +183,30 @@ index a951d80..3932535 100644
55183
+ onEnterFullscreen(mapOf())
56184
isInFullscreen = true
57185
}
58-
186+
59187
@@ -162,6 +165,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
60188
val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
61189
fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter)
62190
videoPlayer?.changePlayerView(playerView)
63191
+ this.onExitFullscreen(mapOf())
64192
isInFullscreen = false
65193
}
66-
194+
195+
diff --git a/node_modules/expo-video/build/VideoPlayer.types.d.ts b/node_modules/expo-video/build/VideoPlayer.types.d.ts
196+
index a09fcfe..65fe29a 100644
197+
--- a/node_modules/expo-video/build/VideoPlayer.types.d.ts
198+
+++ b/node_modules/expo-video/build/VideoPlayer.types.d.ts
199+
@@ -128,6 +128,8 @@ export type VideoPlayerEvents = {
200+
* Handler for an event emitted when the current media source of the player changes.
201+
*/
202+
sourceChange(newSource: VideoSource, previousSource: VideoSource): void;
203+
+
204+
+ timeRemainingChange(timeRemaining: number): void;
205+
};
206+
/**
207+
* Describes the current status of the player.
67208
diff --git a/node_modules/expo-video/build/VideoView.types.d.ts b/node_modules/expo-video/build/VideoView.types.d.ts
68-
index cb9ca6d..60e9f4e 100644
209+
index cb9ca6d..ed8bb7e 100644
69210
--- a/node_modules/expo-video/build/VideoView.types.d.ts
70211
+++ b/node_modules/expo-video/build/VideoView.types.d.ts
71212
@@ -89,5 +89,8 @@ export interface VideoViewProps extends ViewProps {
@@ -77,6 +218,7 @@ index cb9ca6d..60e9f4e 100644
77218
+ onExitFullscreen?: () => void;
78219
}
79220
//# sourceMappingURL=VideoView.types.d.ts.map
221+
\ No newline at end of file
80222
diff --git a/node_modules/expo-video/ios/VideoModule.swift b/node_modules/expo-video/ios/VideoModule.swift
81223
index c537a12..e4a918f 100644
82224
--- a/node_modules/expo-video/ios/VideoModule.swift
@@ -90,19 +232,111 @@ index c537a12..e4a918f 100644
90232
+ "onEnterFullscreen",
91233
+ "onExitFullscreen"
92234
)
93-
235+
94236
Prop("player") { (view, player: VideoPlayer?) in
237+
diff --git a/node_modules/expo-video/ios/VideoPlayer.swift b/node_modules/expo-video/ios/VideoPlayer.swift
238+
index 3315b88..f482390 100644
239+
--- a/node_modules/expo-video/ios/VideoPlayer.swift
240+
+++ b/node_modules/expo-video/ios/VideoPlayer.swift
241+
@@ -185,6 +185,10 @@ internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObse
242+
safeEmit(event: "sourceChange", arguments: newVideoPlayerItem?.videoSource, oldVideoPlayerItem?.videoSource)
243+
}
244+
245+
+ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) {
246+
+ safeEmit(event: "timeRemainingChange", arguments: timeRemaining)
247+
+ }
248+
+
249+
func safeEmit<each A: AnyArgument>(event: String, arguments: repeat each A) {
250+
if self.appContext != nil {
251+
self.emit(event: event, arguments: repeat each arguments)
252+
diff --git a/node_modules/expo-video/ios/VideoPlayerObserver.swift b/node_modules/expo-video/ios/VideoPlayerObserver.swift
253+
index d289e26..d0fdd30 100644
254+
--- a/node_modules/expo-video/ios/VideoPlayerObserver.swift
255+
+++ b/node_modules/expo-video/ios/VideoPlayerObserver.swift
256+
@@ -21,6 +21,7 @@ protocol VideoPlayerObserverDelegate: AnyObject {
257+
func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?)
258+
func onIsMutedChanged(player: AVPlayer, oldIsMuted: Bool?, newIsMuted: Bool)
259+
func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status)
260+
+ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double)
261+
}
262+
263+
// Default implementations for the delegate
264+
@@ -33,6 +34,7 @@ extension VideoPlayerObserverDelegate {
265+
func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?) {}
266+
func onIsMutedChanged(player: AVPlayer, oldIsMuted: Bool?, newIsMuted: Bool) {}
267+
func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status) {}
268+
+ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) {}
269+
}
270+
271+
// Wrapper used to store WeakReferences to the observer delegate
272+
@@ -91,6 +93,7 @@ class VideoPlayerObserver {
273+
private var playerVolumeObserver: NSKeyValueObservation?
274+
private var playerCurrentItemObserver: NSKeyValueObservation?
275+
private var playerIsMutedObserver: NSKeyValueObservation?
276+
+ private var playerPeriodicTimeObserver: Any?
277+
278+
// Current player item observers
279+
private var playbackBufferEmptyObserver: NSKeyValueObservation?
280+
@@ -152,6 +155,9 @@ class VideoPlayerObserver {
281+
playerVolumeObserver?.invalidate()
282+
playerIsMutedObserver?.invalidate()
283+
playerCurrentItemObserver?.invalidate()
284+
+ if let playerPeriodicTimeObserver = self.playerPeriodicTimeObserver {
285+
+ player?.removeTimeObserver(playerPeriodicTimeObserver)
286+
+ }
287+
}
288+
289+
private func initializeCurrentPlayerItemObservers(player: AVPlayer, playerItem: AVPlayerItem) {
290+
@@ -270,6 +276,7 @@ class VideoPlayerObserver {
291+
292+
if isPlaying != (player.timeControlStatus == .playing) {
293+
isPlaying = player.timeControlStatus == .playing
294+
+ addOrRemovePeriodicTimeObserver()
295+
}
296+
}
297+
298+
@@ -310,4 +317,30 @@ class VideoPlayerObserver {
299+
}
300+
}
301+
}
302+
+
303+
+ private func onPlayerTimeRemainingChanged(_ player: AVPlayer, _ timeRemaining: Double) {
304+
+ delegates.forEach { delegate in
305+
+ delegate.value?.onPlayerTimeRemainingChanged(player: player, timeRemaining: timeRemaining)
306+
+ }
307+
+ }
308+
+
309+
+ private func addOrRemovePeriodicTimeObserver() {
310+
+ guard let player = self.player else {
311+
+ return
312+
+ }
313+
+
314+
+ if isPlaying {
315+
+ // Add the time update listener
316+
+ playerPeriodicTimeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1.0, preferredTimescale: Int32(NSEC_PER_SEC)), queue: nil) { event in
317+
+ guard let duration = player.currentItem?.duration else {
318+
+ return
319+
+ }
320+
+
321+
+ let timeRemaining = (duration.seconds - event.seconds).rounded()
322+
+ self.onPlayerTimeRemainingChanged(player, timeRemaining)
323+
+ }
324+
+ } else if let playerPeriodicTimeObserver = self.playerPeriodicTimeObserver {
325+
+ player.removeTimeObserver(playerPeriodicTimeObserver)
326+
+ }
327+
+ }
328+
}
95329
diff --git a/node_modules/expo-video/ios/VideoView.swift b/node_modules/expo-video/ios/VideoView.swift
96330
index f4579e4..10c5908 100644
97331
--- a/node_modules/expo-video/ios/VideoView.swift
98332
+++ b/node_modules/expo-video/ios/VideoView.swift
99333
@@ -41,6 +41,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
100-
334+
101335
let onPictureInPictureStart = EventDispatcher()
102336
let onPictureInPictureStop = EventDispatcher()
103337
+ let onEnterFullscreen = EventDispatcher()
104338
+ let onExitFullscreen = EventDispatcher()
105-
339+
106340
public override var bounds: CGRect {
107341
didSet {
108342
@@ -163,6 +165,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
@@ -112,7 +346,7 @@ index f4579e4..10c5908 100644
112346
+ onEnterFullscreen()
113347
isFullscreen = true
114348
}
115-
349+
116350
@@ -179,6 +182,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
117351
if wasPlaying {
118352
self.player?.pointer.play()
@@ -121,6 +355,19 @@ index f4579e4..10c5908 100644
121355
self.isFullscreen = false
122356
}
123357
}
358+
diff --git a/node_modules/expo-video/src/VideoPlayer.types.ts b/node_modules/expo-video/src/VideoPlayer.types.ts
359+
index aaf4b63..f438196 100644
360+
--- a/node_modules/expo-video/src/VideoPlayer.types.ts
361+
+++ b/node_modules/expo-video/src/VideoPlayer.types.ts
362+
@@ -151,6 +151,8 @@ export type VideoPlayerEvents = {
363+
* Handler for an event emitted when the current media source of the player changes.
364+
*/
365+
sourceChange(newSource: VideoSource, previousSource: VideoSource): void;
366+
+
367+
+ timeRemainingChange(timeRemaining: number): void;
368+
};
369+
370+
/**
124371
diff --git a/node_modules/expo-video/src/VideoView.types.ts b/node_modules/expo-video/src/VideoView.types.ts
125372
index 29fe5db..e1fbf59 100644
126373
--- a/node_modules/expo-video/src/VideoView.types.ts

0 commit comments

Comments
 (0)