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 a7be7058..7fc6cc92 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 @@ -174,6 +176,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 8fefa34c..d3344a3a 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.accessibility.CaptioningManager; import android.widget.FrameLayout; @@ -224,6 +225,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(); @@ -303,6 +305,16 @@ 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.onVideoSeekComplete.invoke(player.getCurrentPosition()); + isSeeking = false; + seekPosition = -1; + isSeekInProgress = false; + } + } + public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) { Timeline.Window window = new Timeline.Window(); if(!player.getCurrentTimeline().isEmpty()) { @@ -852,7 +864,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); @@ -1437,6 +1450,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) { @@ -1470,6 +1484,10 @@ public class ReactExoplayerView extends FrameLayout implements playerControlView.show(); } setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback); + Log.d("ReactExoplayerView", "Player STATE_READY: currentPosition=" + player.getCurrentPosition()); + if (isSeekInProgress) { + handleSeekCompletion(); + } break; case Player.STATE_ENDED: text += "ended"; @@ -1734,6 +1752,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; @@ -2249,6 +2268,10 @@ 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; player.seekTo(positionMs); } } diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 0701067f..714374a1 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -113,6 +113,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? @@ -782,34 +783,50 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _paused = paused } + @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 } - RCTPlayerOperations.seek( - player: player, - playerItem: item, - paused: _paused, - seekTime: time.floatValue, - seekTolerance: tolerance.floatValue - ) { [weak self] (_: Bool) in - guard let self else { return } + 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)) + + 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() self.setPaused(self._paused) - self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))), - "seekTime": time, - "target": self.reactTag as Any]) + + let newCurrentTime = NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))) + self.onVideoSeekComplete?(["currentTime": newCurrentTime, + "seekTime": time, + "target": self.reactTag as Any]) + } - _pendingSeek = false + player.seek(to: seekTime, toleranceBefore: toleranceTime, toleranceAfter: toleranceTime, completionHandler: seekCompletionHandler) } @objc @@ -1682,3 +1699,4 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc func setOnClick(_: Any) {} } + diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 2a3b14e8..196bc032 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -45,6 +45,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 1da0bdb1..43a931d1 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -32,6 +32,7 @@ import type { OnPlaybackStateChangedData, OnProgressData, OnSeekData, + OnSeekCompleteData, OnTextTrackDataChangedData, OnTimedMetadataData, OnVideoAspectRatioData, @@ -97,6 +98,7 @@ const Video = forwardRef( onError, onProgress, onSeek, + onSeekComplete, onEnd, onBuffer, onBandwidthUpdate, @@ -469,6 +471,13 @@ const Video = forwardRef( [onSeek], ); + const onVideoSeekComplete = useCallback( + (e: NativeSyntheticEvent) => { + onSeekComplete?.(e.nativeEvent); + }, + [onSeekComplete] + ); + const onVideoPlaybackStateChanged = useCallback( (e: NativeSyntheticEvent) => { onPlaybackStateChanged?.(e.nativeEvent); @@ -811,6 +820,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 0600b9da..43794aee 100644 --- a/src/specs/VideoNativeComponent.ts +++ b/src/specs/VideoNativeComponent.ts @@ -202,6 +202,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; @@ -377,6 +383,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 f8f405e8..56dc711c 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -12,6 +12,7 @@ import type { OnPlaybackStateChangedData, OnProgressData, OnSeekData, + OnSeekCompleteData, OnTextTrackDataChangedData, OnTimedMetadataData, OnVideoAspectRatioData, @@ -260,6 +261,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