1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import <AVFoundation/AVFoundation.h> 4#import <ExpoModulesCore/EXUtilities.h> 5#import <EXAV/EXAV.h> 6#import <EXAV/EXVideoView.h> 7#import <EXAV/EXAVPlayerData.h> 8#import <EXAV/EXVideoPlayerViewController.h> 9 10static NSString *const EXVideoReadyForDisplayKeyPath = @"readyForDisplay"; 11static NSString *const EXVideoSourceURIKeyPath = @"uri"; 12static NSString *const EXVideoSourceHeadersKeyPath = @"headers"; 13static NSString *const EXVideoBoundsKeyPath = @"videoBounds"; 14static NSString *const EXAVFullScreenViewControllerClassName = @"AVFullScreenViewController"; 15 16@interface EXVideoView () 17 18@property (nonatomic, weak) EXAV *exAV; 19 20@property (nonatomic, assign) BOOL playerHasLoaded; 21@property (nonatomic, strong) EXAVPlayerData *data; 22@property (nonatomic, strong) AVPlayerLayer *playerLayer; 23@property (nonatomic, strong) EXVideoPlayerViewController *playerViewController; 24 25@property (nonatomic, assign) BOOL fullscreenPlayerIsDismissing; 26@property (nonatomic, strong) EXVideoPlayerViewController *fullscreenPlayerViewController; 27@property (nonatomic, strong) EXPromiseResolveBlock requestedFullscreenChangeResolver; 28@property (nonatomic, strong) EXPromiseRejectBlock requestedFullscreenChangeRejecter; 29@property (nonatomic, assign) BOOL requestedFullscreenChange; 30 31@property (nonatomic, strong) UIViewController *presentingViewController; 32@property (nonatomic, assign) BOOL fullscreenPlayerPresented; 33 34@property (nonatomic, strong) NSDictionary *lastSetSource; 35@property (nonatomic, strong) NSMutableDictionary *statusToSet; 36 37@property (nonatomic, weak) EXModuleRegistry *moduleRegistry; 38 39@end 40 41@implementation EXVideoView 42 43#pragma mark - EXVideoView interface methods 44 45- (instancetype)initWithModuleRegistry:(nullable EXModuleRegistry *)moduleRegistry 46{ 47 if ((self = [super init])) { 48 _exAV = [moduleRegistry getModuleImplementingProtocol:@protocol(EXAVInterface)]; 49 [_exAV registerVideoForAudioLifecycle:self]; 50 51 _data = nil; 52 _playerLayer = nil; 53 _playerHasLoaded = NO; 54 _playerViewController = nil; 55 _presentingViewController = nil; 56 _fullscreenPlayerPresented = NO; 57 _fullscreenPlayerViewController = nil; 58 _requestedFullscreenChangeResolver = nil; 59 _requestedFullscreenChangeRejecter = nil; 60 _fullscreenPlayerIsDismissing = NO; 61 _requestedFullscreenChange = NO; 62 _statusToSet = [NSMutableDictionary new]; 63 _useNativeControls = NO; 64 _nativeResizeMode = AVLayerVideoGravityResizeAspectFill; 65 } 66 67 return self; 68} 69 70#pragma mark - callback helper methods 71 72- (void)_callFullscreenCallbackForUpdate:(EXVideoFullscreenUpdate)update 73{ 74 if (_onFullscreenUpdate) { 75 _onFullscreenUpdate(@{@"fullscreenUpdate": @(update), 76 @"status": [_data getStatus]}); 77 } 78} 79 80- (void)_callErrorCallback:(NSString *)error 81{ 82 if (_onError) { 83 _onError(@{@"error": error}); 84 } 85} 86 87#pragma mark - Player and source 88 89- (void)_tryUpdateDataStatus:(EXPromiseResolveBlock)resolve 90 rejecter:(EXPromiseRejectBlock)reject 91{ 92 if (_data) { 93 if ([_statusToSet count] > 0) { 94 NSMutableDictionary *newStatus = [NSMutableDictionary dictionaryWithDictionary:_statusToSet]; 95 [_statusToSet removeAllObjects]; 96 [_data setStatus:newStatus resolver:resolve rejecter:reject]; 97 } else if (resolve) { 98 resolve([_data getStatus]); 99 } 100 } else if (resolve) { 101 resolve([EXAVPlayerData getUnloadedStatus]); 102 } 103} 104 105- (void)_updateForNewPlayer 106{ 107 [self setPlayerHasLoaded:YES]; 108 [self setUseNativeControls:_useNativeControls]; 109 if (_onLoad) { 110 _onLoad([self getStatus]); 111 } 112 if (_requestedFullscreenChangeResolver || _requestedFullscreenChangeRejecter) { 113 [self setFullscreen:_requestedFullscreenChange resolver:_requestedFullscreenChangeResolver rejecter:_requestedFullscreenChangeRejecter]; 114 _requestedFullscreenChangeResolver = nil; 115 _requestedFullscreenChangeRejecter = nil; 116 _requestedFullscreenChange = NO; 117 } 118} 119 120- (void)_removeData { 121 EX_WEAKIFY(self); 122 void (^block)(void) = ^{ 123 EX_ENSURE_STRONGIFY(self); 124 if (self->_data) { 125 [self->_data cleanup]; 126 [self->_data pauseImmediately]; 127 [self->_data setStatusUpdateCallback:nil]; 128 [self->_exAV demoteAudioSessionIfPossible]; 129 self->_data = nil; 130 } 131 }; 132 // Remove EXAVPlayerData on main thread to prevent race conditions 133 // while KVO messages are dispatched on main thread and the player data is 134 // de-allocating 135 [EXUtilities performSynchronouslyOnMainThread:block]; 136} 137 138- (void)_removePlayer 139{ 140 if (_requestedFullscreenChangeRejecter) { 141 _requestedFullscreenChangeRejecter(@"E_VIDEO_FULLSCREEN", @"Player is being removed, cancelling fullscreen change request.", nil); 142 _requestedFullscreenChangeResolver = nil; 143 _requestedFullscreenChangeRejecter = nil; 144 _requestedFullscreenChange = NO; 145 } 146 147 // Any ViewController/layer updates need to be 148 // executed on the main UI thread. 149 EX_WEAKIFY(self); 150 void (^block)(void) = ^ { 151 EX_ENSURE_STRONGIFY(self); 152 [self _removeFullscreenPlayerViewController]; 153 [self _removePlayerLayer]; 154 [self _removePlayerViewController]; 155 }; 156 _playerHasLoaded = NO; 157 [EXUtilities performSynchronouslyOnMainThread:block]; 158} 159 160#pragma mark - _playerViewController / _playerLayer management 161 162- (EXVideoPlayerViewController *)_createNewPlayerViewController 163{ 164 if (_data == nil) { 165 return nil; 166 } 167 EXVideoPlayerViewController *controller = [[EXVideoPlayerViewController alloc] init]; 168 [controller setShowsPlaybackControls:_useNativeControls]; 169 [controller setRctDelegate:self]; 170 [controller setDelegate:self]; 171 [controller.view setFrame:self.bounds]; 172 [controller setPlayer:_data.player]; 173 [controller addObserver:self forKeyPath:EXVideoReadyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil]; 174 return controller; 175} 176 177- (void)_usePlayerLayer 178{ 179 if (_data) { 180 _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_data.player]; 181 [_playerLayer setFrame:self.bounds]; 182 [_playerLayer setNeedsDisplayOnBoundsChange:YES]; 183 [_playerLayer addObserver:self forKeyPath:EXVideoReadyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil]; 184 185 // Resize mode must be set before layer is added 186 // to prevent video from being animated when `resizeMode` is `cover` 187 [self _updateNativeResizeMode]; 188 189 [self.layer addSublayer:_playerLayer]; 190 [self.layer setNeedsDisplayOnBoundsChange:YES]; 191 } 192} 193 194- (void)_removePlayerLayer 195{ 196 if (_playerLayer) { 197 [_playerLayer removeFromSuperlayer]; 198 [_playerLayer removeObserver:self forKeyPath:EXVideoReadyForDisplayKeyPath]; 199 _playerLayer = nil; 200 } 201} 202 203- (void)_removeFullscreenPlayerViewController 204{ 205 if (_fullscreenPlayerViewController) { 206 [_fullscreenPlayerViewController removeObserver:self forKeyPath:EXVideoReadyForDisplayKeyPath]; 207 _fullscreenPlayerViewController = nil; 208 } 209} 210 211- (void)_removePlayerViewController 212{ 213 if (_playerViewController) { 214 [_playerViewController.view removeFromSuperview]; 215 [_playerViewController removeObserver:self forKeyPath:EXVideoReadyForDisplayKeyPath]; 216 if (@available(iOS 12, *)) { 217 // EXVideoBounds monitoring is only used as a fallback on iOS 11 or lower 218 } else { 219 [_playerViewController removeObserver:self forKeyPath:EXVideoBoundsKeyPath]; 220 } 221 _playerViewController = nil; 222 } 223} 224 225 226#pragma mark - Observers 227 228- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 229{ 230 if ((object == _playerLayer || object == _playerViewController || object == _fullscreenPlayerViewController) && [keyPath isEqualToString:EXVideoReadyForDisplayKeyPath]) { 231 if ([change objectForKey:NSKeyValueChangeNewKey] && _onReadyForDisplay) { 232 // Calculate natural size of video: 233 NSDictionary *naturalSize; 234 235 if ([_data.player.currentItem.asset tracksWithMediaType:AVMediaTypeVideo].count > 0) { 236 AVAssetTrack *videoTrack = [[_data.player.currentItem.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; 237 238 // Videos can specify whether they should be rotated when displayed, 239 // using a transform matrix. We apply the transform matrix to get the 240 // actual size that will be displayed on screen. 241 CGSize actualSize = CGSizeApplyAffineTransform(videoTrack.naturalSize, videoTrack.preferredTransform); 242 243 // The rotation transform can result in negative widths/heights, so we 244 // need to make sure we return positive numbers that make sense. 245 CGFloat width = fabs(actualSize.width); 246 CGFloat height = fabs(actualSize.height); 247 248 naturalSize = @{@"width": @(width), 249 @"height": @(height), 250 @"orientation": width < height ? @"portrait" : @"landscape"}; 251 } else { 252 253 // For certain Assets (e.g. AVURLAsset/HSL-streams/m3u8 files), the natural size 254 // cannot be obtained from AVAssetTrack. In these cases fallback to using 255 // the presentationSize from AVPlayerItem. 256 // https://stackoverflow.com/questions/48553686/avfoundation-how-can-you-get-the-video-dimensions-of-a-video-being-streamed-by 257 CGSize presentationSize = _data.player.currentItem.presentationSize; 258 naturalSize = @{@"width": @(presentationSize.width), 259 @"height": @(presentationSize.height), 260 @"orientation": @"landscape"}; 261 } 262 263 _onReadyForDisplay(@{@"naturalSize": naturalSize, 264 @"status": [_data getStatus]}); 265 } 266 267 // On iOS 11 or lower, use video-bounds monitoring to detect changes in the full-screen 268 // mode due to activating native controls 269 } else if (object == _playerViewController && [keyPath isEqualToString:EXVideoBoundsKeyPath]) { 270 CGRect viewBounds = [change[@"new"] CGRectValue]; 271 CGRect screen = [[UIScreen mainScreen] bounds]; 272 if (viewBounds.size.height != screen.size.height && viewBounds.size.width != screen.size.width && _fullscreenPlayerPresented && !_fullscreenPlayerViewController) { 273 // Fullscreen player is being dismissed 274 _fullscreenPlayerPresented = NO; 275 [self _callFullscreenCallbackForUpdate:EXVideoFullscreenUpdatePlayerWillDismiss]; 276 [self _callFullscreenCallbackForUpdate:EXVideoFullscreenUpdatePlayerDidDismiss]; 277 } else if (viewBounds.size.height == screen.size.height && viewBounds.size.width == screen.size.width && !_fullscreenPlayerPresented) { 278 // Fullscreen player is being presented 279 _fullscreenPlayerPresented = YES; 280 [self _callFullscreenCallbackForUpdate:EXVideoFullscreenUpdatePlayerWillPresent]; 281 [self _callFullscreenCallbackForUpdate:EXVideoFullscreenUpdatePlayerDidPresent]; 282 } else { 283 return; 284 } 285 } else { 286 [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 287 } 288} 289 290#pragma mark - Imperative API 291 292- (void)setSource:(NSDictionary *)source 293 withStatus:(NSDictionary *)initialStatus 294 resolver:(EXPromiseResolveBlock)resolve 295 rejecter:(EXPromiseRejectBlock)reject 296{ 297 if (_data) { 298 [_statusToSet addEntriesFromDictionary:[_data getStatus]]; 299 [self _removeData]; 300 } 301 302 [self _removePlayer]; 303 304 if (initialStatus) { 305 [_statusToSet addEntriesFromDictionary:initialStatus]; 306 } 307 308 if (source == nil) { 309 if (resolve) { 310 resolve([EXAVPlayerData getUnloadedStatus]); 311 } 312 return; 313 } 314 315 NSMutableDictionary *statusToInitiallySet = [NSMutableDictionary dictionaryWithDictionary:_statusToSet]; 316 [_statusToSet removeAllObjects]; 317 318 EX_WEAKIFY(self); 319 320 void (^statusUpdateCallback)(NSDictionary *) = ^(NSDictionary *status) { 321 EX_ENSURE_STRONGIFY(self); 322 if (self.onStatusUpdate) { 323 self.onStatusUpdate(status); 324 } 325 }; 326 327 void (^errorCallback)(NSString *) = ^(NSString *error) { 328 EX_ENSURE_STRONGIFY(self); 329 [self _removeData]; 330 [self _removePlayer]; 331 [self _callErrorCallback:error]; 332 }; 333 334 _data = [[EXAVPlayerData alloc] initWithEXAV:_exAV 335 withSource:source 336 withStatus:statusToInitiallySet 337 withLoadFinishBlock:^(BOOL success, NSDictionary *successStatus, NSString *error) { 338 EX_ENSURE_STRONGIFY(self); 339 if (success) { 340 [self _updateForNewPlayer]; 341 if (resolve) { 342 resolve(successStatus); 343 } 344 } else { 345 [self _removeData]; 346 [self _removePlayer]; 347 if (reject) { 348 reject(@"E_VIDEO_NOTCREATED", error, nil); 349 } 350 [self _callErrorCallback:error]; 351 } 352 }]; 353 [_data setStatusUpdateCallback:statusUpdateCallback]; 354 [_data setErrorCallback:errorCallback]; 355 356 // Call onLoadStart on next run loop, otherwise it might not be set yet (if it is set at the same time as uri, via props) 357 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) 0), dispatch_get_main_queue(), ^{ 358 EX_ENSURE_STRONGIFY(self); 359 if (self.onLoadStart) { 360 self.onLoadStart(nil); 361 } 362 }); 363} 364 365- (void)setStatus:(NSDictionary *)status 366 resolver:(EXPromiseResolveBlock)resolve 367 rejecter:(EXPromiseRejectBlock)reject 368{ 369 if (status != nil) { 370 [_statusToSet addEntriesFromDictionary:status]; 371 } 372 [self _tryUpdateDataStatus:resolve rejecter:reject]; 373} 374 375- (void)setStatusFromPlaybackAPI:(NSDictionary *)status 376 resolver:(EXPromiseResolveBlock)resolve 377 rejecter:(EXPromiseRejectBlock)reject; 378{ 379 EX_WEAKIFY(self); 380 dispatch_async(_exAV.methodQueue, ^{ 381 EX_ENSURE_STRONGIFY(self); 382 [self setStatus:status resolver:resolve rejecter:reject]; 383 }); 384} 385 386- (void)replayWithStatus:(NSDictionary *)status 387 resolver:(EXPromiseResolveBlock)resolve 388 rejecter:(EXPromiseRejectBlock)reject 389{ 390 if (status != nil) { 391 [_statusToSet addEntriesFromDictionary:status]; 392 } 393 394 NSMutableDictionary *newStatus = [NSMutableDictionary dictionaryWithDictionary:_statusToSet]; 395 [_statusToSet removeAllObjects]; 396 397 [_data replayWithStatus:newStatus resolver:resolve rejecter:reject]; 398} 399 400- (void)setFullscreen:(BOOL)value 401 resolver:(EXPromiseResolveBlock)resolve 402 rejecter:(EXPromiseRejectBlock)reject 403{ 404 if (!_data) { 405 // Tried to set fullscreen for an unloaded component. 406 if (reject) { 407 reject(@"E_VIDEO_FULLSCREEN", @"Fullscreen encountered an error: video is not loaded.", nil); 408 } 409 return; 410 } else if (!_playerHasLoaded) { 411 // `setUri` has been called, but the video has not yet loaded. 412 if (_requestedFullscreenChangeRejecter) { 413 _requestedFullscreenChangeRejecter(@"E_VIDEO_FULLSCREEN", @"Received newer request, cancelling fullscreen mode change request.", nil); 414 } 415 416 _requestedFullscreenChange = value; 417 _requestedFullscreenChangeRejecter = reject; 418 _requestedFullscreenChangeResolver = resolve; 419 return; 420 } else { 421 EX_WEAKIFY(self); 422 if (value && !_fullscreenPlayerPresented && !_fullscreenPlayerViewController) { 423 _fullscreenPlayerViewController = [self _createNewPlayerViewController]; 424 425 // Resize mode must be set before layer is added 426 // to prevent video from being animated when `resizeMode` is `cover` 427 [self _updateNativeResizeMode]; 428 429 // Set presentation style to fullscreen 430 [_fullscreenPlayerViewController setModalPresentationStyle:UIModalPresentationFullScreen]; 431 432 // Find the nearest view controller 433 UIViewController *controller = [UIApplication sharedApplication].keyWindow.rootViewController; 434 UIViewController *presentedController = controller.presentedViewController; 435 while (presentedController && ![presentedController isBeingDismissed]) { 436 controller = presentedController; 437 presentedController = controller.presentedViewController; 438 } 439 440 _presentingViewController = controller; 441 [self _callFullscreenCallbackForUpdate:EXVideoFullscreenUpdatePlayerWillPresent]; 442 443 dispatch_async(dispatch_get_main_queue(), ^{ 444 EX_ENSURE_STRONGIFY(self); 445 self.fullscreenPlayerViewController.showsPlaybackControls = YES; 446 [self.presentingViewController presentViewController:self.fullscreenPlayerViewController animated:YES completion:^{ 447 EX_ENSURE_STRONGIFY(self); 448 self.fullscreenPlayerPresented = YES; 449 [self _callFullscreenCallbackForUpdate:EXVideoFullscreenUpdatePlayerDidPresent]; 450 if (resolve) { 451 resolve([self getStatus]); 452 } 453 }]; 454 }); 455 } else if (!value && _fullscreenPlayerPresented && !_fullscreenPlayerIsDismissing) { 456 [self videoPlayerViewControllerWillDismiss:_fullscreenPlayerViewController]; 457 458 dispatch_async(dispatch_get_main_queue(), ^{ 459 EX_ENSURE_STRONGIFY(self); 460 [self.presentingViewController dismissViewControllerAnimated:YES completion:^{ 461 EX_ENSURE_STRONGIFY(self); 462 [self videoPlayerViewControllerDidDismiss:self.fullscreenPlayerViewController]; 463 if (resolve) { 464 resolve([self getStatus]); 465 } 466 }]; 467 }); 468 } else if (value && !_fullscreenPlayerPresented && _fullscreenPlayerViewController && reject) { 469 // Fullscreen player should be presented, is being presented, but hasn't been presented yet. 470 reject(@"E_VIDEO_FULLSCREEN", @"Fullscreen player is already being presented. Await the first change request.", nil); 471 } else if (!value && _fullscreenPlayerIsDismissing && _fullscreenPlayerViewController && reject) { 472 // Fullscreen player should be dismissing, is already dismissing, but hasn't dismissed yet. 473 reject(@"E_VIDEO_FULLSCREEN", @"Fullscreen player is already being dismissed. Await the first change request.", nil); 474 } else if (!value && !_fullscreenPlayerPresented && _fullscreenPlayerViewController && reject) { 475 // Fullscreen player is being presented and we receive request to dismiss it. 476 reject(@"E_VIDEO_FULLSCREEN", @"Fullscreen player is being presented. Await the `present` request and then dismiss the player.", nil); 477 } else if (value && _fullscreenPlayerIsDismissing && _fullscreenPlayerViewController && reject) { 478 // Fullscreen player is being dismissed and we receive request to present it. 479 reject(@"E_VIDEO_FULLSCREEN", @"Fullscreen player is being dismissed. Await the `dismiss` request and then present the player again.", nil); 480 } else if (resolve) { 481 // Fullscreen is already appropriately set. 482 resolve([self getStatus]); 483 } 484 } 485} 486 487#pragma mark - Prop setters 488 489- (void)setSource:(NSDictionary *)source 490{ 491 if (![source isEqualToDictionary:_lastSetSource]) { 492 EX_WEAKIFY(self); 493 // ? Why dispatch to _exAV.methodQueue rather than remain on the main thread? 494 // ? Can lead to race conditions with Imperative API being sent on the main thread. 495 // ? I've made Imperative API dispatch to _exAV.methodQueue since I do not know 496 // ? the reason for it, but ultimately I believe Prop setters should run on the main thread 497 // ? rather than move Imperative API methods to _exAV.methodQueue. 498 dispatch_async(_exAV.methodQueue, ^{ 499 EX_ENSURE_STRONGIFY(self); 500 self.lastSetSource = source; 501 [self setSource:source withStatus:nil resolver:nil rejecter:nil]; 502 }); 503 } 504} 505 506- (NSDictionary *)source 507{ 508 return @{ 509 EXVideoSourceURIKeyPath: (_data != nil && _data.url != nil) ? _data.url.absoluteString : @"", 510 EXVideoSourceHeadersKeyPath: _data.headers 511 }; 512} 513 514- (void)setUseNativeControls:(BOOL)useNativeControls 515{ 516 _useNativeControls = useNativeControls; 517 if (!_playerHasLoaded) { 518 return; 519 } 520 521 EX_WEAKIFY(self); 522 dispatch_async(dispatch_get_main_queue(), ^{ 523 EX_ENSURE_STRONGIFY(self); 524 if (!self.playerHasLoaded) { 525 return; 526 } 527 if (self.useNativeControls) { 528 if (self.playerLayer) { 529 [self _removePlayerLayer]; 530 } 531 if (!self.playerViewController && self.data) { 532 self.playerViewController = [self _createNewPlayerViewController]; 533 if (@available(iOS 12, *)) { 534 // On iOS 12 or higher, use the AVPlayerViewControllerDelegate full-screen delegate methods: 535 // https://stackoverflow.com/a/58809976/3785358 536 } else { 537 // On iOS 11 or earlier, fallback to listening for changes to `videoBounds`. 538 // See https://stackoverflow.com/questions/36323259/detect-video-playing-full-screen-in-portrait-or-landscape/36388184#36388184 539 // and https://github.com/expo/expo/issues/1566 540 [self.playerViewController addObserver:self forKeyPath:EXVideoBoundsKeyPath options:NSKeyValueObservingOptionNew context:nil]; 541 } 542 // Resize mode must be set before layer is added 543 // to prevent video from being animated when `resizeMode` is `cover` 544 [self _updateNativeResizeMode]; 545 [self addSubview:self.playerViewController.view]; 546 } 547 } else { 548 if (self.playerViewController) { 549 [self _removePlayerViewController]; 550 } 551 if (!self.playerLayer) { 552 [self _usePlayerLayer]; 553 } 554 } 555 }); 556} 557 558- (void)setNativeResizeMode:(NSString*)mode 559{ 560 _nativeResizeMode = mode; 561 [self _updateNativeResizeMode]; 562} 563 564- (void)_updateNativeResizeMode 565{ 566 if (_useNativeControls) { 567 if (_playerViewController) { 568 [_playerViewController setVideoGravity:_nativeResizeMode]; 569 } 570 if (_fullscreenPlayerViewController) { 571 [_fullscreenPlayerViewController setVideoGravity:_nativeResizeMode]; 572 } 573 } else if (_playerLayer) { 574 [_playerLayer setVideoGravity:_nativeResizeMode]; 575 } 576} 577 578- (void)setStatus:(NSDictionary *)status 579{ 580 EX_WEAKIFY(self); 581 // ? Why dispatch to _exAV.methodQueue rather than remain on the main thread? 582 // ? Can lead to race conditions with Imperative API being sent on the main thread. 583 // ? I've made Imperative API dispatch to _exAV.methodQueue since I do not know 584 // ? the reason for it, but ultimately I believe Prop setters should run on the main thread 585 // ? rather than move Imperative API methods to _exAV.methodQueue. 586 dispatch_async(_exAV.methodQueue, ^{ 587 EX_ENSURE_STRONGIFY(self); 588 [self setStatus:status resolver:nil rejecter:nil]; 589 }); 590} 591 592- (NSDictionary *)getStatus 593{ 594 if (_data) { 595 return [_data getStatus]; 596 } else { 597 return [EXAVPlayerData getUnloadedStatus]; 598 } 599} 600 601#pragma mark - React View Management 602 603//- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex 604//{ 605// // We are early in the game and somebody wants to set a subview. 606// // That can only be in the context of playerViewController. 607// if (!_useNativeControls && !_playerLayer && !_playerViewController) { 608// [self setUseNativeControls:YES]; 609// } 610// 611// if (_useNativeControls && _playerViewController) { 612// [super insertReactSubview:view atIndex:atIndex]; 613// [view setFrame:self.bounds]; 614// [_playerViewController.contentOverlayView insertSubview:view atIndex:atIndex]; 615// } else { 616// RCTLogError(@"video cannot have any subviews"); 617// } 618//} 619 620//- (void)removeReactSubview:(UIView *)subview 621//{ 622// if (_useNativeControls) { 623// [super removeReactSubview:subview]; 624// [subview removeFromSuperview]; 625// } else { 626// RCTLogError(@"video cannot have any subviews"); 627// } 628//} 629 630- (void)layoutSubviews 631{ 632 [super layoutSubviews]; 633 if (_useNativeControls && _playerViewController) { 634 [_playerViewController.view setFrame:self.bounds]; 635 636 // also adjust all subviews of contentOverlayView 637 for (UIView* subview in _playerViewController.contentOverlayView.subviews) { 638 [subview setFrame:self.bounds]; 639 } 640 } else if (!_useNativeControls && _playerLayer) { 641 [CATransaction begin]; 642 [CATransaction setAnimationDuration:0]; 643 [_playerLayer setFrame:self.bounds]; 644 [CATransaction commit]; 645 } 646} 647 648- (void)removeFromSuperview 649{ 650 [self _removeData]; 651 [self _removePlayer]; 652 [super removeFromSuperview]; 653} 654 655#pragma mark - EXVideoPlayerViewControllerDelegate 656 657- (void)videoPlayerViewControllerWillDismiss:(AVPlayerViewController *)playerViewController 658{ 659 if (_fullscreenPlayerViewController == playerViewController && _fullscreenPlayerPresented && !_fullscreenPlayerIsDismissing) { 660 _fullscreenPlayerIsDismissing = YES; 661 [self _callFullscreenCallbackForUpdate:EXVideoFullscreenUpdatePlayerWillDismiss]; 662 } 663} 664 665- (void)videoPlayerViewControllerDidDismiss:(AVPlayerViewController *)playerViewController 666{ 667 if (_fullscreenPlayerViewController == playerViewController && _fullscreenPlayerPresented) { 668 _fullscreenPlayerIsDismissing = NO; 669 _fullscreenPlayerPresented = NO; 670 _presentingViewController = nil; 671 [self _removeFullscreenPlayerViewController]; 672 [self setUseNativeControls:_useNativeControls]; 673 [self _callFullscreenCallbackForUpdate:EXVideoFullscreenUpdatePlayerDidDismiss]; 674 } 675} 676 677#pragma mark - AVVideoPlayerViewControllerDelegate 678 679- (void)playerViewController:(AVPlayerViewController *)playerViewController 680willBeginFullScreenPresentationWithAnimationCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator 681{ 682 if (playerViewController == _playerViewController) { 683 _fullscreenPlayerPresented = YES; 684 [self _callFullscreenCallbackForUpdate:EXVideoFullscreenUpdatePlayerWillPresent]; 685 [self _callFullscreenCallbackForUpdate:EXVideoFullscreenUpdatePlayerDidPresent]; 686 } 687} 688 689- (void)playerViewController:(AVPlayerViewController *)playerViewController 690willEndFullScreenPresentationWithAnimationCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator 691{ 692 if (playerViewController == _playerViewController) { 693 _fullscreenPlayerPresented = NO; 694 [self _callFullscreenCallbackForUpdate:EXVideoFullscreenUpdatePlayerWillDismiss]; 695 [self _callFullscreenCallbackForUpdate:EXVideoFullscreenUpdatePlayerDidDismiss]; 696 } 697} 698 699#pragma mark - EXAVObject 700 701- (void)pauseImmediately 702{ 703 if (_data) { 704 [_data pauseImmediately]; 705 } 706} 707 708- (EXAVAudioSessionMode)getAudioSessionModeRequired 709{ 710 return _data == nil ? EXAVAudioSessionModeInactive : [_data getAudioSessionModeRequired]; 711} 712 713- (void)appDidForeground 714{ 715 if (_data) { 716 [_data appDidForeground]; 717 718 _playerViewController.player = _data.player; 719 _playerLayer.player = _data.player; 720 } 721} 722 723- (void)appDidBackgroundStayActive:(BOOL)stayActive 724{ 725 if (_data) { 726 [_data appDidBackgroundStayActive:stayActive]; 727 728 if (stayActive) { 729 _playerViewController.player = nil; 730 _playerLayer.player = nil; 731 } 732 } 733} 734 735- (void)handleAudioSessionInterruption:(NSNotification*)notification 736{ 737 if (_data) { 738 [_data handleAudioSessionInterruption:notification]; 739 } 740} 741 742- (void)handleMediaServicesReset:(void (^)(void))finishCallback 743{ 744 if (_data) { 745 if (_onLoadStart) { 746 _onLoadStart(nil); 747 } 748 [self _removePlayer]; 749 750 EX_WEAKIFY(self); 751 [_data handleMediaServicesReset:^{ 752 EX_STRONGIFY(self); 753 if (self) { 754 [self _updateForNewPlayer]; 755 } 756 if (finishCallback != nil) { 757 finishCallback(); 758 } 759 }]; 760 } 761} 762 763#pragma mark - NSObject Lifecycle 764 765- (void)dealloc 766{ 767 [_exAV unregisterVideoForAudioLifecycle:self]; 768 [_data pauseImmediately]; 769 [_exAV demoteAudioSessionIfPossible]; 770} 771 772@end 773 774