From d69729dc044a7f8477f7a5df43adfffb8454352c Mon Sep 17 00:00:00 2001 From: Kat Huang Date: Tue, 6 Aug 2024 00:11:17 -0600 Subject: [PATCH 1/7] expose-on-seek-complete (#1) Reviewed-on: https://dev.railbird.ai/railbird/react-native-video/pulls/1 --- ios/Video/RCTVideo.swift | 31 ++++++++++++++++++------------- ios/Video/RCTVideoManager.m | 1 + src/Video.tsx | 10 ++++++++++ src/specs/VideoNativeComponent.ts | 7 +++++++ src/types/events.ts | 2 ++ 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 325a06f1..4d16679a 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -114,6 +114,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc var onVideoProgress: RCTDirectEventBlock? @objc var onVideoBandwidthUpdate: RCTDirectEventBlock? @objc var onVideoSeek: RCTDirectEventBlock? + @objc var onVideoSeekComplete: RCTDirectEventBlock? @objc var onVideoEnd: RCTDirectEventBlock? @objc var onTimedMetadata: RCTDirectEventBlock? @objc var onVideoAudioBecomingNoisy: RCTDirectEventBlock? @@ -764,28 +765,32 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc func setSeek(_ time: NSNumber, _ tolerance: NSNumber) { let item: AVPlayerItem? = _player?.currentItem - - _pendingSeek = true - guard item != nil, let player = _player, let item, item.status == AVPlayerItem.Status.readyToPlay else { + _pendingSeek = true _pendingSeekTime = time.floatValue return } + let wasPaused = _paused - RCTPlayerOperations.seek( - player: player, - playerItem: item, - paused: _paused, - seekTime: time.floatValue, - seekTolerance: tolerance.floatValue - ) { [weak self] (_: Bool) in - guard let self else { return } + let seekTime = CMTimeMakeWithSeconds(Float64(time.floatValue), preferredTimescale: Int32(NSEC_PER_SEC)) + let toleranceTime = CMTimeMakeWithSeconds(Float64(tolerance.floatValue), preferredTimescale: Int32(NSEC_PER_SEC)) + + player.seek(to: seekTime, toleranceBefore: toleranceTime, toleranceAfter: toleranceTime) { [weak self] (finished) in + guard let self = self, finished else { return } self._playerObserver.addTimeObserverIfNotSet() - self.setPaused(self._paused) - self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))), + if !wasPaused { + self.setPaused(false) + } + + let currentTime = NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))) + self.onVideoSeek?(["currentTime": currentTime, "seekTime": time, "target": self.reactTag]) + + self.onVideoSeekComplete?(["currentTime": currentTime, + "seekTime": time, + "target": self.reactTag]) } _pendingSeek = false diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 3807f955..49a54933 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -46,6 +46,7 @@ RCT_EXPORT_VIEW_PROPERTY(onVideoError, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoProgress, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoBandwidthUpdate, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoSeek, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoSeekComplete, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoEnd, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onTimedMetadata, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoAudioBecomingNoisy, RCTDirectEventBlock); diff --git a/src/Video.tsx b/src/Video.tsx index 98b96b64..3a798741 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -30,6 +30,7 @@ import type { OnPlaybackStateChangedData, OnProgressData, OnSeekData, + OnSeekCompleteData, OnTextTrackDataChangedData, OnTimedMetadataData, OnVideoAspectRatioData, @@ -90,6 +91,7 @@ const Video = forwardRef( onError, onProgress, onSeek, + onSeekComplete, onEnd, onBuffer, onBandwidthUpdate, @@ -385,6 +387,13 @@ const Video = forwardRef( [onSeek], ); + const onVideoSeekComplete = useCallback( + (e: NativeSyntheticEvent) => { + onSeekComplete?.(e.nativeEvent); + }, + [onSeekComplete] + ); + const onVideoPlaybackStateChanged = useCallback( (e: NativeSyntheticEvent) => { onPlaybackStateChanged?.(e.nativeEvent); @@ -716,6 +725,7 @@ const Video = forwardRef( onVideoError={onError ? onVideoError : undefined} onVideoProgress={onProgress ? onVideoProgress : undefined} onVideoSeek={onSeek ? onVideoSeek : undefined} + onVideoSeekComplete={onSeekComplete ? onVideoSeekComplete : undefined} onVideoEnd={onEnd} onVideoBuffer={onBuffer ? onVideoBuffer : undefined} onVideoPlaybackStateChanged={ diff --git a/src/specs/VideoNativeComponent.ts b/src/specs/VideoNativeComponent.ts index 546bf97d..72813c6c 100644 --- a/src/specs/VideoNativeComponent.ts +++ b/src/specs/VideoNativeComponent.ts @@ -182,6 +182,12 @@ export type OnSeekData = Readonly<{ seekTime: Float; }>; +export type OnSeekCompleteData = Readonly<{ + currentTime: number; + seekTime: number; + target: number; +}>; + export type OnPlaybackStateChangedData = Readonly<{ isPlaying: boolean; isSeeking: boolean; @@ -349,6 +355,7 @@ export interface VideoNativeProps extends ViewProps { onVideoProgress?: DirectEventHandler; onVideoBandwidthUpdate?: DirectEventHandler; onVideoSeek?: DirectEventHandler; + onVideoSeekComplete?: DirectEventHandler; onVideoEnd?: DirectEventHandler<{}>; // all onVideoAudioBecomingNoisy?: DirectEventHandler<{}>; onVideoFullscreenPlayerWillPresent?: DirectEventHandler<{}>; // ios, android diff --git a/src/types/events.ts b/src/types/events.ts index 6d3e0d9e..fd4f2b6d 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -12,6 +12,7 @@ import type { OnPlaybackStateChangedData, OnProgressData, OnSeekData, + OnSeekCompleteData, OnTextTrackDataChangedData, OnTimedMetadataData, OnVideoAspectRatioData, @@ -258,6 +259,7 @@ export interface ReactVideoEvents { onReceiveAdEvent?: (e: OnReceiveAdEventData) => void; //Android, iOS onRestoreUserInterfaceForPictureInPictureStop?: () => void; //iOS onSeek?: (e: OnSeekData) => void; //Android, iOS, Windows UWP + onSeekComplete?: (e: OnSeekCompleteData) => void; // iOS onPlaybackStateChanged?: (e: OnPlaybackStateChangedData) => void; // Android, iOS onTimedMetadata?: (e: OnTimedMetadataData) => void; //Android, iOS onAudioTracks?: (e: OnAudioTracksData) => void; // Android From f3deabd75ecbd0b9b19277eee222dd47035fd145 Mon Sep 17 00:00:00 2001 From: Kat Huang Date: Tue, 20 Aug 2024 22:24:00 -0600 Subject: [PATCH 2/7] Implement ios onSeekComplete --- ios/Video/RCTVideo.swift | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 4d16679a..5740cba1 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -762,6 +762,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _paused = paused } + @objc func setSeek(_ time: NSNumber, _ tolerance: NSNumber) { let item: AVPlayerItem? = _player?.currentItem @@ -770,30 +771,44 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _pendingSeekTime = time.floatValue return } - let wasPaused = _paused + let wasPaused = _paused let seekTime = CMTimeMakeWithSeconds(Float64(time.floatValue), preferredTimescale: Int32(NSEC_PER_SEC)) let toleranceTime = CMTimeMakeWithSeconds(Float64(tolerance.floatValue), preferredTimescale: Int32(NSEC_PER_SEC)) - player.seek(to: seekTime, toleranceBefore: toleranceTime, toleranceAfter: toleranceTime) { [weak self] (finished) in - guard let self = self, finished else { return } + let currentTimeBeforeSeek = CMTimeGetSeconds(item.currentTime()) + + // Call onVideoSeek before starting the seek operation + let currentTime = NSNumber(value: Float(currentTimeBeforeSeek)) + self.onVideoSeek?(["currentTime": currentTime, + "seekTime": time, + "target": self.reactTag]) + + _pendingSeek = true + + let seekCompletionHandler: (Bool) -> Void = { [weak self] finished in + guard let self = self else { return } + + self._pendingSeek = false + + guard finished else { + return + } self._playerObserver.addTimeObserverIfNotSet() if !wasPaused { self.setPaused(false) } - let currentTime = NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))) - self.onVideoSeek?(["currentTime": currentTime, - "seekTime": time, - "target": self.reactTag]) + let currentTimeAfterSeek = CMTimeGetSeconds(item.currentTime()) - self.onVideoSeekComplete?(["currentTime": currentTime, - "seekTime": time, - "target": self.reactTag]) + let newCurrentTime = NSNumber(value: Float(currentTimeAfterSeek)) + self.onVideoSeekComplete?(["currentTime": newCurrentTime, + "seekTime": time, + "target": self.reactTag]) } - _pendingSeek = false + player.seek(to: seekTime, toleranceBefore: toleranceTime, toleranceAfter: toleranceTime, completionHandler: seekCompletionHandler) } @objc @@ -1661,3 +1676,4 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc func setOnClick(_: Any) {} } + From 01b3322e03a6191ee5c1761e766dd79bc81a0594 Mon Sep 17 00:00:00 2001 From: Kat Huang Date: Sat, 17 Aug 2024 17:19:10 -0600 Subject: [PATCH 3/7] Log in seek --- .../brentvatne/common/react/VideoEventEmitter.kt | 7 +++++++ .../brentvatne/exoplayer/ReactExoplayerView.java | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt index b227bfc1..fa6dd107 100644 --- a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt +++ b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt @@ -22,6 +22,7 @@ enum class EventTypes(val eventName: String) { EVENT_BANDWIDTH("onVideoBandwidthUpdate"), EVENT_CONTROLS_VISIBILITY_CHANGE("onControlsVisibilityChange"), EVENT_SEEK("onVideoSeek"), + EVENT_SEEK_COMPLETE("onVideoSeekComplete"), EVENT_END("onVideoEnd"), EVENT_FULLSCREEN_WILL_PRESENT("onVideoFullscreenPlayerWillPresent"), EVENT_FULLSCREEN_DID_PRESENT("onVideoFullscreenPlayerDidPresent"), @@ -71,6 +72,7 @@ class VideoEventEmitter { lateinit var onVideoBandwidthUpdate: (bitRateEstimate: Long, height: Int, width: Int, trackId: String) -> Unit lateinit var onVideoPlaybackStateChanged: (isPlaying: Boolean, isSeeking: Boolean) -> Unit lateinit var onVideoSeek: (currentPosition: Long, seekTime: Long) -> Unit + lateinit var onVideoSeekComplete: (currentPosition: Long) -> Unit lateinit var onVideoEnd: () -> Unit lateinit var onVideoFullscreenPlayerWillPresent: () -> Unit lateinit var onVideoFullscreenPlayerDidPresent: () -> Unit @@ -170,6 +172,11 @@ class VideoEventEmitter { putDouble("seekTime", seekTime / 1000.0) } } + onVideoSeekComplete = { currentPosition -> + event.dispatch(EventTypes.EVENT_SEEK_COMPLETE) { + putDouble("currentTime", currentPosition / 1000.0) + } + } onVideoEnd = { event.dispatch(EventTypes.EVENT_END) } diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 6f854efb..52757b32 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -23,6 +23,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.text.TextUtils; +import android.util.Log; import android.view.View; import android.view.Window; import android.view.accessibility.CaptioningManager; @@ -303,6 +304,15 @@ public class ReactExoplayerView extends FrameLayout implements } }; + private void handleSeekCompletion() { + if (player != null && player.getPlaybackState() == Player.STATE_READY) { + Log.d("ReactExoplayerView", "onSeekComplete triggered. Current position: " + player.getCurrentPosition()); + eventEmitter.onSeekComplete.invoke(player.getCurrentPosition()); + isSeeking = false; + seekPosition = -1; + } + } + public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) { Timeline.Window window = new Timeline.Window(); if(!player.getCurrentTimeline().isEmpty()) { @@ -761,7 +771,8 @@ public class ReactExoplayerView extends FrameLayout implements .setBandwidthMeter(bandwidthMeter) .setLoadControl(loadControl) .setMediaSourceFactory(mediaSourceFactory) - .build(); + .build(); + player.addListener(self); ReactNativeVideoManager.Companion.getInstance().onInstanceCreated(instanceId, player); refreshDebugState(); player.addListener(self); @@ -1371,6 +1382,7 @@ public class ReactExoplayerView extends FrameLayout implements playerControlView.show(); } setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback); + handleSeekCompletion(); break; case Player.STATE_ENDED: text += "ended"; @@ -2137,6 +2149,8 @@ public class ReactExoplayerView extends FrameLayout implements public void seekTo(long positionMs) { if (player != null) { + isSeeking = true; + seekPosition = positionMs; player.seekTo(positionMs); } } From f0db0a68683fa780d60ccdc88a9274c39fcdffd2 Mon Sep 17 00:00:00 2001 From: Kat Huang Date: Sat, 17 Aug 2024 17:33:52 -0600 Subject: [PATCH 4/7] kat wip --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 52757b32..3d4ff996 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -307,7 +307,7 @@ public class ReactExoplayerView extends FrameLayout implements private void handleSeekCompletion() { if (player != null && player.getPlaybackState() == Player.STATE_READY) { Log.d("ReactExoplayerView", "onSeekComplete triggered. Current position: " + player.getCurrentPosition()); - eventEmitter.onSeekComplete.invoke(player.getCurrentPosition()); + eventEmitter.onVideoSeekComplete.invoke(player.getCurrentPosition()); isSeeking = false; seekPosition = -1; } From c7a45d421b168dcc61235ee06c684c9fbbc05eb9 Mon Sep 17 00:00:00 2001 From: Kat Huang Date: Sat, 17 Aug 2024 18:04:24 -0600 Subject: [PATCH 5/7] Only complete seek if seek was in progress --- .../java/com/brentvatne/exoplayer/ReactExoplayerView.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 3d4ff996..ac435dcc 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -229,6 +229,7 @@ public class ReactExoplayerView extends FrameLayout implements */ private boolean isSeeking = false; private long seekPosition = -1; + private boolean isSeekInProgress = false; // Props from React private Source source = new Source(); @@ -310,6 +311,7 @@ public class ReactExoplayerView extends FrameLayout implements eventEmitter.onVideoSeekComplete.invoke(player.getCurrentPosition()); isSeeking = false; seekPosition = -1; + isSeekInProgress = false; } } @@ -1382,7 +1384,9 @@ public class ReactExoplayerView extends FrameLayout implements playerControlView.show(); } setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback); - handleSeekCompletion(); + if (isSeekInProgress) { + handleSeekCompletion(); + } break; case Player.STATE_ENDED: text += "ended"; @@ -2149,6 +2153,7 @@ public class ReactExoplayerView extends FrameLayout implements public void seekTo(long positionMs) { if (player != null) { + isSeekInProgress = true; isSeeking = true; seekPosition = positionMs; player.seekTo(positionMs); From 2dcde42fd64e8cb8db4a15a7d0478254b7a7297c Mon Sep 17 00:00:00 2001 From: Kat Huang Date: Sat, 17 Aug 2024 21:23:13 -0600 Subject: [PATCH 6/7] Add logs --- .../com/brentvatne/exoplayer/ReactExoplayerView.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index ac435dcc..b6cd9106 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -306,9 +306,9 @@ public class ReactExoplayerView extends FrameLayout implements }; private void handleSeekCompletion() { - if (player != null && player.getPlaybackState() == Player.STATE_READY) { - Log.d("ReactExoplayerView", "onSeekComplete triggered. Current position: " + player.getCurrentPosition()); - eventEmitter.onVideoSeekComplete.invoke(player.getCurrentPosition()); + if (player != null && player.getPlaybackState() == Player.STATE_READY && isSeekInProgress) { + Log.d("ReactExoplayerView", "handleSeekCompletion: currentPosition=" + player.getCurrentPosition()); + eventEmitter.onSeekComplete.invoke(player.getCurrentPosition()); isSeeking = false; seekPosition = -1; isSeekInProgress = false; @@ -1351,6 +1351,7 @@ public class ReactExoplayerView extends FrameLayout implements if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) { int playbackState = player.getPlaybackState(); boolean playWhenReady = player.getPlayWhenReady(); + Log.d("ReactExoplayerView", "onEvents: playbackState=" + playbackState + ", playWhenReady=" + playWhenReady); String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState="; eventEmitter.onPlaybackRateChange.invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f); switch (playbackState) { @@ -1384,6 +1385,7 @@ public class ReactExoplayerView extends FrameLayout implements playerControlView.show(); } setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback); + Log.d("ReactExoplayerView", "Player STATE_READY: currentPosition=" + player.getCurrentPosition()); if (isSeekInProgress) { handleSeekCompletion(); } @@ -1650,6 +1652,7 @@ public class ReactExoplayerView extends FrameLayout implements @Override public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) { + Log.d("ReactExoplayerView", "onPositionDiscontinuity: reason=" + reason + ", oldPosition=" + oldPosition.positionMs + ", newPosition=" + newPosition.positionMs); if (reason == Player.DISCONTINUITY_REASON_SEEK) { isSeeking = true; seekPosition = newPosition.positionMs; @@ -2153,6 +2156,7 @@ public class ReactExoplayerView extends FrameLayout implements public void seekTo(long positionMs) { if (player != null) { + Log.d("ReactExoplayerView", "seekTo: positionMs=" + positionMs); isSeekInProgress = true; isSeeking = true; seekPosition = positionMs; From d2ab22b99fc86c25b97dd873863ed2e755afe61d Mon Sep 17 00:00:00 2001 From: Kat Huang Date: Sat, 17 Aug 2024 21:28:29 -0600 Subject: [PATCH 7/7] wip --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index b6cd9106..95eb11b9 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -308,7 +308,7 @@ public class ReactExoplayerView extends FrameLayout implements private void handleSeekCompletion() { if (player != null && player.getPlaybackState() == Player.STATE_READY && isSeekInProgress) { Log.d("ReactExoplayerView", "handleSeekCompletion: currentPosition=" + player.getCurrentPosition()); - eventEmitter.onSeekComplete.invoke(player.getCurrentPosition()); + eventEmitter.onVideoSeekComplete.invoke(player.getCurrentPosition()); isSeeking = false; seekPosition = -1; isSeekInProgress = false;