diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index c340e14945..54fc7e1860 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -805,6 +805,36 @@ public void getCurrentPosition(Promise promise) { } } + public void getCurrentPlaybackStatus(Promise promise) { + if (player != null) { + int playbackState = player.getPlaybackState(); + switch (playbackState) { + case Player.STATE_IDLE: + promise.resolve("idle"); + break; + case Player.STATE_BUFFERING: + promise.resolve("buffering"); + break; + case Player.STATE_READY: + if (isPaused) { + promise.resolve("paused"); + } else { + promise.resolve("playing"); + } + break; + case Player.STATE_ENDED: + promise.resolve("ended"); + break; + default: + promise.resolve("unknown"); + break; + } + } else { + promise.resolve("released"); + } + } + + private void initializePlayerCore(ReactExoplayerView self) { ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); self.trackSelector = new DefaultTrackSelector(getContext(), videoTrackSelectionFactory); diff --git a/android/src/main/java/com/brentvatne/react/VideoManagerModule.kt b/android/src/main/java/com/brentvatne/react/VideoManagerModule.kt index d396827682..f899f8984a 100644 --- a/android/src/main/java/com/brentvatne/react/VideoManagerModule.kt +++ b/android/src/main/java/com/brentvatne/react/VideoManagerModule.kt @@ -79,6 +79,13 @@ class VideoManagerModule(reactContext: ReactApplicationContext?) : ReactContextB } } + @ReactMethod + fun getCurrentPlaybackStatus(reactTag: Int, promise: Promise) { + performOnPlayerView(reactTag) { + it?.getCurrentPlaybackStatus(promise) + } + } + companion object { private const val REACT_CLASS = "VideoManager" } diff --git a/docs/pages/component/methods.mdx b/docs/pages/component/methods.mdx index b3b0ebc091..26efb443dc 100644 --- a/docs/pages/component/methods.mdx +++ b/docs/pages/component/methods.mdx @@ -137,6 +137,18 @@ On iOS, this displays the video in a fullscreen view controller with controls. On Android, this puts the navigation controls in fullscreen mode. It is not a complete fullscreen implementation, so you will still need to apply a style that makes the width and height match your screen dimensions to get a fullscreen video. +### `getCurrentPlaybackStatus` + +This function retrieves the current playback status of the media and can return one of the following states: + +- **idle** (Android only): The media is not yet prepared, and the player must be prepared before it can play the media. +- **paused**: The media is loaded but not actively playing. +- **playing**: The media is currently playing. +- **buffering**: The media is loading or waiting for data to continue playback. +- **ended**: The media has finished playing. +- **unknown**: The media has unknown status. +- **released**: The player is not initialized. + ### Example Usage ```tsx diff --git a/examples/basic/ios/Podfile.lock b/examples/basic/ios/Podfile.lock index 124bb1a674..7479bf3540 100644 --- a/examples/basic/ios/Podfile.lock +++ b/examples/basic/ios/Podfile.lock @@ -994,7 +994,7 @@ PODS: - React-Mapbuffer (0.74.5): - glog - React-debug - - react-native-video (6.6.2): + - react-native-video (6.6.4): - DoubleConversion - glog - hermes-engine @@ -1008,7 +1008,7 @@ PODS: - React-featureflags - React-graphics - React-ImageManager - - react-native-video/Video (= 6.6.2) + - react-native-video/Video (= 6.6.4) - React-NativeModulesApple - React-RCTFabric - React-rendererdebug @@ -1038,7 +1038,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-video/Video (6.6.2): + - react-native-video/Video (6.6.4): - DoubleConversion - glog - hermes-engine @@ -1559,7 +1559,7 @@ SPEC CHECKSUMS: React-jsitracing: c83efb63c8e9e1dff72a3c56e88ae1c530a87795 React-logger: 257858bd55f3a4e1bc0cf07ddc8fb9faba6f8c7c React-Mapbuffer: dce508662b995ffefd29e278a16b78217039d43d - react-native-video: b52a7473fd467d8c18032535ec5f332b81f9bdf0 + react-native-video: c9889e8b3449e88191c396aaff3b9a48868ce695 react-native-video-plugin-sample: d3a93b7ad777cad7fa2c30473de75a2635ce5feb React-nativeconfig: f326487bc61eba3f0e328da6efb2711533dcac46 React-NativeModulesApple: d89733f5baed8b9249ca5a8e497d63c550097312 @@ -1590,7 +1590,7 @@ SPEC CHECKSUMS: SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d - Yoga: 1ab23c1835475da69cf14e211a560e73aab24cb0 + Yoga: 33622183a85805e12703cd618b2c16bfd18bfffb PODFILE CHECKSUM: a73d485df51877001f2b04a5a4379cfa5a3ba8fa diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 92cea601ce..9266628d3e 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -1685,6 +1685,44 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } } + @objc + func getCurrentPlaybackStatus(_ resolve: @escaping RCTPromiseResolveBlock, _ reject: @escaping RCTPromiseRejectBlock) { + if let player = _player { + switch player.status { + case .unknown: + resolve("unknown") + case .failed: + reject("PLAYBACK_ERROR", "Playback failed", player.error) + case .readyToPlay: + if let currentItem = player.currentItem { + let currentTime = CMTimeGetSeconds(player.currentTime()) + let duration = CMTimeGetSeconds(currentItem.duration) + + if currentTime >= duration { + resolve("ended") + } else { + switch player.timeControlStatus { + case .paused: + resolve("paused") + case .waitingToPlayAtSpecifiedRate: + resolve("buffering") + case .playing: + resolve("playing") + @unknown default: + resolve("unknown") + } + } + } else { + resolve("unknown") + } + @unknown default: + resolve("unknown") + } + } else { + resolve("released") + } + } + // Workaround for #3418 - https://github.com/TheWidlarzGroup/react-native-video/issues/3418#issuecomment-2043508862 @objc func setOnClick(_: Any) {} diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index edb9d0ff5e..bc8ab225ff 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -86,5 +86,9 @@ @interface RCT_EXTERN_MODULE (RCTVideoManager, RCTViewManager) : (nonnull NSNumber*)reactTag resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(getCurrentPlaybackStatus + : (nonnull NSNumber*)reactTag resolve + : (RCTPromiseResolveBlock)resolve reject + : (RCTPromiseRejectBlock)reject) @end diff --git a/ios/Video/RCTVideoManager.swift b/ios/Video/RCTVideoManager.swift index dbe83ba0cd..1fe2e8bf43 100644 --- a/ios/Video/RCTVideoManager.swift +++ b/ios/Video/RCTVideoManager.swift @@ -93,6 +93,13 @@ class RCTVideoManager: RCTViewManager { }) } + @objc(getCurrentPlaybackStatus:resolve:reject:) + func getCurrentPlaybackStatus(_ reactTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + performOnVideoView(withReactTag: reactTag, callback: { videoView in + videoView?.getCurrentPlaybackStatus(resolve, reject) + }) + } + override class func requiresMainQueueSetup() -> Bool { return true } diff --git a/src/Video.tsx b/src/Video.tsx index 0a27054ed8..eb7baea396 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -54,6 +54,7 @@ import type { ReactVideoProps, CmcdData, ReactVideoSource, + TPlaybackStatus, } from './types'; export interface VideoRef { @@ -70,6 +71,7 @@ export interface VideoRef { setSource: (source?: ReactVideoSource) => void; save: (options: object) => Promise | void; getCurrentPosition: () => Promise; + getCurrentPlaybackStatus: () => Promise; } const Video = forwardRef( @@ -413,6 +415,13 @@ const Video = forwardRef( return NativeVideoManager.getCurrentPosition(getReactTag(nativeRef)); }, []); + const getCurrentPlaybackStatus = useCallback(() => { + // @todo Must implement it in a different way. + return NativeVideoManager.getCurrentPlaybackStatus( + getReactTag(nativeRef), + ); + }, []); + const restoreUserInterfaceForPictureInPictureStopCompleted = useCallback( (restored: boolean) => { setRestoreUserInterfaceForPIPStopCompletionHandler(restored); @@ -648,6 +657,7 @@ const Video = forwardRef( getCurrentPosition, setFullScreen, setSource, + getCurrentPlaybackStatus, }), [ seek, @@ -661,6 +671,7 @@ const Video = forwardRef( getCurrentPosition, setFullScreen, setSource, + getCurrentPlaybackStatus, ], ); diff --git a/src/specs/NativeVideoManager.ts b/src/specs/NativeVideoManager.ts index 3b1b261ef6..8789317fc7 100644 --- a/src/specs/NativeVideoManager.ts +++ b/src/specs/NativeVideoManager.ts @@ -4,6 +4,7 @@ import type { Float, UnsafeObject, } from 'react-native/Libraries/Types/CodegenTypes'; +import {TPlaybackStatus} from '../types'; export type VideoSaveData = { uri: string; @@ -28,6 +29,7 @@ export interface VideoManagerType { setVolumeCmd: (reactTag: Int32, volume: number) => Promise; save: (reactTag: Int32, option: UnsafeObject) => Promise; getCurrentPosition: (reactTag: Int32) => Promise; + getCurrentPlaybackStatus: (reactTag: Int32) => Promise; } export default NativeModules.VideoManager as VideoManagerType; diff --git a/src/types/video.ts b/src/types/video.ts index 1d6e907714..d1e71ddd35 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -40,6 +40,15 @@ export type ReactVideoSourceProperties = { textTracks?: TextTracks; }; +export type TPlaybackStatus = { + paused: 'paused'; + playing: 'playing'; + buffering: 'buffering'; + ended: 'ended'; + idle: 'idle'; + unknown: 'unknown'; +}; + export type ReactVideoSource = Readonly< Omit & { uri?: string | NodeRequire;