I created an AVPlayerViewController
and an attached AVPlayer
in the viewDidAppear
method of a custom UIViewController
. Ho
The fact that Apple provides no built-in way to handle the Done button is disappointing.
I didn't feel like inheriting from AVPlayerViewController, because it isn't supported by Apple and will probably open up a can of worms in one of the next iOS updates.
My workaround is to have a timer fire every 200 msec and check for the following condition:
if (playerVC.player.rate == 0 &&
(playerVC.isBeingDismissed || playerVC.nextResponder == nil)) {
// Handle user Done button click and invalidate timer
}
The player's rate
property of 0 indicates that the video is no longer playing. And if the view controller is being dismissed or already dismissed, we can safely assume that the user clicked the Done button.
I solved by keeping a weak reference to the AVPlayerViewController instance, and monitoring with a timer where the reference change to nil.
private weak var _playerViewController : AVPlayerViewController? // global reference
...
...
let playerController = AVPlayerViewController() // local reference
...
self.present(playerController, animated: true) { [weak self] in
playerController.player?.play()
self?._playerViewController = playerController
// schedule a timer to intercept player dismissal
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] (timer) in
if self?.playerViewController == nil {
// player vc dismissed
timer.invalidate()
}
}
Can't believe nobody introduced the most obvious solution: just fire a notification or do whatever you need in AVPlayerViewController
's -dealloc
method. Just don't hold a strong
reference to it, otherwise -dealloc
won't be called.
There's nothing wrong with subclassing AVPlayerViewController
(even if docs say otherwise), if you don't break internal logic (e.g. not calling super
from overridden methods). I've been using the following code for years (since iOS 8 came out) without any runtime or AppStore submission issues:
@interface VideoPlayerViewController : AVPlayerViewController
@end
@implementation VideoPlayerViewController
- (UIInterfaceOrientationMask)supportedInterfaceOrientations { return UIInterfaceOrientationMaskLandscape; }
@end
// now use VideoPlayerViewController instead of AVPlayerViewController
So, to answer the question, just add this to AVPlayerViewController
subclass:
- (void)dealloc {
// player was dismissed and is going to die, do any cleanup now
}
If you are really afraid to subclass, then use smart technique by attaching an associated object to AVPlayerViewController
, whose -dealloc
you can control, see here: https://stackoverflow.com/a/19344475
To comment on other solutions:
AVPlayer
's rate
becomes 0.0f
and check AVPlayerViewController
's isBeingDismissed
: clever, but fails when player is paused beforehand (doing it with timer instead works, of course)AVPlayerViewController
's -viewWillDisappear:
: might fail when something is presented on top of the player, e.g. built-in subtitle selector. A similar solution would be checking in -viewWillAppear:
of the presenting view controller if a player was presented before and should work 100% of the time.If you have a view controller A that presents a AVPlayerViewController
, you could probably check in viewDidAppear/viewWillAppear inside VC A. Whenever those are called, at least we would know that the AVPlayerViewController is no longer shown and thus should not be playing.
Here is code how I managed to detect clicks.
In application delegate class. But it detects every buttons find in that view. You can add some control, check title something like that.
-(UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window{
if ([window.rootViewController isKindOfClass:[AVPlayerViewController class]]) {
return UIInterfaceOrientationMaskAll;
}
else if(window.rootViewController.presentedViewController != nil)
{
if ([window.rootViewController.presentedViewController isKindOfClass:[AVPlayerViewController class]] || [window.rootViewController.presentedViewController isKindOfClass:NSClassFromString(@"AVFullScreenViewController")]) {
if ([window.rootViewController.presentedViewController isKindOfClass:NSClassFromString(@"AVFullScreenViewController")]) {
[self findAllButton:window.rootViewController.presentedViewController.view];
}
return UIInterfaceOrientationMaskAll;
}
}
[[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortrait) forKey:@"orientation"];
[[UIApplication sharedApplication] setStatusBarHidden:NO];
return UIInterfaceOrientationMaskPortrait;
}
-(void)findAllButton:(UIView*)view{
for (UIView *subV in view.subviews) {
if ([subV isKindOfClass:[UIButton class]]) {
NSLog(@"%@",[((UIButton*)subV) titleForState:UIControlStateNormal]);
[((UIButton*)subV) addTarget:self action:@selector(doneButtonCliked) forControlEvents:UIControlEventTouchUpInside];
}
else{
[self findAllButton:subV];
}
}
}
-(IBAction)doneButtonCliked{
NSLog(@"DONECLICK");
}
I am not sure if this helps your usecase, but if you can target iOS 12+, AVPlayerViewController does provide a delegate property/protocol (AVPlayerViewControllerDelegate
) with a few related delegate methods that might help (see below):
Particularly on iOS 12+:
func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator)
On tvOS 11+:
func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool
func playerViewControllerWillBeginDismissalTransition(_ playerViewController: AVPlayerViewController)
func playerViewControllerDidEndDismissalTransition(_ playerViewController: AVPlayerViewController)