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 b7b1ea69..c82aef8d 100644 --- a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt +++ b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt @@ -47,7 +47,7 @@ enum class EventTypes(val eventName: String) { companion object { fun toMap() = mutableMapOf().apply { - EventTypes.entries.forEach { eventType -> + EventTypes.values().toList().forEach { eventType -> put("top${eventType.eventName.removePrefix("on")}", mapOf("registrationName" to eventType.eventName)) } } diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 7429160a..e9ae70bc 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -7,7 +7,6 @@ import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; -import androidx.media3.common.util.Util; import com.brentvatne.common.api.BufferConfig; import com.brentvatne.common.api.BufferingStrategy; @@ -203,7 +202,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager = listOf( - VideoDecoderPropertiesModule(reactContext), + VideoDecoderInfoModule(reactContext), VideoManagerModule(reactContext) ) diff --git a/android/src/main/java/com/brentvatne/react/VideoDecoderPropertiesModule.kt b/android/src/main/java/com/brentvatne/react/VideoDecoderInfoModule.kt similarity index 80% rename from android/src/main/java/com/brentvatne/react/VideoDecoderPropertiesModule.kt rename to android/src/main/java/com/brentvatne/react/VideoDecoderInfoModule.kt index ab80bab1..2cd41cac 100644 --- a/android/src/main/java/com/brentvatne/react/VideoDecoderPropertiesModule.kt +++ b/android/src/main/java/com/brentvatne/react/VideoDecoderInfoModule.kt @@ -11,7 +11,7 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import java.util.UUID -class VideoDecoderPropertiesModule(reactContext: ReactApplicationContext?) : ReactContextBaseJavaModule(reactContext) { +class VideoDecoderInfoModule(reactContext: ReactApplicationContext?) : ReactContextBaseJavaModule(reactContext) { override fun getName(): String = REACT_CLASS @ReactMethod @@ -37,36 +37,36 @@ class VideoDecoderPropertiesModule(reactContext: ReactApplicationContext?) : Rea } @ReactMethod - fun isCodecSupported(mimeType: String?, width: Int, height: Int, p: Promise) { + fun isCodecSupported(mimeType: String?, width: Double, height: Double, p: Promise?) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - p.resolve("unsupported") + p?.resolve("unsupported") return } val mRegularCodecs = MediaCodecList(MediaCodecList.REGULAR_CODECS) - val format = MediaFormat.createVideoFormat(mimeType!!, width, height) + val format = MediaFormat.createVideoFormat(mimeType!!, width.toInt(), height.toInt()) val codecName = mRegularCodecs.findDecoderForFormat(format) if (codecName == null) { - p.resolve("unsupported") + p?.resolve("unsupported") return } // Fallback for android < 10 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - p.resolve("software") + p?.resolve("software") return } val isHardwareAccelerated = mRegularCodecs.codecInfos.any { it.name.equals(codecName, ignoreCase = true) && it.isHardwareAccelerated } - p.resolve(if (isHardwareAccelerated) "software" else "hardware") + p?.resolve(if (isHardwareAccelerated) "software" else "hardware") } @ReactMethod - fun isHEVCSupported(p: Promise) = isCodecSupported("video/hevc", 1920, 1080, p) + fun isHEVCSupported(p: Promise) = isCodecSupported("video/hevc", 1920.0, 1080.0, p) companion object { private val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L) private const val SECURITY_LEVEL_PROPERTY = "securityLevel" - private const val REACT_CLASS = "VideoDecoderProperties" + private const val REACT_CLASS = "VideoDecoderInfoModule" } } diff --git a/android/src/main/java/com/brentvatne/react/VideoManagerModule.kt b/android/src/main/java/com/brentvatne/react/VideoManagerModule.kt index 716f337d..1e624e7c 100644 --- a/android/src/main/java/com/brentvatne/react/VideoManagerModule.kt +++ b/android/src/main/java/com/brentvatne/react/VideoManagerModule.kt @@ -1,12 +1,10 @@ package com.brentvatne.react -import com.brentvatne.common.toolbox.ReactBridgeUtils import com.brentvatne.exoplayer.ReactExoplayerView import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod -import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.common.UIManagerType @@ -37,31 +35,33 @@ class VideoManagerModule(reactContext: ReactApplicationContext?) : ReactContextB } @ReactMethod - fun setPlayerPauseState(paused: Boolean?, reactTag: Int) { + fun setPlayerPauseStateCmd(reactTag: Int, paused: Boolean?) { performOnPlayerView(reactTag) { it?.setPausedModifier(paused!!) } } @ReactMethod - fun seek(info: ReadableMap, reactTag: Int) { - if (!info.hasKey("time")) { - return - } - - val time = ReactBridgeUtils.safeGetInt(info, "time") + fun seekCmd(reactTag: Int, time: Float, tolerance: Float) { performOnPlayerView(reactTag) { it?.seekTo((time * 1000f).roundToInt().toLong()) } } @ReactMethod - fun setVolume(volume: Float, reactTag: Int) { + fun setVolumeCmd(reactTag: Int, volume: Float) { performOnPlayerView(reactTag) { it?.setVolumeModifier(volume) } } + @ReactMethod + fun setFullScreenCmd(reactTag: Int, fullScreen: Boolean) { + performOnPlayerView(reactTag) { + it?.setFullscreen(fullScreen) + } + } + @ReactMethod fun getCurrentPosition(reactTag: Int, promise: Promise) { performOnPlayerView(reactTag) { @@ -69,13 +69,6 @@ class VideoManagerModule(reactContext: ReactApplicationContext?) : ReactContextB } } - @ReactMethod - fun setFullScreen(fullScreen: Boolean, reactTag: Int) { - performOnPlayerView(reactTag) { - it?.setFullscreen(fullScreen) - } - } - companion object { private const val REACT_CLASS = "VideoManager" } diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index bf9e2d14..c4f4e99a 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -762,15 +762,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setSeek(_ info: NSDictionary!) { - let seekTime: NSNumber! = info["time"] as! NSNumber - let seekTolerance: NSNumber! = info["tolerance"] as! NSNumber + 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 { - _pendingSeekTime = seekTime.floatValue + _pendingSeekTime = time.floatValue return } @@ -778,15 +776,15 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH player: player, playerItem: item, paused: _paused, - seekTime: seekTime.floatValue, - seekTolerance: seekTolerance.floatValue + seekTime: time.floatValue, + seekTolerance: tolerance.floatValue ) { [weak self] (_: Bool) in guard let self else { return } self._playerObserver.addTimeObserverIfNotSet() self.setPaused(self._paused) self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))), - "seekTime": seekTime, + "seekTime": time, "target": self.reactTag]) } @@ -1303,7 +1301,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // MARK: - Export @objc - func save(options: NSDictionary!, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + func save(_ options: NSDictionary!, _ resolve: @escaping RCTPromiseResolveBlock, _ reject: @escaping RCTPromiseRejectBlock) { RCTVideoSave.save( options: options, resolve: resolve, @@ -1320,14 +1318,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _resouceLoaderDelegate?.setLicenseResultError(error, licenseUrl) } - func dismissFullscreenPlayer() { - setFullscreen(false) - } - - func presentFullscreenPlayer() { - setFullscreen(true) - } - // MARK: - RCTPlayerObserverHandler func handleTimeUpdate(time _: CMTime) { @@ -1381,18 +1371,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH Task { if self._pendingSeek { - self.setSeek([ - "time": NSNumber(value: self._pendingSeekTime), - "tolerance": NSNumber(value: 100), - ]) + self.setSeek(NSNumber(value: self._pendingSeekTime), NSNumber(value: 100)) self._pendingSeek = false } if self._startPosition >= 0 { - self.setSeek([ - "time": NSNumber(value: self._startPosition), - "tolerance": NSNumber(value: 100), - ]) + self.setSeek(NSNumber(value: self._startPosition), NSNumber(value: 100)) self._startPosition = -1 } diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 8c4bad60..2693d36e 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -1,5 +1,5 @@ #import "React/RCTViewManager.h" -#import +#import @interface RCT_EXTERN_MODULE (RCTVideoManager, RCTViewManager) @@ -37,7 +37,7 @@ RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float); RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL); RCT_EXPORT_VIEW_PROPERTY(localSourceEncryptionKeyScheme, NSString); RCT_EXPORT_VIEW_PROPERTY(subtitleStyle, NSDictionary); -RCT_EXPORT_VIEW_PROPERTY(showNotificationControls, BOOL) +RCT_EXPORT_VIEW_PROPERTY(showNotificationControls, BOOL); /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */ RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTDirectEventBlock); @@ -68,31 +68,21 @@ RCT_EXPORT_VIEW_PROPERTY(onTextTracks, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onAudioTracks, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onTextTrackDataChanged, RCTDirectEventBlock); +RCT_EXTERN_METHOD(seekCmd : (nonnull NSNumber*)reactTag time : (nonnull NSNumber*)time tolerance : (nonnull NSNumber*)tolerance) +RCT_EXTERN_METHOD(setLicenseResultCmd : (nonnull NSNumber*)reactTag lisence : (NSString*)license licenseUrl : (NSString*)licenseUrl) +RCT_EXTERN_METHOD(setLicenseResultErrorCmd : (nonnull NSNumber*)reactTag error : (NSString*)error licenseUrl : (NSString*)licenseUrl) +RCT_EXTERN_METHOD(setPlayerPauseStateCmd : (nonnull NSNumber*)reactTag paused : (nonnull BOOL)paused) +RCT_EXTERN_METHOD(setVolumeCmd : (nonnull NSNumber*)reactTag volume : (nonnull float*)volume) +RCT_EXTERN_METHOD(setFullScreenCmd : (nonnull NSNumber*)reactTag fullscreen : (nonnull BOOL)fullScreen) + RCT_EXTERN_METHOD(save - : (NSDictionary*)options reactTag - : (nonnull NSNumber*)reactTag resolver - : (RCTPromiseResolveBlock)resolve rejecter + : (nonnull NSNumber*)reactTag options + : (NSDictionary*)options resolve + : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(seek : (NSDictionary*)info reactTag : (nonnull NSNumber*)reactTag) - -RCT_EXTERN_METHOD(setLicenseResult : (NSString*)license licenseUrl : (NSString*)licenseUrl reactTag : (nonnull NSNumber*)reactTag) - -RCT_EXTERN_METHOD(setLicenseResultError : (NSString*)error licenseUrl : (NSString*)licenseUrl reactTag : (nonnull NSNumber*)reactTag) - -RCT_EXTERN_METHOD(setPlayerPauseState : (nonnull NSNumber*)paused reactTag : (nonnull NSNumber*)reactTag) - -RCT_EXTERN_METHOD(presentFullscreenPlayer : (nonnull NSNumber*)reactTag) - -RCT_EXTERN_METHOD(dismissFullscreenPlayer : (nonnull NSNumber*)reactTag) - -RCT_EXTERN_METHOD(setVolume : (nonnull float*)volume reactTag : (nonnull NSNumber*)reactTag) - RCT_EXTERN_METHOD(getCurrentPosition - : (nonnull NSNumber*)reactTag resolver - : (RCTPromiseResolveBlock)resolve rejecter + : (nonnull NSNumber*)reactTag resolve + : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(setFullScreen : (BOOL)fullScreen reactTag : (nonnull NSNumber*)reactTag) - @end diff --git a/ios/Video/RCTVideoManager.swift b/ios/Video/RCTVideoManager.swift index 12ac4222..26d40edd 100644 --- a/ios/Video/RCTVideoManager.swift +++ b/ios/Video/RCTVideoManager.swift @@ -30,76 +30,62 @@ class RCTVideoManager: RCTViewManager { } } - @objc(save:reactTag:resolver:rejecter:) - func save(options: NSDictionary, reactTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + @objc(seekCmd:time:tolerance:) + func seekCmd(_ reactTag: NSNumber, time: NSNumber, tolerance: NSNumber) { performOnVideoView(withReactTag: reactTag, callback: { videoView in - videoView?.save(options: options, resolve: resolve, reject: reject) + videoView?.setSeek(time, tolerance) }) } - @objc(seek:reactTag:) - func seek(info: NSDictionary, reactTag: NSNumber) { - performOnVideoView(withReactTag: reactTag, callback: { videoView in - videoView?.setSeek(info) - }) - } - - @objc(setLicenseResult:licenseUrl:reactTag:) - func setLicenseResult(license: NSString, licenseUrl: NSString, reactTag: NSNumber) { + @objc(setLicenseResultCmd:license:licenseUrl:) + func setLicenseResultCmd(_ reactTag: NSNumber, license: NSString, licenseUrl: NSString) { performOnVideoView(withReactTag: reactTag, callback: { videoView in videoView?.setLicenseResult(license as String, licenseUrl as String) }) } - @objc(setLicenseResultError:licenseUrl:reactTag:) - func setLicenseResultError(error: NSString, licenseUrl: NSString, reactTag: NSNumber) { + @objc(setLicenseResultErrorCmd:error:licenseUrl:) + func setLicenseResultErrorCmd(_ reactTag: NSNumber, error: NSString, licenseUrl: NSString) { performOnVideoView(withReactTag: reactTag, callback: { videoView in videoView?.setLicenseResultError(error as String, licenseUrl as String) }) } - @objc(dismissFullscreenPlayer:) - func dismissFullscreenPlayer(_ reactTag: NSNumber) { + @objc(setPlayerPauseStateCmd:paused:) + func setPlayerPauseStateCmd(_ reactTag: NSNumber, paused: Bool) { performOnVideoView(withReactTag: reactTag, callback: { videoView in - videoView?.dismissFullscreenPlayer() + videoView?.setPaused(paused) }) } - @objc(presentFullscreenPlayer:) - func presentFullscreenPlayer(_ reactTag: NSNumber) { + @objc(setVolumeCmd:volume:) + func setVolumeCmd(_ reactTag: NSNumber, volume: Float) { performOnVideoView(withReactTag: reactTag, callback: { videoView in - videoView?.presentFullscreenPlayer() + videoView?.setVolume(volume) }) } - @objc(setPlayerPauseState:reactTag:) - func setPlayerPauseState(paused: NSNumber, reactTag: NSNumber) { - performOnVideoView(withReactTag: reactTag, callback: { videoView in - videoView?.setPaused(paused.boolValue) - }) - } - - @objc(setVolume:reactTag:) - func setVolume(value: Float, reactTag: NSNumber) { - performOnVideoView(withReactTag: reactTag, callback: { videoView in - videoView?.setVolume(value) - }) - } - - @objc(getCurrentPosition:resolver:rejecter:) - func getCurrentPosition(reactTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - performOnVideoView(withReactTag: reactTag, callback: { videoView in - videoView?.getCurrentPlaybackTime(resolve, reject) - }) - } - - @objc(setFullScreen:reactTag:) - func setFullScreen(fullScreen: Bool, reactTag: NSNumber) { + @objc(setFullScreenCmd:fullscreen:) + func setFullScreenCmd(_ reactTag: NSNumber, fullScreen: Bool) { performOnVideoView(withReactTag: reactTag, callback: { videoView in videoView?.setFullscreen(fullScreen) }) } + @objc(save:options:resolve:reject:) + func save(_ reactTag: NSNumber, options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + performOnVideoView(withReactTag: reactTag, callback: { videoView in + videoView?.save(options, resolve, reject) + }) + } + + @objc(getCurrentPosition:resolve:reject:) + func getCurrentPosition(_ reactTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + performOnVideoView(withReactTag: reactTag, callback: { videoView in + videoView?.getCurrentPlaybackTime(resolve, reject) + }) + } + override class func requiresMainQueueSetup() -> Bool { return true } diff --git a/src/Video.tsx b/src/Video.tsx index 1fd8e73b..f2f91f3b 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -5,57 +5,47 @@ import React, { useRef, forwardRef, useImperativeHandle, - type ComponentRef, } from 'react'; -import { - View, - StyleSheet, - Image, - Platform, - type StyleProp, - type ImageStyle, - type NativeSyntheticEvent, -} from 'react-native'; +import type {ElementRef} from 'react'; +import {View, StyleSheet, Image, Platform, processColor} from 'react-native'; +import type {StyleProp, ImageStyle, NativeSyntheticEvent} from 'react-native'; -import NativeVideoComponent, { - type OnAudioFocusChangedData, - type OnAudioTracksData, - type OnBandwidthUpdateData, - type OnBufferData, - type OnControlsVisibilityChange, - type OnExternalPlaybackChangeData, - type OnGetLicenseData, - type OnLoadStartData, - type OnPictureInPictureStatusChangedData, - type OnPlaybackStateChangedData, - type OnProgressData, - type OnSeekData, - type OnTextTrackDataChangedData, - type OnTimedMetadataData, - type OnVideoAspectRatioData, - type OnVideoErrorData, - type OnVideoTracksData, - type VideoComponentType, - type VideoSrc, +import NativeVideoComponent from './specs/VideoNativeComponent'; +import type { + OnAudioFocusChangedData, + OnAudioTracksData, + OnBandwidthUpdateData, + OnBufferData, + OnControlsVisibilityChange, + OnExternalPlaybackChangeData, + OnGetLicenseData, + OnLoadStartData, + OnPictureInPictureStatusChangedData, + OnPlaybackStateChangedData, + OnProgressData, + OnSeekData, + OnTextTrackDataChangedData, + OnTimedMetadataData, + OnVideoAspectRatioData, + OnVideoErrorData, + OnVideoTracksData, + VideoSrc, } from './specs/VideoNativeComponent'; import { generateHeaderForNative, getReactTag, resolveAssetSourceForVideo, } from './utils'; -import {VideoManager} from './specs/VideoNativeComponent'; -import { - type OnLoadData, - type OnTextTracksData, - type OnReceiveAdEventData, - type ReactVideoProps, - ViewType, +import NativeVideoManager from './specs/NativeVideoManager'; +import type {VideoSaveData} from './specs/NativeVideoManager'; +import {ViewType} from './types'; +import type { + OnLoadData, + OnTextTracksData, + OnReceiveAdEventData, + ReactVideoProps, } from './types'; -export type VideoSaveData = { - uri: string; -}; - export interface VideoRef { seek: (time: number, tolerance?: number) => void; resume: () => void; @@ -65,10 +55,10 @@ export interface VideoRef { restoreUserInterfaceForPictureInPictureStopCompleted: ( restore: boolean, ) => void; - save: (options: object) => Promise; setVolume: (volume: number) => void; - getCurrentPosition: () => Promise; setFullScreen: (fullScreen: boolean) => void; + save: (options: object) => Promise | void; + getCurrentPosition: () => Promise; } const Video = forwardRef( @@ -79,7 +69,6 @@ const Video = forwardRef( resizeMode, posterResizeMode, poster, - fullscreen, drm, textTracks, selectedVideoTrack, @@ -88,6 +77,7 @@ const Video = forwardRef( useTextureView, useSecureView, viewType, + shutterColor, onLoadStart, onLoad, onError, @@ -122,9 +112,8 @@ const Video = forwardRef( }, ref, ) => { - const nativeRef = useRef>(null); + const nativeRef = useRef>(null); const [showPoster, setShowPoster] = useState(!!poster); - const [isFullscreen, setIsFullscreen] = useState(fullscreen); const [ _restoreUserInterfaceForPIPStopCompletionHandler, setRestoreUserInterfaceForPIPStopCompletionHandler, @@ -274,12 +263,10 @@ const Video = forwardRef( } const callSeekFunction = () => { - VideoManager.seek( - { - time, - tolerance: tolerance || 0, - }, + NativeVideoManager.seekCmd( getReactTag(nativeRef), + time, + tolerance || 0, ); }; @@ -287,31 +274,59 @@ const Video = forwardRef( ios: callSeekFunction, android: callSeekFunction, default: () => { - // TODO: Implement VideoManager.seek for windows + // TODO: Implement VideoManager.seekCmd for windows nativeRef.current?.setNativeProps({seek: time}); }, })(); }, []); - const presentFullscreenPlayer = useCallback(() => { - setIsFullscreen(true); - }, [setIsFullscreen]); - - const dismissFullscreenPlayer = useCallback(() => { - setIsFullscreen(false); - }, [setIsFullscreen]); - - const save = useCallback((options: object) => { - // VideoManager.save can be null on android & windows - return VideoManager.save?.(options, getReactTag(nativeRef)); - }, []); - const pause = useCallback(() => { - return VideoManager.setPlayerPauseState(true, getReactTag(nativeRef)); + return NativeVideoManager.setPlayerPauseStateCmd( + getReactTag(nativeRef), + true, + ); }, []); const resume = useCallback(() => { - return VideoManager.setPlayerPauseState(false, getReactTag(nativeRef)); + return NativeVideoManager.setPlayerPauseStateCmd( + getReactTag(nativeRef), + false, + ); + }, []); + + const setVolume = useCallback((volume: number) => { + return NativeVideoManager.setVolumeCmd(getReactTag(nativeRef), volume); + }, []); + + const setFullScreen = useCallback((fullScreen: boolean) => { + return NativeVideoManager.setFullScreenCmd( + getReactTag(nativeRef), + fullScreen, + ); + }, []); + + const presentFullscreenPlayer = useCallback( + () => setFullScreen(true), + [setFullScreen], + ); + + const dismissFullscreenPlayer = useCallback( + () => setFullScreen(false), + [setFullScreen], + ); + + const save = useCallback((options: object) => { + // VideoManager.save can be null on android & windows + if (Platform.OS !== 'ios') { + return; + } + // @todo Must implement it in a different way. + return NativeVideoManager.save?.(getReactTag(nativeRef), options); + }, []); + + const getCurrentPosition = useCallback(() => { + // @todo Must implement it in a different way. + return NativeVideoManager.getCurrentPosition(getReactTag(nativeRef)); }, []); const restoreUserInterfaceForPictureInPictureStopCompleted = useCallback( @@ -321,18 +336,6 @@ const Video = forwardRef( [setRestoreUserInterfaceForPIPStopCompletionHandler], ); - const setVolume = useCallback((volume: number) => { - return VideoManager.setVolume(volume, getReactTag(nativeRef)); - }, []); - - const getCurrentPosition = useCallback(() => { - return VideoManager.getCurrentPosition(getReactTag(nativeRef)); - }, []); - - const setFullScreen = useCallback((fullScreen: boolean) => { - return VideoManager.setFullScreen(fullScreen, getReactTag(nativeRef)); - }, []); - const onVideoLoadStart = useCallback( (e: NativeSyntheticEvent) => { hasPoster && setShowPoster(true); @@ -379,6 +382,11 @@ const Video = forwardRef( [onPlaybackStateChanged], ); + const _shutterColor = useMemo(() => { + const color = processColor(shutterColor); + return typeof color === 'number' ? color : undefined; + }, [shutterColor]); + // android only const _onTimedMetadata = useCallback( (e: NativeSyntheticEvent) => { @@ -494,56 +502,41 @@ const Video = forwardRef( [onControlsVisibilityChange], ); - const useExternalGetLicense = drm?.getLicense instanceof Function; + const usingExternalGetLicense = drm?.getLicense instanceof Function; const onGetLicense = useCallback( - (event: NativeSyntheticEvent) => { - if (useExternalGetLicense) { - const data = event.nativeEvent; - if (data && data.spcBase64) { - const getLicenseOverride = drm.getLicense( + async (event: NativeSyntheticEvent) => { + if (!usingExternalGetLicense) { + return; + } + const data = event.nativeEvent; + let result; + if (data?.spcBase64) { + try { + // Handles both scenarios, getLicenseOverride being a promise and not. + const license = await drm.getLicense( data.spcBase64, data.contentId, data.licenseUrl, data.loadedLicenseUrl, ); - const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not. - getLicensePromise - .then((result) => { - if (result !== undefined) { - nativeRef.current && - VideoManager.setLicenseResult( - result, - data.loadedLicenseUrl, - getReactTag(nativeRef), - ); - } else { - nativeRef.current && - VideoManager.setLicenseResultError( - 'Empty license result', - data.loadedLicenseUrl, - getReactTag(nativeRef), - ); - } - }) - .catch(() => { - nativeRef.current && - VideoManager.setLicenseResultError( - 'fetch error', - data.loadedLicenseUrl, - getReactTag(nativeRef), - ); - }); - } else { - VideoManager.setLicenseResultError( - 'No spc received', - data.loadedLicenseUrl, - getReactTag(nativeRef), - ); + result = + typeof license === 'string' ? license : 'Empty license result'; + } catch { + result = 'fetch error'; } + } else { + result = 'No spc received'; + } + if (nativeRef.current) { + NativeVideoManager.setLicenseResultErrorCmd( + getReactTag(nativeRef), + result, + data.loadedLicenseUrl, + ); } }, - [drm, useExternalGetLicense], + [drm, usingExternalGetLicense], ); useImperativeHandle( @@ -613,7 +606,6 @@ const Video = forwardRef( src={src} style={StyleSheet.absoluteFill} resizeMode={resizeMode} - fullscreen={isFullscreen} restoreUserInterfaceForPIPStopCompletionHandler={ _restoreUserInterfaceForPIPStopCompletionHandler } @@ -621,7 +613,8 @@ const Video = forwardRef( selectedTextTrack={_selectedTextTrack} selectedAudioTrack={_selectedAudioTrack} selectedVideoTrack={_selectedVideoTrack} - onGetLicense={useExternalGetLicense ? onGetLicense : undefined} + shutterColor={_shutterColor} + onGetLicense={usingExternalGetLicense ? onGetLicense : undefined} onVideoLoad={ onLoad || hasPoster ? (onVideoLoad as (e: NativeSyntheticEvent) => void) diff --git a/src/VideoDecoderProperties.ts b/src/VideoDecoderProperties.ts new file mode 100644 index 00000000..8c1d7f5b --- /dev/null +++ b/src/VideoDecoderProperties.ts @@ -0,0 +1,29 @@ +import {Platform} from 'react-native'; + +import NativeVideoDecoderInfoModule from './specs/NativeVideoDecoderInfoModule'; + +const errMsgGen = (moduleName: string, propertyName: string) => + `The method or property ${moduleName}.${propertyName} is not available on ${Platform.OS}.`; + +export const VideoDecoderProperties = { + async getWidevineLevel() { + if (Platform.OS !== 'android') { + throw new Error(errMsgGen('VideoDecoderProperties', 'getWidevineLevel')); + } + return NativeVideoDecoderInfoModule.getWidevineLevel(); + }, + async isCodecSupported( + ...args: Parameters + ) { + if (Platform.OS !== 'android') { + throw new Error(errMsgGen('VideoDecoderProperties', 'isCodecSupported')); + } + return NativeVideoDecoderInfoModule.isCodecSupported(...args); + }, + async isHEVCSupported() { + if (Platform.OS !== 'android') { + throw new Error(errMsgGen('VideoDecoderProperties', 'isHEVCSupported')); + } + return NativeVideoDecoderInfoModule.isHEVCSupported(); + }, +}; diff --git a/src/index.ts b/src/index.ts index 56fd87d2..575ac839 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import Video from './Video'; -export {VideoDecoderProperties} from './specs/VideoNativeComponent'; +export {VideoDecoderProperties} from './VideoDecoderProperties'; export * from './types'; export type {VideoRef} from './Video'; export default Video; diff --git a/src/specs/NativeVideoDecoderInfoModule.ts b/src/specs/NativeVideoDecoderInfoModule.ts new file mode 100644 index 00000000..25cf9cc2 --- /dev/null +++ b/src/specs/NativeVideoDecoderInfoModule.ts @@ -0,0 +1,15 @@ +import {NativeModules} from 'react-native'; +import type {Int32} from 'react-native/Libraries/Types/CodegenTypes'; + +// @TODO rename to "Spec" when applying new arch +interface VideoDecoderInfoModuleType { + getWidevineLevel: () => Promise; + isCodecSupported: ( + mimeType: string, + width: Int32, + height: Int32, + ) => Promise<'unsupported' | 'hardware' | 'software'>; + isHEVCSupported: () => Promise<'unsupported' | 'hardware' | 'software'>; +} + +export default NativeModules.VideoDecoderInfoModule as VideoDecoderInfoModuleType; diff --git a/src/specs/NativeVideoManager.ts b/src/specs/NativeVideoManager.ts new file mode 100644 index 00000000..9472d453 --- /dev/null +++ b/src/specs/NativeVideoManager.ts @@ -0,0 +1,32 @@ +import {NativeModules} from 'react-native'; +import type { + Int32, + Float, + UnsafeObject, +} from 'react-native/Libraries/Types/CodegenTypes'; + +export type VideoSaveData = { + uri: string; +}; + +// @TODO rename to "Spec" when applying new arch +export interface VideoManagerType { + seekCmd: (reactTag: Int32, time: Float, tolerance?: Float) => Promise; + setPlayerPauseStateCmd: (reactTag: Int32, paused: boolean) => Promise; + setLicenseResultCmd: ( + reactTag: Int32, + result: string, + licenseUrl: string, + ) => Promise; + setLicenseResultErrorCmd: ( + reactTag: Int32, + error: string, + licenseUrl: string, + ) => Promise; + setFullScreenCmd: (reactTag: Int32, fullScreen: boolean) => Promise; + setVolumeCmd: (reactTag: Int32, volume: number) => Promise; + save: (reactTag: Int32, option: UnsafeObject) => Promise; + getCurrentPosition: (reactTag: Int32) => Promise; +} + +export default NativeModules.VideoManager as VideoManagerType; diff --git a/src/specs/VideoNativeComponent.ts b/src/specs/VideoNativeComponent.ts index bb6a8056..b5f392b9 100644 --- a/src/specs/VideoNativeComponent.ts +++ b/src/specs/VideoNativeComponent.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/ban-types */ import type {HostComponent, ViewProps} from 'react-native'; -import {NativeModules, requireNativeComponent} from 'react-native'; +import {requireNativeComponent} from 'react-native'; import type { DirectEventHandler, Double, @@ -91,11 +91,6 @@ type SelectedVideoTrack = Readonly<{ value?: string; }>; -export type Seek = Readonly<{ - time: Float; - tolerance?: Float; -}>; - type BufferConfigLive = Readonly<{ maxPlaybackSpeed?: Float; minPlaybackSpeed?: Float; @@ -289,7 +284,7 @@ export type OnAudioFocusChangedData = Readonly<{ type ControlsStyles = Readonly<{ hideSeekBar?: boolean; - seekIncrementMS?: number; + seekIncrementMS?: Int32; }>; export type OnControlsVisibilityChange = Readonly<{ @@ -300,10 +295,13 @@ export interface VideoNativeProps extends ViewProps { src?: VideoSrc; adTagUrl?: string; allowsExternalPlayback?: boolean; // ios, true + disableFocus?: boolean; // android maxBitRate?: Float; resizeMode?: WithDefault; repeat?: boolean; automaticallyWaitsToMinimizeStalling?: boolean; + shutterColor?: Int32; + audioOutput?: WithDefault; textTracks?: TextTracks; selectedTextTrack?: SelectedTextTrack; selectedAudioTrack?: SelectedAudioTrack; @@ -375,45 +373,8 @@ export interface VideoNativeProps extends ViewProps { onVideoTracks?: DirectEventHandler; // android } -export type VideoComponentType = HostComponent; - -export type VideoSaveData = { - uri: string; -}; - -export interface VideoManagerType { - save: (option: object, reactTag: number) => Promise; - seek: (option: Seek, reactTag: number) => Promise; - setPlayerPauseState: (paused: boolean, reactTag: number) => Promise; - setLicenseResult: ( - result: string, - licenseUrl: string, - reactTag: number, - ) => Promise; - setLicenseResultError: ( - error: string, - licenseUrl: string, - reactTag: number, - ) => Promise; - setVolume: (volume: number, reactTag: number) => Promise; - getCurrentPosition: (reactTag: number) => Promise; - setFullScreen: (fullScreen: boolean, reactTag: number) => Promise; -} - -export interface VideoDecoderPropertiesType { - getWidevineLevel: () => Promise; - isCodecSupported: ( - mimeType: string, - width: number, - height: number, - ) => Promise<'unsupported' | 'hardware' | 'software'>; - isHEVCSupported: () => Promise<'unsupported' | 'hardware' | 'software'>; -} - -export const VideoManager = NativeModules.VideoManager as VideoManagerType; -export const VideoDecoderProperties = - NativeModules.VideoDecoderProperties as VideoDecoderPropertiesType; +type NativeVideoComponentType = HostComponent; export default requireNativeComponent( 'RCTVideo', -) as VideoComponentType; +) as NativeVideoComponentType; diff --git a/src/types/ViewType.ts b/src/types/ViewType.ts index 34e6a419..e4159a42 100644 --- a/src/types/ViewType.ts +++ b/src/types/ViewType.ts @@ -2,10 +2,10 @@ * Define Available view type for android * these values shall match android spec, see ViewType.kt */ -enum ResizeMode { +enum ViewType { TEXTURE = 0, SURFACE = 1, SURFACE_SECURE = 2, } -export default ResizeMode; +export default ViewType; diff --git a/src/types/video.ts b/src/types/video.ts index 463f30b5..15c2dfd8 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -68,7 +68,7 @@ export type Drm = Readonly<{ contentId: string, licenseUrl: string, loadedLicenseUrl: string, - ) => void; // ios + ) => string | Promise; // ios /* eslint-enable @typescript-eslint/no-unused-vars */ }>; diff --git a/src/utils.ts b/src/utils.ts index 4d5e29c8..149844b1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -43,6 +43,10 @@ export function resolveAssetSourceForVideo( return source as ReactVideoSourceProperties; } +/** + * @deprecated + * Do not use this fn anymore. "findNodeHandle" will be deprecated. + * */ export function getReactTag( ref: RefObject< | Component