Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}
Expand Down
26 changes: 26 additions & 0 deletions ios/ReactNativeVideoPlayerView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
157 changes: 125 additions & 32 deletions ios/ReactNativeVideoPlayerView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ @implementation ReactNativeVideoPlayerView {
AVPlayer *_player;
AVPlayerLayer *_layer;
id _timeObserver;
BOOL _didRelease;
NSString *_currentUri;
NSDictionary *_currentHeaders;
}

#pragma mark - Common
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand All @@ -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;
}

Expand All @@ -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
Expand Down Expand Up @@ -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];
}];
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<const ReactNativeVideoPlayerViewEventEmitter>(
_eventEmitter)
Expand All @@ -416,6 +498,10 @@ - (void)emitOnReady {
}

- (void)emitOnLoad {
if (self.onLoad) {
self.onLoad(nil);
return;
}
if (_eventEmitter) {
std::dynamic_pointer_cast<const ReactNativeVideoPlayerViewEventEmitter>(
_eventEmitter)
Expand All @@ -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<const ReactNativeVideoPlayerViewEventEmitter>(
_eventEmitter)
Expand All @@ -434,6 +526,10 @@ - (void)emitOnError:(NSError *)error {
}

- (void)emitOnEnd {
if (self.onEnd) {
self.onEnd(nil);
return;
}
if (_eventEmitter) {
std::dynamic_pointer_cast<const ReactNativeVideoPlayerViewEventEmitter>(
_eventEmitter)
Expand All @@ -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<const ReactNativeVideoPlayerViewEventEmitter>(
_eventEmitter)
Expand All @@ -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<const ReactNativeVideoPlayerViewEventEmitter>(
_eventEmitter)
Expand All @@ -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 {
Expand Down