diff --git a/android/build.gradle b/android/build.gradle index 6faf1d5..03e0e3b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -27,7 +27,9 @@ import groovy.json.JsonSlurper def registrationCompat = { def reactNativeManifest = file("$rootDir/../node_modules/react-native/package.json").exists() ? file("$rootDir/../node_modules/react-native/package.json") - : file("$rootDir/../../node_modules/react-native/package.json") + : file("$rootDir/../../node_modules/react-native/package.json").exists() + ? file("$rootDir/../../node_modules/react-native/package.json") + : file("$rootDir/../../../node_modules/react-native/package.json") def reactNativeVersion = new JsonSlurper().parseText(reactNativeManifest.text).version as String // Fabric was introduced at react-native@0.68, full CMake support were introduced at react-native@0.70 // Use Android.mk for compatibility with react-native@0.68/0.69 diff --git a/android/src/main/java/com/fugood/reactnativevideoplayer/ReactNativeVideoPlayerView.kt b/android/src/main/java/com/fugood/reactnativevideoplayer/ReactNativeVideoPlayerView.kt index c6660b6..e8bbc06 100644 --- a/android/src/main/java/com/fugood/reactnativevideoplayer/ReactNativeVideoPlayerView.kt +++ b/android/src/main/java/com/fugood/reactnativevideoplayer/ReactNativeVideoPlayerView.kt @@ -214,8 +214,22 @@ class ReactNativeVideoPlayerView : FrameLayout, SurfaceHolder.Callback, TextureV } if (mUrl?.isEmpty() == false) { mState = State.PREPARING - player!!.setDataSource(context, Uri.parse(mUrl), mHeaders) - player!!.prepareAsync() + try { + player!!.setDataSource(context, Uri.parse(mUrl), mHeaders) + } catch (e: Exception) { + Log.e("ReactNativeVideoPlayerView", "Failed to set data source: ${e.message}", e) + mState = State.IDLE + fireEvent(ReactVideoErrorEvent(surfaceId, id, "MEDIA_ERROR_INVALID_SOURCE")) + return + } + try { + player!!.prepareAsync() + } catch (e: Exception) { + Log.e("ReactNativeVideoPlayerView", "Failed to prepare: ${e.message}", e) + mState = State.IDLE + fireEvent(ReactVideoErrorEvent(surfaceId, id, "MEDIA_ERROR_PREPARE_FAILED")) + return + } player!!.setOnPreparedListener(this) player!!.setOnCompletionListener(this) player!!.setOnErrorListener(this) @@ -394,6 +408,7 @@ class ReactNativeVideoPlayerView : FrameLayout, SurfaceHolder.Callback, TextureV mp.setLooping(mLoop) mp.setVolume(volume, volume) fireEvent(ReactVideoReadyEvent(surfaceId, id)) + fireEvent(ReactVideoLoadEvent(surfaceId, id)) if (mSeekTo > 0) { if (!mPaused) { mState = State.PLAYING @@ -458,7 +473,6 @@ class ReactNativeVideoPlayerView : FrameLayout, SurfaceHolder.Callback, TextureV fireEvent(ReactVideoBufferEvent(surfaceId, id, false)) } MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START -> { - fireEvent(ReactVideoLoadEvent(surfaceId, id)) updateProgress() } } diff --git a/ios/ReactNativeVideoPlayerView.h b/ios/ReactNativeVideoPlayerView.h index d274cc9..8863a4e 100644 --- a/ios/ReactNativeVideoPlayerView.h +++ b/ios/ReactNativeVideoPlayerView.h @@ -8,8 +8,34 @@ NS_ASSUME_NONNULL_BEGIN +// Dummy block type for interop compatibility +typedef void (^RCTDirectEventBlock)(NSDictionary *_Nullable body); + @interface ReactNativeVideoPlayerView : RCTViewComponentView +// Props for Legacy View Manager Interop compatibility +// These are handled via updateProps in New Architecture but must be declared +// for the interop layer which expects individual setters +@property(nonatomic, copy, nullable) NSDictionary *source; +@property(nonatomic, assign) BOOL paused; +@property(nonatomic, assign) Float64 seek; +@property(nonatomic, assign) float volume; +@property(nonatomic, assign) float speed; +@property(nonatomic, assign) BOOL muted; +@property(nonatomic, assign) BOOL loop; +@property(nonatomic, copy, nullable) NSString *resizeMode; +@property(nonatomic, assign) int progressUpdateInterval; + +// Event callbacks for Legacy View Manager Interop compatibility +// These are not used in New Architecture (event emitters are used instead) +// but must be declared to avoid crashes when interop layer tries to set them +@property(nonatomic, copy, nullable) RCTDirectEventBlock onReadyForDisplay; +@property(nonatomic, copy, nullable) RCTDirectEventBlock onLoad; +@property(nonatomic, copy, nullable) RCTDirectEventBlock onProgress; +@property(nonatomic, copy, nullable) RCTDirectEventBlock onEnd; +@property(nonatomic, copy, nullable) RCTDirectEventBlock onBuffer; +@property(nonatomic, copy, nullable) RCTDirectEventBlock onError; + - (void)play; - (void)pause; - (void)stop; diff --git a/ios/ReactNativeVideoPlayerView.mm b/ios/ReactNativeVideoPlayerView.mm index b05980a..edffee9 100644 --- a/ios/ReactNativeVideoPlayerView.mm +++ b/ios/ReactNativeVideoPlayerView.mm @@ -44,6 +44,9 @@ @implementation ReactNativeVideoPlayerView { AVPlayer *_player; AVPlayerLayer *_layer; id _timeObserver; + BOOL _didRelease; + NSString *_currentUri; + NSDictionary *_currentHeaders; } #pragma mark - Common @@ -56,6 +59,7 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { } - (void)initCommon:(UIView *)view { + _didRelease = NO; _player = [[AVPlayer alloc] init]; _layer = [AVPlayerLayer playerLayerWithPlayer:_player]; _layer.videoGravity = AVLayerVideoGravityResizeAspect; @@ -101,10 +105,17 @@ - (void)addPlayerObservers { } - (void)removePlayerObservers { - [_player removeObserver:self forKeyPath:STATUS_KEY]; - [_player removeObserver:self forKeyPath:CURR_STATUS_KEY]; - [_player removeObserver:self forKeyPath:CURR_BUFF_EMPTY_KEY]; - [_player removeObserver:self forKeyPath:CURR_CONTINUE_PLAY_KEY]; + if (!_player) { + return; + } + @try { + [_player removeObserver:self forKeyPath:STATUS_KEY]; + [_player removeObserver:self forKeyPath:CURR_STATUS_KEY]; + [_player removeObserver:self forKeyPath:CURR_BUFF_EMPTY_KEY]; + [_player removeObserver:self forKeyPath:CURR_CONTINUE_PLAY_KEY]; + } @catch (NSException *exception) { + // Observers might already be removed if cleanup ran earlier. + } } - (void)layoutSubviews { @@ -135,7 +146,14 @@ - (void)removeFromSuperview { - (void)prepareForRecycle { [_player pause]; #ifdef RCT_NEW_ARCH_ENABLED - _needReplay = YES; + if (_player.currentItem) { + [_player.currentItem cancelPendingSeeks]; + } + [_player replaceCurrentItemWithPlayerItem:nil]; + if (_layer) { + _layer.contents = nil; + } + _needReplay = (_currentUri != nil); #endif } @@ -146,16 +164,24 @@ - (void)restorePlay:(NSNotification *)notification { } - (void)_release { + if (_didRelease) { + return; + } + _didRelease = YES; [[NSNotificationCenter defaultCenter] removeObserver:self]; [self removePlayerObservers]; - if (_timeObserver) { + if (_player && _timeObserver) { [_player removeTimeObserver:_timeObserver]; _timeObserver = nil; } - [_player pause]; - [_player replaceCurrentItemWithPlayerItem:nil]; + if (_player) { + [_player pause]; + [_player replaceCurrentItemWithPlayerItem:nil]; + } + if (_layer) { + [_layer removeFromSuperlayer]; + } _player = nil; - [_layer removeFromSuperlayer]; _layer = nil; } @@ -175,6 +201,20 @@ - (void)playItem:(AVPlayerItem *)item { [_player replaceCurrentItemWithPlayerItem:item]; } +- (void)reloadCurrentSource { + if (!_currentUri) { + return; + } + NSDictionary *headersCopy = + _currentHeaders ? [[NSDictionary alloc] initWithDictionary:_currentHeaders] + : nil; + AVPlayerItem *item = [Utils sourceToPlayItem:_currentUri headers:headersCopy]; + if (!item) { + return; + } + [self playItem:item]; +} + - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change @@ -251,26 +291,55 @@ - (void)setLoop:(BOOL)loop { _loop = loop; } +- (void)setSource:(NSDictionary *)source { + if (source == nil) { + return; + } + NSString *uri = + [[NSString alloc] initWithUTF8String:[source[@"uri"] UTF8String]]; + NSDictionary *headers = source[@"headers"]; + _currentUri = uri; + _currentHeaders = headers ? [[NSDictionary alloc] initWithDictionary:headers] + : nil; + [self playItem:[Utils sourceToPlayItem:uri headers:headers]]; +} + +- (void)setResizeMode:(NSString *)resizeMode { + if ([resizeMode isEqualToString:@"stretch"]) { + [self setLayerGravity:AVLayerVideoGravityResize]; + } else if ([resizeMode isEqualToString:@"cover"]) { + [self setLayerGravity:AVLayerVideoGravityResizeAspectFill]; + } else { + [self setLayerGravity:AVLayerVideoGravityResizeAspect]; + } +} + - (void)setProgressUpdateInterval:(int)ms { if (_timeObserver) { [_player removeTimeObserver:_timeObserver]; } CMTime interval = CMTimeMakeWithSeconds((Float64)ms / 1000.0, NSEC_PER_SEC); + __weak __typeof(self) weakSelf = self; _timeObserver = [_player addPeriodicTimeObserverForInterval:interval queue:NULL usingBlock:^(CMTime time) { - CMTime duration = _player.currentItem.duration; + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + AVPlayer *player = strongSelf->_player; + CMTime duration = player.currentItem.duration; if (CMTIME_IS_INVALID(duration)) { return; } - CMTime currentTime = _player.currentTime; + CMTime currentTime = player.currentTime; Float64 currentTimeSec = CMTimeGetSeconds(currentTime); Float64 durationSec = CMTimeGetSeconds(duration); - [self emitOnProgress:currentTimeSec - duration:durationSec]; + [strongSelf emitOnProgress:currentTimeSec + duration:durationSec]; }]; } @@ -346,9 +415,16 @@ - (void)updateProps:(Props::Shared const &)props [headers setValue:value forKey:key]; } } + _currentUri = uri; + _currentHeaders = headers ? [[NSDictionary alloc] initWithDictionary:headers] + : nil; [self playItem:[Utils sourceToPlayItem:uri headers:headers]]; } else if (_needReplay) { - [self seekTo:0]; + if (_player.currentItem) { + [self seekTo:0]; + } else { + [self reloadCurrentSource]; + } _needReplay = NO; } @@ -407,6 +483,12 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { } - (void)emitOnReady { + // Try callback block first (for Legacy View Manager Interop) + if (self.onReadyForDisplay) { + self.onReadyForDisplay(nil); + return; + } + // Fall back to event emitter (for pure Fabric) if (_eventEmitter) { std::dynamic_pointer_cast( _eventEmitter) @@ -416,6 +498,10 @@ - (void)emitOnReady { } - (void)emitOnLoad { + if (self.onLoad) { + self.onLoad(nil); + return; + } if (_eventEmitter) { std::dynamic_pointer_cast( _eventEmitter) @@ -424,6 +510,12 @@ - (void)emitOnLoad { } - (void)emitOnError:(NSError *)error { + if (self.onError) { + self.onError(@{ + @"message" : error.localizedDescription ?: @"Unknown error", + }); + return; + } if (_eventEmitter) { std::dynamic_pointer_cast( _eventEmitter) @@ -434,6 +526,10 @@ - (void)emitOnError:(NSError *)error { } - (void)emitOnEnd { + if (self.onEnd) { + self.onEnd(nil); + return; + } if (_eventEmitter) { std::dynamic_pointer_cast( _eventEmitter) @@ -442,6 +538,13 @@ - (void)emitOnEnd { } - (void)emitOnProgress:(Float64)currentTime duration:(Float64)duration { + if (self.onProgress) { + self.onProgress(@{ + @"currentTime" : @(currentTime), + @"duration" : @(duration), + }); + return; + } if (_eventEmitter) { std::dynamic_pointer_cast( _eventEmitter) @@ -453,6 +556,12 @@ - (void)emitOnProgress:(Float64)currentTime duration:(Float64)duration { } - (void)emitOnBuffer:(BOOL)buffering { + if (self.onBuffer) { + self.onBuffer(@{ + @"isBuffering" : @(buffering), + }); + return; + } if (_eventEmitter) { std::dynamic_pointer_cast( _eventEmitter) @@ -478,24 +587,8 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge { return self; } -- (void)setSource:(NSDictionary *)source { - if (source == nil) { - return; - } - NSString *uri = - [[NSString alloc] initWithUTF8String:[source[@"uri"] UTF8String]]; - NSDictionary *headers = source[@"headers"]; - [self playItem:[Utils sourceToPlayItem:uri headers:headers]]; -} - -- (void)setResizeMode:(NSString *)resizeMode { - if ([resizeMode isEqualToString:@"stretch"]) { - [self setLayerGravity:AVLayerVideoGravityResize]; - } else if ([resizeMode isEqualToString:@"cover"]) { - [self setLayerGravity:AVLayerVideoGravityResizeAspectFill]; - } else { - [self setLayerGravity:AVLayerVideoGravityResizeAspect]; - } +- (void)dealloc { + [self _release]; } - (void)emitOnReady {