Skip to content

Commit

Permalink
Attempt to sync two videos.
Browse files Browse the repository at this point in the history
  • Loading branch information
marcin-dziennik-miquido committed Sep 8, 2023
1 parent 7c79051 commit 300794d
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 7 deletions.
12 changes: 12 additions & 0 deletions Video.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ export default class Video extends Component {
)
};

setMasterVideoId = (masterId) => {
this.setNativeProps({
masterVideo: masterId
})
}

setSlaveVideoId = (slaveId) => {
this.setNativeProps({
slaveVideo: slaveId
})
}

toTypeString(x) {
switch (typeof x) {
case 'object':
Expand Down
19 changes: 19 additions & 0 deletions ios/Video/CurrentVideos.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// CurrentVideos.h
// react-native-video
//
// Created by marcin.dziennik on 9/8/23.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class RCTVideo;
@interface CurrentVideos : NSObject
+ (instancetype)shared;
- (void)add: (RCTVideo*)video forTag: (NSNumber*)tag;
- (nullable RCTVideo*)videoForTag: (NSNumber*)tag;
@end

NS_ASSUME_NONNULL_END
56 changes: 56 additions & 0 deletions ios/Video/CurrentVideos.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// CurrentVideos.m
// react-native-video
//
// Created by marcin.dziennik on 9/8/23.
//

#import "CurrentVideos.h"
#import "RCTVideo.h"
#import "WeakVideoRef.h"

@interface CurrentVideos ()
@property (strong) NSMutableDictionary <NSNumber*, WeakVideoRef*> *videos;
@end

@implementation CurrentVideos

+ (nonnull instancetype)shared {
static CurrentVideos *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}

- (instancetype)init {
if (self = [super init]) {
_videos = [[NSMutableDictionary alloc] init];
}

return self;
}

- (void)add:(nonnull RCTVideo *)video forTag:(nonnull NSNumber *)tag {
[self cleanup];
WeakVideoRef *ref = [[WeakVideoRef alloc] init];
ref.video = video;
[_videos setObject:ref forKey:tag];
}


- (nullable RCTVideo *)videoForTag:(nonnull NSNumber *)tag {
[self cleanup];
return [[_videos objectForKey:tag] video];
}

- (void)cleanup {
for (NSNumber* key in [_videos allKeys]) {
if (_videos[key].video == nil) {
[_videos removeObjectForKey:key];
}
}
}

@end
8 changes: 7 additions & 1 deletion ios/Video/RCTVideo.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ typedef NS_ENUM(NSInteger, RCTVideoError) {
RCTVideoErrorNoDRMData
};

typedef enum {
VideoStateUnknown = 0,
VideoStateLoaded = 1 << 0,
VideoStateReady = 1 << 1
} VideoState;

- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;

- (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem;
Expand All @@ -65,5 +71,5 @@ typedef NS_ENUM(NSInteger, RCTVideoError) {
- (void)setLicenseResult:(NSString * )license;
- (BOOL)setLicenseResultError:(NSString * )error;
- (void)requestedCurrentTime:(nonnull NSNumber *)requestId;

- (void)onSlaveVideoStatusChange;
@end
214 changes: 208 additions & 6 deletions ios/Video/RCTVideo.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <MediaAccessibility/MediaAccessibility.h>
#include <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>
#import "CurrentVideos.h"

static NSString *const statusKeyPath = @"status";
static NSString *const playbackLikelyToKeepUpKeyPath = @"playbackLikelyToKeepUp";
Expand Down Expand Up @@ -98,6 +99,10 @@ @implementation RCTVideo
void (^__strong _Nonnull _restoreUserInterfaceForPIPStopCompletionHandler)(BOOL);
AVPictureInPictureController *_pipController;
#endif
NSNumber *_masterVideo;
NSNumber *_slaveVideo;
VideoState _videoState;
BOOL _masterPendingPlayRequest;
}

- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
Expand Down Expand Up @@ -127,6 +132,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
_pictureInPicture = false;
_ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey
_mixWithOthers = @"inherit"; // inherit, mix, duck
_videoState = VideoStateUnknown;
#if TARGET_OS_IOS
_restoreUserInterfaceForPIPStopCompletionHandler = NULL;
#endif
Expand Down Expand Up @@ -447,6 +453,7 @@ - (void)setSrc:(NSDictionary *)source
}];
});
_videoLoadStarted = YES;
[[CurrentVideos shared] add:self forTag:self.reactTag];
}

- (void)setDrm:(NSDictionary *)drm {
Expand Down Expand Up @@ -646,6 +653,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
{

if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey] && self.onReadyForDisplay) {
[self updateState:VideoStateReady];
self.onReadyForDisplay(@{@"target": self.reactTag});
return;
}
Expand Down Expand Up @@ -712,6 +720,8 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
}

if (self.onVideoLoad && _videoLoadStarted) {
[self updateState:VideoStateLoaded];

self.onVideoLoad(@{@"duration": [NSNumber numberWithFloat:duration],
@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(_playerItem.currentTime)],
@"canPlayReverse": [NSNumber numberWithBool:_playerItem.canPlayReverse],
Expand Down Expand Up @@ -745,12 +755,20 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
_playerBufferEmpty = YES;
self.onVideoBuffer(@{@"isBuffering": @(YES), @"target": self.reactTag});
} else if ([keyPath isEqualToString:playbackLikelyToKeepUpKeyPath]) {
// Continue playing (or not if paused) after being paused due to hitting an unbuffered zone.
if ((!(_controls || _fullscreenPlayerPresented) || _playerBufferEmpty) && _playerItem.playbackLikelyToKeepUp) {
[self setPaused:_paused];
}
_playerBufferEmpty = NO;
self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag});
if ([self isManaged]) {
if ([self isSlave]) {
[self onSlaveVideoStatusChange];
} else {
_masterPendingPlayRequest = YES;
}
} else {
// Continue playing (or not if paused) after being paused due to hitting an unbuffered zone.
if ((!(_controls || _fullscreenPlayerPresented) || _playerBufferEmpty) && _playerItem.playbackLikelyToKeepUp) {
[self setPaused:_paused];
}
_playerBufferEmpty = NO;
self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag});
}
}
} else if (object == _player) {
if([keyPath isEqualToString:playbackRate]) {
Expand Down Expand Up @@ -963,6 +981,10 @@ - (void)setMixWithOthers:(NSString *)mixWithOthers

- (void)setPaused:(BOOL)paused
{
if ([self isManaged]) {
[self setManagedPaused:paused];
return;
}
if (paused) {
[_player pause];
[_player setRate:0.0];
Expand Down Expand Up @@ -998,6 +1020,15 @@ - (void)setCurrentTime:(float)currentTime

- (void)setSeek:(NSDictionary *)info
{
if ([self isManaged]) {
if ([self isSlave]) {
return;
}
RCTVideo *slave = [self slave];
[self setSeekManaged:info];
[slave setSeekManaged:info];
return;
}
NSNumber *seekTime = info[@"time"];
NSNumber *seekTolerance = info[@"tolerance"];

Expand Down Expand Up @@ -2074,4 +2105,175 @@ - (void)requestedCurrentTime:(nonnull NSNumber *) requestId {
self.onCommandResult(result);
}


- (void)setMasterVideo:(NSNumber*)tag {
_masterVideo = tag;
}

- (void)setSlaveVideo:(NSNumber*)tag {
_slaveVideo = tag;
}

- (BOOL)isMaster {
return _slaveVideo != nil;
}

- (BOOL)isSlave {
return _masterVideo != nil;
}

- (nullable RCTVideo*)slave {
if (_slaveVideo == nil) {
return nil;
}

return [[CurrentVideos shared] videoForTag:_slaveVideo];
}

- (nullable RCTVideo*)master {
if (_masterVideo == nil) {
return nil;
}

return [[CurrentVideos shared] videoForTag:_masterVideo];
}


- (BOOL)isManaged {
return [self isSlave] || [self isMaster];
}

- (void)setManagedPaused: (BOOL)paused {
if ([self isSlave]) {
return;
}
RCTVideo *slave = [self slave];
AVPlayer *slavePlayer = [slave player];
if (paused) {
[_player pause];
[slavePlayer pause];
[_player setRate:0.0];
[slavePlayer setRate:0.0];
} else {
if (![self isVideoReady] || ![slave isVideoReady]) {
_masterPendingPlayRequest = YES;
return;
}
[self configureAudio];
[slave configureAudio];

if (@available(iOS 10.0, *) && !_automaticallyWaitsToMinimizeStalling) {
[_player playImmediatelyAtRate:_rate];
[slavePlayer playImmediatelyAtRate:_rate];
} else {
[_player play];
[slavePlayer play];
}
[_player setRate:_rate];
[slavePlayer setRate:_rate];
}
_paused = paused;
[slave setRawPaused:paused];
}

- (AVPlayer*)player {
return _player;
}

- (void)setRawPaused: (BOOL) paused {
_paused = paused;
}

- (BOOL)isVideoReady {
return (_videoState & VideoStateLoaded) && (_videoState & VideoStateReady);
}

- (void)setSeekManaged:(NSDictionary*) info {
if (![self isMaster]) {
return;
}
RCTVideo *slave = [self slave];
NSNumber *seekTime = info[@"time"];
NSNumber *seekTolerance = info[@"tolerance"];

int timeScale = 1000;

AVPlayerItem *item = _player.currentItem;
AVPlayer *slavePlayer = [slave player];
AVPlayerItem *slaveItem = [slavePlayer currentItem];
if (item && item.status == AVPlayerItemStatusReadyToPlay && slaveItem && slaveItem.status == AVPlayerItemStatusReadyToPlay) {
// TODO check loadedTimeRanges

CMTime cmSeekTime = CMTimeMakeWithSeconds([seekTime floatValue], timeScale);
CMTime current = item.currentTime;
// TODO figure out a good tolerance level
CMTime tolerance = CMTimeMake([seekTolerance floatValue], timeScale);

if (CMTimeCompare(current, cmSeekTime) != 0) {
[_player pause];
[slavePlayer pause];

dispatch_group_t seekGroup = dispatch_group_create();

dispatch_group_enter(seekGroup);
[_player seekToTime:cmSeekTime toleranceBefore:tolerance toleranceAfter:tolerance completionHandler:^(BOOL finished) {
dispatch_group_leave(seekGroup);
}];
dispatch_group_enter(seekGroup);
[slavePlayer seekToTime:cmSeekTime toleranceBefore:tolerance toleranceAfter:tolerance completionHandler:^(BOOL finished) {
dispatch_group_leave(seekGroup);
}];

dispatch_group_notify(seekGroup, dispatch_get_main_queue(), ^{
[self seekCompletedForSeekTime:seekTime];
[slave seekCompletedForSeekTime:seekTime];
dispatch_after(DISPATCH_TIME_NOW + 1 * NSEC_PER_SEC, dispatch_get_main_queue(), ^{
[self setManagedPaused:NO];
});
});
_pendingSeek = false;
}

} else {
_pendingSeek = true;
_pendingSeekTime = [seekTime floatValue];
}
}

- (void)seekCompletedForSeekTime:(NSNumber*)seekTime {
AVPlayer *item = _player.currentItem;
if (!_timeObserver) {
[self addPlayerTimeObserver];
}

if(self.onVideoSeek) {
self.onVideoSeek(@{@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(item.currentTime)],
@"seekTime": seekTime,
@"target": self.reactTag});
}

}

- (void)updateState:(VideoState) newState {
if (![self isManaged]) {
return;
}
_videoState |= newState;
if ([self isSlave]) {
RCTVideo *master = [self master];
[master onSlaveVideoStatusChange];
}
}

- (void)onSlaveVideoStatusChange {
RCTVideo *slave = [self slave];
if (!slave) {
return;
}

if ([slave isVideoReady] && _masterPendingPlayRequest) {
[self setPaused:NO];
_masterPendingPlayRequest = NO;
}
}
@end
Loading

0 comments on commit 300794d

Please sign in to comment.