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