From d52d29621e0f5df9cba16795d40db8a413248342 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 29 Aug 2024 08:28:41 -0700 Subject: [PATCH] [Video] Add `timeRemainingChange` event to `player` in `expo-video` (#5013) --- patches/expo-video+1.2.4.patch | 269 +++++++++++++++++- .../VideoEmbedInner/VideoEmbedInnerNative.tsx | 86 +++--- 2 files changed, 296 insertions(+), 59 deletions(-) diff --git a/patches/expo-video+1.2.4.patch b/patches/expo-video+1.2.4.patch index 918c8a8d..a159f7e5 100644 --- a/patches/expo-video+1.2.4.patch +++ b/patches/expo-video+1.2.4.patch @@ -1,3 +1,27 @@ +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 +index 473f964..f37aff9 100644 +--- 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 +@@ -41,6 +41,11 @@ sealed class PlayerEvent { + override val name = "playToEnd" + } + ++ data class PlayerTimeRemainingChanged(val timeRemaining: Double): PlayerEvent() { ++ override val name = "timeRemainingChange" ++ override val arguments = arrayOf(timeRemaining) ++ } ++ + fun emit(player: VideoPlayer, listeners: List) { + when (this) { + is StatusChanged -> listeners.forEach { it.onStatusChanged(player, status, oldStatus, error) } +@@ -49,6 +54,7 @@ sealed class PlayerEvent { + is SourceChanged -> listeners.forEach { it.onSourceChanged(player, source, oldSource) } + is PlaybackRateChanged -> listeners.forEach { it.onPlaybackRateChanged(player, rate, oldRate) } + is PlayedToEnd -> listeners.forEach { it.onPlayedToEnd(player) } ++ is PlayerTimeRemainingChanged -> listeners.forEach { it.onPlayerTimeRemainingChanged(player, timeRemaining) } + } + } + } 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 index 9905e13..47342ff 100644 --- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt @@ -8,10 +32,10 @@ index 9905e13..47342ff 100644 setTimeBarInteractive(requireLinearPlayback) + setShowSubtitleButton(true) } - + @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) { - + @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) { - val fullscreenButton = findViewById(androidx.media3.ui.R.id.exo_fullscreen) @@ -20,6 +44,42 @@ index 9905e13..47342ff 100644 fullscreenButton?.visibility = if (visible) { android.view.View.VISIBLE } else { +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 +new file mode 100644 +index 0000000..0249e23 +--- /dev/null ++++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/ProgressTracker.kt +@@ -0,0 +1,29 @@ ++import android.os.Handler ++import android.os.Looper ++import androidx.annotation.OptIn ++import androidx.media3.common.util.UnstableApi ++import expo.modules.video.PlayerEvent ++import expo.modules.video.VideoPlayer ++import kotlin.math.floor ++ ++@OptIn(UnstableApi::class) ++class ProgressTracker(private val videoPlayer: VideoPlayer) : Runnable { ++ private val handler: Handler = Handler(Looper.getMainLooper()) ++ private val player = videoPlayer.player ++ ++ init { ++ handler.post(this) ++ } ++ ++ override fun run() { ++ val currentPosition = player.currentPosition ++ val duration = player.duration ++ val timeRemaining = floor(((duration - currentPosition) / 1000).toDouble()) ++ videoPlayer.sendEvent(PlayerEvent.PlayerTimeRemainingChanged(timeRemaining)) ++ handler.postDelayed(this, 1000 /* ms */) ++ } ++ ++ fun remove() { ++ handler.removeCallbacks(this) ++ } ++} +\ No newline at end of file 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 index ec3da2a..5a1397a 100644 --- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt @@ -33,8 +93,76 @@ index ec3da2a..5a1397a 100644 + "onEnterFullscreen", + "onExitFullscreen" ) - + Prop("player") { view: VideoView, player: VideoPlayer -> +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 +index 58f00af..5ad8237 100644 +--- 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 +@@ -1,5 +1,6 @@ + package expo.modules.video + ++import ProgressTracker + import android.content.Context + import android.view.SurfaceView + import androidx.media3.common.MediaItem +@@ -35,11 +36,13 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou + .Builder(context, renderersFactory) + .setLooper(context.mainLooper) + .build() ++ var progressTracker: ProgressTracker? = null + + val serviceConnection = PlaybackServiceConnection(WeakReference(player)) + + var playing by IgnoreSameSet(false) { new, old -> + sendEvent(PlayerEvent.IsPlayingChanged(new, old)) ++ addOrRemoveProgressTracker() + } + + var uncommittedSource: VideoSource? = source +@@ -141,6 +144,9 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou + } + + override fun close() { ++ this.progressTracker?.remove() ++ this.progressTracker = null ++ + appContext?.reactContext?.unbindService(serviceConnection) + serviceConnection.playbackServiceBinder?.service?.unregisterPlayer(player) + VideoManager.unregisterVideoPlayer(this@VideoPlayer) +@@ -228,7 +234,7 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou + listeners.removeAll { it.get() == videoPlayerListener } + } + +- private fun sendEvent(event: PlayerEvent) { ++ fun sendEvent(event: PlayerEvent) { + // Emits to the native listeners + event.emit(this, listeners.mapNotNull { it.get() }) + // Emits to the JS side +@@ -240,4 +246,13 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou + sendEvent(eventName, *args) + } + } ++ ++ private fun addOrRemoveProgressTracker() { ++ this.progressTracker?.remove() ++ if (this.playing) { ++ this.progressTracker = ProgressTracker(this) ++ } else { ++ this.progressTracker = null ++ } ++ } + } +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 +index f654254..dcfe3f0 100644 +--- 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 +@@ -15,4 +15,5 @@ interface VideoPlayerListener { + fun onSourceChanged(player: VideoPlayer, source: VideoSource?, oldSource: VideoSource?) {} + fun onPlaybackRateChanged(player: VideoPlayer, rate: Float, oldRate: Float?) {} + fun onPlayedToEnd(player: VideoPlayer) {} ++ fun onPlayerTimeRemainingChanged(player: VideoPlayer, timeRemaining: Double) {} + } 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 index a951d80..3932535 100644 --- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt @@ -45,7 +173,7 @@ index a951d80..3932535 100644 val onPictureInPictureStop by EventDispatcher() + val onEnterFullscreen by EventDispatcher() + val onExitFullscreen by EventDispatcher() - + var willEnterPiP: Boolean = false var isInFullscreen: Boolean = false @@ -154,6 +156,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap @@ -55,7 +183,7 @@ index a951d80..3932535 100644 + onEnterFullscreen(mapOf()) isInFullscreen = true } - + @@ -162,6 +165,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen) fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter) @@ -63,9 +191,22 @@ index a951d80..3932535 100644 + this.onExitFullscreen(mapOf()) isInFullscreen = false } - + +diff --git a/node_modules/expo-video/build/VideoPlayer.types.d.ts b/node_modules/expo-video/build/VideoPlayer.types.d.ts +index a09fcfe..65fe29a 100644 +--- a/node_modules/expo-video/build/VideoPlayer.types.d.ts ++++ b/node_modules/expo-video/build/VideoPlayer.types.d.ts +@@ -128,6 +128,8 @@ export type VideoPlayerEvents = { + * Handler for an event emitted when the current media source of the player changes. + */ + sourceChange(newSource: VideoSource, previousSource: VideoSource): void; ++ ++ timeRemainingChange(timeRemaining: number): void; + }; + /** + * Describes the current status of the player. diff --git a/node_modules/expo-video/build/VideoView.types.d.ts b/node_modules/expo-video/build/VideoView.types.d.ts -index cb9ca6d..60e9f4e 100644 +index cb9ca6d..ed8bb7e 100644 --- a/node_modules/expo-video/build/VideoView.types.d.ts +++ b/node_modules/expo-video/build/VideoView.types.d.ts @@ -89,5 +89,8 @@ export interface VideoViewProps extends ViewProps { @@ -77,6 +218,7 @@ index cb9ca6d..60e9f4e 100644 + onExitFullscreen?: () => void; } //# sourceMappingURL=VideoView.types.d.ts.map +\ No newline at end of file diff --git a/node_modules/expo-video/ios/VideoModule.swift b/node_modules/expo-video/ios/VideoModule.swift index c537a12..e4a918f 100644 --- a/node_modules/expo-video/ios/VideoModule.swift @@ -90,19 +232,111 @@ index c537a12..e4a918f 100644 + "onEnterFullscreen", + "onExitFullscreen" ) - + Prop("player") { (view, player: VideoPlayer?) in +diff --git a/node_modules/expo-video/ios/VideoPlayer.swift b/node_modules/expo-video/ios/VideoPlayer.swift +index 3315b88..f482390 100644 +--- a/node_modules/expo-video/ios/VideoPlayer.swift ++++ b/node_modules/expo-video/ios/VideoPlayer.swift +@@ -185,6 +185,10 @@ internal final class VideoPlayer: SharedRef, Hashable, VideoPlayerObse + safeEmit(event: "sourceChange", arguments: newVideoPlayerItem?.videoSource, oldVideoPlayerItem?.videoSource) + } + ++ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) { ++ safeEmit(event: "timeRemainingChange", arguments: timeRemaining) ++ } ++ + func safeEmit(event: String, arguments: repeat each A) { + if self.appContext != nil { + self.emit(event: event, arguments: repeat each arguments) +diff --git a/node_modules/expo-video/ios/VideoPlayerObserver.swift b/node_modules/expo-video/ios/VideoPlayerObserver.swift +index d289e26..d0fdd30 100644 +--- a/node_modules/expo-video/ios/VideoPlayerObserver.swift ++++ b/node_modules/expo-video/ios/VideoPlayerObserver.swift +@@ -21,6 +21,7 @@ protocol VideoPlayerObserverDelegate: AnyObject { + func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?) + func onIsMutedChanged(player: AVPlayer, oldIsMuted: Bool?, newIsMuted: Bool) + func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status) ++ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) + } + + // Default implementations for the delegate +@@ -33,6 +34,7 @@ extension VideoPlayerObserverDelegate { + func onItemChanged(player: AVPlayer, oldVideoPlayerItem: VideoPlayerItem?, newVideoPlayerItem: VideoPlayerItem?) {} + func onIsMutedChanged(player: AVPlayer, oldIsMuted: Bool?, newIsMuted: Bool) {} + func onPlayerItemStatusChanged(player: AVPlayer, oldStatus: AVPlayerItem.Status?, newStatus: AVPlayerItem.Status) {} ++ func onPlayerTimeRemainingChanged(player: AVPlayer, timeRemaining: Double) {} + } + + // Wrapper used to store WeakReferences to the observer delegate +@@ -91,6 +93,7 @@ class VideoPlayerObserver { + private var playerVolumeObserver: NSKeyValueObservation? + private var playerCurrentItemObserver: NSKeyValueObservation? + private var playerIsMutedObserver: NSKeyValueObservation? ++ private var playerPeriodicTimeObserver: Any? + + // Current player item observers + private var playbackBufferEmptyObserver: NSKeyValueObservation? +@@ -152,6 +155,9 @@ class VideoPlayerObserver { + playerVolumeObserver?.invalidate() + playerIsMutedObserver?.invalidate() + playerCurrentItemObserver?.invalidate() ++ if let playerPeriodicTimeObserver = self.playerPeriodicTimeObserver { ++ player?.removeTimeObserver(playerPeriodicTimeObserver) ++ } + } + + private func initializeCurrentPlayerItemObservers(player: AVPlayer, playerItem: AVPlayerItem) { +@@ -270,6 +276,7 @@ class VideoPlayerObserver { + + if isPlaying != (player.timeControlStatus == .playing) { + isPlaying = player.timeControlStatus == .playing ++ addOrRemovePeriodicTimeObserver() + } + } + +@@ -310,4 +317,30 @@ class VideoPlayerObserver { + } + } + } ++ ++ private func onPlayerTimeRemainingChanged(_ player: AVPlayer, _ timeRemaining: Double) { ++ delegates.forEach { delegate in ++ delegate.value?.onPlayerTimeRemainingChanged(player: player, timeRemaining: timeRemaining) ++ } ++ } ++ ++ private func addOrRemovePeriodicTimeObserver() { ++ guard let player = self.player else { ++ return ++ } ++ ++ if isPlaying { ++ // Add the time update listener ++ playerPeriodicTimeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1.0, preferredTimescale: Int32(NSEC_PER_SEC)), queue: nil) { event in ++ guard let duration = player.currentItem?.duration else { ++ return ++ } ++ ++ let timeRemaining = (duration.seconds - event.seconds).rounded() ++ self.onPlayerTimeRemainingChanged(player, timeRemaining) ++ } ++ } else if let playerPeriodicTimeObserver = self.playerPeriodicTimeObserver { ++ player.removeTimeObserver(playerPeriodicTimeObserver) ++ } ++ } + } diff --git a/node_modules/expo-video/ios/VideoView.swift b/node_modules/expo-video/ios/VideoView.swift index f4579e4..10c5908 100644 --- a/node_modules/expo-video/ios/VideoView.swift +++ b/node_modules/expo-video/ios/VideoView.swift @@ -41,6 +41,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate { - + let onPictureInPictureStart = EventDispatcher() let onPictureInPictureStop = EventDispatcher() + let onEnterFullscreen = EventDispatcher() + let onExitFullscreen = EventDispatcher() - + public override var bounds: CGRect { didSet { @@ -163,6 +165,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate { @@ -112,7 +346,7 @@ index f4579e4..10c5908 100644 + onEnterFullscreen() isFullscreen = true } - + @@ -179,6 +182,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate { if wasPlaying { self.player?.pointer.play() @@ -121,6 +355,19 @@ index f4579e4..10c5908 100644 self.isFullscreen = false } } +diff --git a/node_modules/expo-video/src/VideoPlayer.types.ts b/node_modules/expo-video/src/VideoPlayer.types.ts +index aaf4b63..f438196 100644 +--- a/node_modules/expo-video/src/VideoPlayer.types.ts ++++ b/node_modules/expo-video/src/VideoPlayer.types.ts +@@ -151,6 +151,8 @@ export type VideoPlayerEvents = { + * Handler for an event emitted when the current media source of the player changes. + */ + sourceChange(newSource: VideoSource, previousSource: VideoSource): void; ++ ++ timeRemainingChange(timeRemaining: number): void; + }; + + /** diff --git a/node_modules/expo-video/src/VideoView.types.ts b/node_modules/expo-video/src/VideoView.types.ts index 29fe5db..e1fbf59 100644 --- a/node_modules/expo-video/src/VideoView.types.ts diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx index f08fe0bf..f5ee139e 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -102,29 +102,22 @@ function VideoControls({ const {_} = useLingui() const t = useTheme() const [isMuted, setIsMuted] = useState(player.muted) - const [duration, setDuration] = useState(() => Math.floor(player.duration)) - const [currentTime, setCurrentTime] = useState(() => - Math.floor(player.currentTime), - ) + const [timeRemaining, setTimeRemaining] = React.useState(0) useEffect(() => { - const interval = setInterval(() => { - // duration gets reset to 0 on loop - if (player.duration) setDuration(Math.floor(player.duration)) - setCurrentTime(Math.floor(player.currentTime)) - - // how often should we update the time? - // 1000 gets out of sync with the video time - }, 250) - // eslint-disable-next-line @typescript-eslint/no-shadow - const sub = player.addListener('volumeChange', ({isMuted}) => { + const volumeSub = player.addListener('volumeChange', ({isMuted}) => { setIsMuted(isMuted) }) - + const timeSub = player.addListener( + 'timeRemainingChange', + secondsRemaining => { + setTimeRemaining(secondsRemaining) + }, + ) return () => { - clearInterval(interval) - sub.remove() + volumeSub.remove() + timeSub.remove() } }, [player]) @@ -160,8 +153,7 @@ function VideoControls({ // 1. timeRemaining is a number - was seeing NaNs // 2. duration is greater than 0 - means metadata has loaded // 3. we're less than 5 second into the video - const timeRemaining = duration - currentTime - const showTime = !isNaN(timeRemaining) && duration > 0 && currentTime <= 5 + const showTime = !isNaN(timeRemaining) return ( @@ -173,35 +165,33 @@ function VideoControls({ accessibilityHint={_(msg`Tap to enter full screen`)} accessibilityRole="button" /> - {duration > 0 && ( - - - {isMuted ? ( - - ) : ( - - )} - - - )} + + + {isMuted ? ( + + ) : ( + + )} + + ) }