FullScreenVideoPlayer.mm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. #import <UIKit/UIKit.h>
  2. #import <MediaPlayer/MediaPlayer.h>
  3. #import <AVFoundation/AVFoundation.h>
  4. #import <AVKit/AVKit.h>
  5. #import <UIKit/UIGestureRecognizerSubclass.h>
  6. #include "UnityAppController.h"
  7. #include "UI/UnityView.h"
  8. #include "UI/UnityViewControllerBase.h"
  9. #include "UI/OrientationSupport.h"
  10. #include "UI/UnityAppController+ViewHandling.h"
  11. #include "Unity/ObjCRuntime.h"
  12. #include "Unity/VideoPlayer.h"
  13. #include "PluginBase/UnityViewControllerListener.h"
  14. @interface UICancelGestureRecognizer : UITapGestureRecognizer
  15. @end
  16. #if PLATFORM_IOS
  17. @interface MPVideoPlayback : NSObject<UnityViewControllerListener, UIGestureRecognizerDelegate>
  18. {
  19. MPMoviePlayerController* moviePlayer;
  20. UIColor* bgColor;
  21. MPMovieControlStyle controlMode;
  22. MPMovieScalingMode scalingMode;
  23. BOOL cancelOnTouch;
  24. }
  25. - (id)initAndPlay:(NSURL*)url bgColor:(UIColor*)color controls:(MPMovieControlStyle)control scaling:(MPMovieScalingMode)scaling cancelOnTouch:(BOOL)cot;
  26. - (void)actuallyStartTheMovie:(NSURL*)url;
  27. - (void)moviePlayBackDidFinish:(NSNotification*)notification;
  28. - (void)finish;
  29. @end
  30. #endif
  31. @interface AVKitVideoPlayback : NSObject<VideoPlayerDelegate, UIViewControllerTransitioningDelegate, UIGestureRecognizerDelegate>
  32. {
  33. AVPlayerViewController* videoViewController;
  34. VideoPlayer* videoPlayer;
  35. UIColor* bgColor;
  36. const NSString* videoGravity;
  37. BOOL showControls;
  38. BOOL cancelOnTouch;
  39. }
  40. + (BOOL)IsSupported;
  41. - (void)onPlayerReady;
  42. - (void)onPlayerDidFinishPlayingVideo;
  43. - (id)initAndPlay:(NSURL*)url bgColor:(UIColor*)color showControls:(BOOL)controls videoGravity:(const NSString*)scaling cancelOnTouch:(BOOL)cot;
  44. - (void)actuallyStartTheMovie:(NSURL*)url;
  45. - (void)finish;
  46. @end
  47. #if PLATFORM_IOS
  48. static MPVideoPlayback* _MPVideoPlayback = nil;
  49. #endif
  50. static AVKitVideoPlayback* _AVKitVideoPlayback = nil;
  51. #if PLATFORM_IOS
  52. @implementation MPVideoPlayback
  53. - (id)initAndPlay:(NSURL*)url bgColor:(UIColor*)color controls:(MPMovieControlStyle)control scaling:(MPMovieScalingMode)scaling cancelOnTouch:(BOOL)cot
  54. {
  55. if ((self = [super init]))
  56. {
  57. UnityPause(1);
  58. bgColor = color;
  59. controlMode = control;
  60. scalingMode = scaling;
  61. cancelOnTouch = cot;
  62. UnityRegisterViewControllerListener((id<UnityViewControllerListener>)self);
  63. [self performSelector: @selector(actuallyStartTheMovie:) withObject: url afterDelay: 0];
  64. }
  65. return self;
  66. }
  67. - (void)dealloc
  68. {
  69. [self finish];
  70. }
  71. - (void)actuallyStartTheMovie:(NSURL*)url
  72. {
  73. @autoreleasepool
  74. {
  75. moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL: url];
  76. if (moviePlayer == nil)
  77. return;
  78. UIView* bgView = [moviePlayer backgroundView];
  79. bgView.backgroundColor = bgColor;
  80. [moviePlayer prepareToPlay];
  81. moviePlayer.controlStyle = controlMode;
  82. moviePlayer.scalingMode = scalingMode;
  83. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(moviePlayBackDidFinish:) name: MPMoviePlayerPlaybackDidFinishNotification object: moviePlayer];
  84. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(moviePlayBackDidFinish:) name: MPMoviePlayerDidExitFullscreenNotification object: moviePlayer];
  85. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(moviePlayBackSourceTypeAvailable:) name: MPMovieSourceTypeAvailableNotification object: moviePlayer];
  86. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(moviePlayBackMediaTypesAvailable:) name: MPMovieMediaTypesAvailableNotification object: moviePlayer];
  87. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(moviePlayBackNaturalSizeAvailable:) name: MPMovieNaturalSizeAvailableNotification object: moviePlayer];
  88. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(audioRouteChanged:) name: AVAudioSessionRouteChangeNotification object: nil];
  89. moviePlayer.view.frame = GetAppController().rootView.bounds;
  90. UIView* videoView = moviePlayer.view;
  91. if (cancelOnTouch)
  92. {
  93. UICancelGestureRecognizer *cancelTouch = [[UICancelGestureRecognizer alloc] initWithTarget: self action: @selector(handleTap:)];
  94. cancelTouch.delegate = self;
  95. [moviePlayer.view addGestureRecognizer: cancelTouch];
  96. }
  97. [GetAppController().rootView addSubview: videoView];
  98. }
  99. }
  100. - (void)moviePlayBackDidFinish:(NSNotification*)notification
  101. {
  102. [self finish];
  103. }
  104. - (void)moviePlayBackSourceTypeAvailable:(NSNotification*)notification
  105. {
  106. if (moviePlayer.movieSourceType == MPMovieSourceTypeUnknown)
  107. {
  108. [self finish];
  109. }
  110. }
  111. - (void)moviePlayBackMediaTypesAvailable:(NSNotification*)notification
  112. {
  113. if (moviePlayer.movieMediaTypes == MPMovieMediaTypeMaskNone)
  114. {
  115. [self finish];
  116. }
  117. }
  118. - (void)moviePlayBackNaturalSizeAvailable:(NSNotification*)notification
  119. {
  120. CGSize screenSize = GetAppController().rootView.bounds.size;
  121. BOOL ret = [VideoPlayer CheckScalingModeAspectFill: moviePlayer.naturalSize screenSize: screenSize];
  122. if (ret == YES && moviePlayer.scalingMode == MPMovieScalingModeAspectFit)
  123. {
  124. moviePlayer.scalingMode = MPMovieScalingModeAspectFill;
  125. }
  126. }
  127. - (void)audioRouteChanged:(NSNotification*)notification
  128. {
  129. // not really cool:
  130. // it might happen that due to audio route changing ios can pause playback
  131. // alas at this point playbackRate might be not yet changed, so we just resume always
  132. if (moviePlayer)
  133. [moviePlayer play];
  134. }
  135. - (void)viewDidLayoutSubviews:(NSNotification *)notification
  136. {
  137. moviePlayer.view.frame = GetAppController().rootView.bounds;
  138. }
  139. - (void)handleTap:(UIGestureRecognizer *)sender
  140. {
  141. if (sender.state == UIGestureRecognizerStateEnded)
  142. [self finish];
  143. }
  144. - (void)finish
  145. {
  146. @synchronized(self)
  147. {
  148. if (moviePlayer)
  149. {
  150. // remove notifications right away to avoid recursively calling finish from callback
  151. [[NSNotificationCenter defaultCenter] removeObserver: self];
  152. }
  153. [moviePlayer.view removeFromSuperview];
  154. [moviePlayer pause];
  155. [moviePlayer stop];
  156. moviePlayer = nil;
  157. UnityUnregisterViewControllerListener((id<UnityViewControllerListener>)self);
  158. // On simulator _MPVideoPlayback is collected as soon as it is set to nil, make sure
  159. // it's not used via self
  160. _MPVideoPlayback = nil;
  161. if (UnityIsPaused())
  162. UnityPause(0);
  163. }
  164. }
  165. @end
  166. #endif
  167. @implementation AVKitVideoPlayback
  168. static Class _AVPlayerViewControllerClass = nil;
  169. #if PLATFORM_IOS
  170. static void AVPlayerViewController_SetAllowsPictureInPicturePlayback_OldIOSImpl(id self_, SEL _cmd, BOOL allow) {}
  171. static NSUInteger supportedInterfaceOrientations_DefaultImpl(id self_, SEL _cmd)
  172. {
  173. return GetAppController().rootViewController.supportedInterfaceOrientations;
  174. }
  175. static bool prefersStatusBarHidden_DefaultImpl(id self_, SEL _cmd)
  176. {
  177. if (_AVKitVideoPlayback) // video is still playing
  178. return _AVKitVideoPlayback->videoViewController.showsPlaybackControls ? NO : YES;
  179. else // video has beed stopped
  180. return GetAppController().rootViewController.prefersStatusBarHidden;
  181. }
  182. #endif
  183. + (void)initialize
  184. {
  185. if (self == [AVKitVideoPlayback class])
  186. {
  187. // NB: AVPlayerViewController was added along with AVKit so we dont need to handle "bundle loaded but class is not there"
  188. _AVPlayerViewControllerClass = [[NSBundle bundleWithPath: @"/System/Library/Frameworks/AVKit.framework"] classNamed: @"AVPlayerViewController"];
  189. if (!_AVPlayerViewControllerClass)
  190. return;
  191. #if PLATFORM_IOS
  192. if (![_AVPlayerViewControllerClass instancesRespondToSelector: @selector(setAllowsPictureInPicturePlayback:)])
  193. {
  194. class_replaceMethod(_AVPlayerViewControllerClass, @selector(setAllowsPictureInPicturePlayback:),
  195. (IMP)&AVPlayerViewController_SetAllowsPictureInPicturePlayback_OldIOSImpl, AVPlayerViewController_setAllowsPictureInPicturePlayback_Enc);
  196. }
  197. class_replaceMethod(_AVPlayerViewControllerClass, @selector(supportedInterfaceOrientations), (IMP)&supportedInterfaceOrientations_DefaultImpl, UIViewController_supportedInterfaceOrientations_Enc);
  198. class_replaceMethod(_AVPlayerViewControllerClass, @selector(prefersStatusBarHidden), (IMP)&prefersStatusBarHidden_DefaultImpl, UIViewController_prefersStatusBarHidden_Enc);
  199. #endif
  200. }
  201. }
  202. + (BOOL)IsSupported
  203. {
  204. return _AVPlayerViewControllerClass != nil;
  205. }
  206. - (id)initAndPlay:(NSURL*)url bgColor:(UIColor*)color showControls:(BOOL)controls videoGravity:(const NSString*)scaling cancelOnTouch:(BOOL)cot
  207. {
  208. if ((self = [super init]))
  209. {
  210. UnityPause(1);
  211. showControls = controls;
  212. videoGravity = scaling;
  213. bgColor = color;
  214. cancelOnTouch = cot;
  215. [self performSelector: @selector(actuallyStartTheMovie:) withObject: url afterDelay: 0];
  216. }
  217. return self;
  218. }
  219. - (void)dealloc
  220. {
  221. [self finish];
  222. }
  223. - (void)actuallyStartTheMovie:(NSURL*)url
  224. {
  225. @autoreleasepool
  226. {
  227. videoViewController = [[_AVPlayerViewControllerClass alloc] init];
  228. videoViewController.showsPlaybackControls = showControls;
  229. videoViewController.view.backgroundColor = bgColor;
  230. videoViewController.videoGravity = (NSString*)videoGravity;
  231. videoViewController.transitioningDelegate = self;
  232. #if PLATFORM_IOS
  233. videoViewController.allowsPictureInPicturePlayback = NO;
  234. #endif
  235. #if PLATFORM_TVOS
  236. // In tvOS clicking Menu button while video is playing will exit the app. So when
  237. // app disables exiting to menu behavior, we need to catch the click and ignore it.
  238. if (!UnityGetAppleTVRemoteAllowExitToMenu())
  239. {
  240. UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(handleTap:)];
  241. tapRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
  242. [videoViewController.view addGestureRecognizer: tapRecognizer];
  243. }
  244. #endif
  245. if (cancelOnTouch)
  246. {
  247. UICancelGestureRecognizer *cancelTouch = [[UICancelGestureRecognizer alloc] initWithTarget: self action: @selector(handleTap:)];
  248. cancelTouch.delegate = self;
  249. [videoViewController.view addGestureRecognizer: cancelTouch];
  250. }
  251. videoPlayer = [[VideoPlayer alloc] init];
  252. videoPlayer.delegate = self;
  253. [videoPlayer loadVideo: url];
  254. }
  255. }
  256. - (void)handleTap:(UITapGestureRecognizer*)sender
  257. {
  258. if (cancelOnTouch && (sender.state == UIGestureRecognizerStateEnded))
  259. [self finish];
  260. }
  261. - (void)onPlayerReady
  262. {
  263. videoViewController.player = videoPlayer.player;
  264. CGSize screenSize = GetAppController().rootView.bounds.size;
  265. BOOL ret = [VideoPlayer CheckScalingModeAspectFill: videoPlayer.videoSize screenSize: screenSize];
  266. if (ret == YES && [videoViewController.videoGravity isEqualToString: AVLayerVideoGravityResizeAspect] == YES)
  267. {
  268. videoViewController.videoGravity = AVLayerVideoGravityResizeAspectFill;
  269. }
  270. [videoPlayer playVideoPlayer];
  271. #if PLATFORM_TVOS
  272. GetAppController().window.rootViewController = videoViewController;
  273. #else
  274. UIViewController *viewController = [GetAppController() topMostController];
  275. if ([viewController isEqual: videoViewController] == NO && [videoViewController isBeingPresented] == NO)
  276. {
  277. [viewController presentViewController: videoViewController animated: NO completion: nil];
  278. }
  279. #endif
  280. }
  281. - (void)onPlayerDidFinishPlayingVideo
  282. {
  283. [self finish];
  284. }
  285. - (void)onPlayerTryResume
  286. {
  287. if (![videoPlayer isPlaying])
  288. [videoPlayer resume];
  289. }
  290. - (void)onPlayerError:(NSError*)error
  291. {
  292. [self finish];
  293. }
  294. - (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
  295. {
  296. if ([dismissed isEqual: videoViewController] == YES)
  297. {
  298. [self finish];
  299. }
  300. return nil;
  301. }
  302. - (void)finish
  303. {
  304. @synchronized(self)
  305. {
  306. #if PLATFORM_TVOS
  307. GetAppController().window.rootViewController = GetAppController().rootViewController;
  308. #else
  309. UIViewController *viewController = [GetAppController() topMostController];
  310. if ([viewController isEqual: videoViewController] == YES && [viewController isBeingDismissed] == NO)
  311. {
  312. [viewController dismissViewControllerAnimated: NO completion: nil];
  313. }
  314. #endif
  315. [videoPlayer unloadPlayer];
  316. videoPlayer = nil;
  317. videoViewController = nil;
  318. _AVKitVideoPlayback = nil;
  319. #if PLATFORM_TVOS
  320. UnityCancelTouches();
  321. #endif
  322. if (UnityIsPaused())
  323. UnityPause(0);
  324. }
  325. }
  326. @end
  327. @implementation UICancelGestureRecognizer
  328. //instead of having lots of UITapGestureRecognizers with different finger numbers
  329. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  330. {
  331. [self setState: UIGestureRecognizerStateRecognized];
  332. }
  333. @end
  334. extern "C" void UnityPlayFullScreenVideo(const char* path, const float* color, unsigned controls, unsigned scaling)
  335. {
  336. const BOOL cancelOnTouch[] = { NO, NO, YES, NO };
  337. UIColor* bgColor = [UIColor colorWithRed: color[0] green: color[1] blue: color[2] alpha: color[3]];
  338. const bool isURL = ::strstr(path, "://") != 0;
  339. NSURL* url = nil;
  340. if (isURL)
  341. {
  342. url = [NSURL URLWithString: [NSString stringWithUTF8String: path]];
  343. }
  344. else
  345. {
  346. NSString* relPath = path[0] == '/' ? [NSString stringWithUTF8String: path] : [NSString stringWithFormat: @"Data/Raw/%s", path];
  347. NSString* fullPath = [[NSBundle mainBundle].bundlePath stringByAppendingPathComponent: relPath];
  348. url = [NSURL fileURLWithPath: fullPath];
  349. }
  350. // first try AVKit
  351. {
  352. const BOOL showControls[] = { YES, YES, NO, NO };
  353. const NSString* videoGravity[] =
  354. {
  355. AVLayerVideoGravityResizeAspectFill, // ???
  356. AVLayerVideoGravityResizeAspect,
  357. AVLayerVideoGravityResizeAspectFill,
  358. AVLayerVideoGravityResize,
  359. };
  360. if ([AVKitVideoPlayback IsSupported])
  361. {
  362. if (_AVKitVideoPlayback)
  363. [_AVKitVideoPlayback finish];
  364. _AVKitVideoPlayback = [[AVKitVideoPlayback alloc] initAndPlay: url bgColor: bgColor
  365. showControls: showControls[controls] videoGravity: videoGravity[scaling] cancelOnTouch: cancelOnTouch[controls]
  366. ];
  367. return;
  368. }
  369. }
  370. // MediaPlayer only if AVKit is not supported (old ios)
  371. #if PLATFORM_IOS
  372. {
  373. const MPMovieControlStyle controlMode[] =
  374. {
  375. MPMovieControlStyleFullscreen,
  376. MPMovieControlStyleEmbedded,
  377. MPMovieControlStyleNone,
  378. MPMovieControlStyleNone,
  379. };
  380. const MPMovieScalingMode scalingMode[] =
  381. {
  382. MPMovieScalingModeNone,
  383. MPMovieScalingModeAspectFit,
  384. MPMovieScalingModeAspectFill,
  385. MPMovieScalingModeFill,
  386. };
  387. if (_MPVideoPlayback)
  388. [_MPVideoPlayback finish];
  389. _MPVideoPlayback = [[MPVideoPlayback alloc] initAndPlay: url bgColor: bgColor
  390. controls: controlMode[controls] scaling: scalingMode[scaling] cancelOnTouch: cancelOnTouch[controls]
  391. ];
  392. }
  393. #endif
  394. }
  395. extern "C" void UnityStopFullScreenVideoIfPlaying()
  396. {
  397. if (_AVKitVideoPlayback)
  398. [_AVKitVideoPlayback finish];
  399. #if PLATFORM_IOS
  400. if (_MPVideoPlayback)
  401. [_MPVideoPlayback finish];
  402. #endif
  403. }
  404. extern "C" int UnityIsFullScreenPlaying()
  405. {
  406. #if PLATFORM_IOS
  407. return _MPVideoPlayback || _AVKitVideoPlayback ? 1 : 0;
  408. #else
  409. return _AVKitVideoPlayback ? 1 : 0;
  410. #endif
  411. }
  412. extern "C" void TryResumeFullScreenVideo()
  413. {
  414. if (_AVKitVideoPlayback)
  415. [_AVKitVideoPlayback onPlayerTryResume];
  416. }