问题
I created an AVPlayerViewController
and an attached AVPlayer
in the viewDidAppear
method of a custom UIViewController
. However, when I press the "Done" button my custom view controller is dismissed automatically.
I would like to intercept this action in order to use my own unwind Segue, but I'm not sure how to do this. I've found examples for MPMoviePlayerViewController
but not AVPlayerViewController
.
The code I found for MPMoviePlayerViewController
is below:
- (void)playVideo:(NSString *)aVideoUrl {
// Initialize the movie player view controller with a video URL string
MPMoviePlayerViewController *playerVC = [[[MPMoviePlayerViewController alloc] initWithContentURL:[NSURL URLWithString:aVideoUrl]] autorelease];
// Remove the movie player view controller from the "playback did finish" notification observers
[[NSNotificationCenter defaultCenter] removeObserver:playerVC
name:MPMoviePlayerPlaybackDidFinishNotification
object:playerVC.moviePlayer];
// Register this class as an observer instead
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(movieFinishedCallback:)
name:MPMoviePlayerPlaybackDidFinishNotification
object:playerVC.moviePlayer];
// Set the modal transition style of your choice
playerVC.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
// Present the movie player view controller
[self presentModalViewController:playerVC animated:YES];
// Start playback
[playerVC.moviePlayer prepareToPlay];
[playerVC.moviePlayer play];
}
- (void)movieFinishedCallback:(NSNotification *)aNotification {
// Obtain the reason why the movie playback finished
NSNumber *finishReason = [[aNotification userInfo] objectForKey:MPMoviePlayerPlaybackDidFinishReasonUserInfoKey];
// Dismiss the view controller ONLY when the reason is not "playback ended"
if ([finishReason intValue] != MPMovieFinishReasonPlaybackEnded) {
MPMoviePlayerController *moviePlayer = [aNotification object];
// Remove this class from the observers
[[NSNotificationCenter defaultCenter] removeObserver:self
name:MPMoviePlayerPlaybackDidFinishNotification
object:moviePlayer];
// Dismiss the view controller
[self dismissModalViewControllerAnimated:YES];
}
}
I asked Apple about this problem and they've replied as follows:
Thank you for contacting Apple Developer Technical Support (DTS). Our engineers have reviewed your request and have concluded that there is no supported way to achieve the desired functionality given the currently shipping system configurations.
回答1:
I subclassed AVPlayerViewController and am posting a notification from viewWillDisappear to indicate dismissing of AVPlayerViewController.
- (void) viewWillDisappear:(BOOL)animated {
[[NSNotificationCenter defaultCenter] postNotificationName:kPlayerViewDismissedNotification object:nil];
[super viewWillDisappear:animated];
}
This might not be 100% correct (as it would fail if you have another view being displayed over AVPlayerViewController), but it worked for me as AVPlayerViewController is always at the top of the stack.
回答2:
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.
回答3:
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()
}
}
回答4:
You could do it by subclassing AVPLayerViewController. But there is some undefined behaviour due to this. (Given in Apple docs. See below) I also tried subclassing AVPlayerViewController and I faced Memory issue.
According to the Apple Docs:
Do not subclass AVPlayerViewController. Overriding this class’s methods is unsupported and results in undefined behavior.
Currently, they do not offer a callback when the AVPlayerViewController is dismissed. See Apple developer forum:
- Thread1
- Thread2
In thread1 Apple guy says:
I still believe that managing the exit of the AVPlayerViewController instance manually by utilizing the recommended gesture method above would be the most reliable way to know that the player view controller has been dismissed, since you'll be in charge of its dismissal at that point
Hope it helps!
Well, there is a fix to the problem. You can add a button as a subview of AVPlayerViewController. By doing so, you could intercept done button tap gesture.
回答5:
Since there don't seem to be any perfect answers here, a workaround you can use for some situations is to monitor whether the AVPlayer is still playing and set an observer in case it closes automatically after being played all the way through.
var player:AVPlayer = AVPlayer()
var videoPlayTimer:NSTimer = NSTimer()
func playVideo(action: UIAlertAction) -> Void {
player = AVPlayer(URL: NSURL(fileURLWithPath: myFilePath))
player.actionAtItemEnd = .None
let playerController = AVPlayerViewController()
playerController.player = player
self.presentViewController(playerController, animated: true) {
self.player.play()
self.monitorVideoPlayStatus()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(MyViewController.onVideoEnd(_:)), name: AVPlayerItemDidPlayToEndTimeNotification, object: self.player.currentItem)
}
}
//setting a timer to monitor video play status in case it is closed by user
func monitorVideoPlayStatus(){
if ((player.rate != 0) && (player.error == nil)) {
NSLog("player is playing")
videoPlayTimer = NSTimer.after(0.5, monitorVideoPlayStatus)
} else {
NSLog("player is NOT playing")
onVideoEnd()
}
}
//will be called when video plays all the way through
func onVideoEnd(note: NSNotification){
NSLog("onVideoEnd")
onVideoEnd()
//canceling video play monitor
videoPlayTimer.invalidate()
}
func onVideoEnd(){
NSLog("finished playing video")
NSNotificationCenter.defaultCenter().removeObserver(self, name: AVPlayerItemDidPlayToEndTimeNotification, object: nil)
//*******
//DO WHATEVER YOU WANT AFTER VIDEO HAS ENDED RIGHT HERE
//*******
}
回答6:
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");
}
回答7:
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.
回答8:
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:
- timers: use as last resort only, don't clutter your code with timers
- use property observation to know when
AVPlayer
'srate
becomes0.0f
and checkAVPlayerViewController
'sisBeingDismissed
: clever, but fails when player is paused beforehand (doing it with timer instead works, of course) - use
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. - adding another action to the Close button: I used this solution for a long time successfully on iOS 8-13, but it suddenly stopped working after I switched from Xcode 9 to 10 (i.e. raised iOS SDK used for building from 11 to 12)
回答9:
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)
回答10:
One way is to add additional action to the existing "Done" button on the AVPlayerViewController by searching the respective UIButton inside the subviews of AVPlayerViewController. Once the button is found, add a custom action using addTarget:action:forControlEvents:
At least this works for me.
- (UIButton*)findButtonOnView:(UIView*)view withText:(NSString*)text
{
__block UIButton *retButton = nil;
[view.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if([obj isKindOfClass:[UIButton class]]) {
UIButton *button = (UIButton*)obj;
if([button.titleLabel.text isEqualToString:text]) {
retButton = button;
*stop = YES;
}
}
else if([obj isKindOfClass:[UIView class]]) {
retButton = [self findButtonOnView:obj withText:text];
if(retButton) {
*stop = YES;
}
}
}];
return retButton;
}
- (void)showPlayer:(AVPlayer*)player
{
AVPlayerViewController *vc = [[AVPlayerViewController alloc] init];
vc.player = player;
[self presentViewController:vc animated:NO completion:^{
UIButton *doneButton = [self findButtonOnView:vc.view withText:@"Done"];
[doneButton addTarget:self action:@selector(doneAction:) forControlEvents:UIControlEventTouchUpInside];
[vc.player play];
}];
}
- (void)doneAction:(UIButton*)button
{
// perform any action required here
}
来源:https://stackoverflow.com/questions/28671578/how-do-i-intercept-tapping-of-the-done-button-in-avplayerviewcontroller