Search code examples
objective-cavplayertvosuitapgesturerecognizeravplayerviewcontroller

How to listen to Apple TV Remote in Objective-C?


This seems like it should be pretty straight-forward but I'm having trouble finding a working example, good documentation, or even many StackOverflow posts that are helpful.

I have a custom view which contains an AVPlayer, like so:

@implementation
{
    @private
    AVPlayerViewController *controller
}

- (id) init
{
    self = [super init];
    if (self)
    {
        controller = [[AVPlayerViewController alloc] init];
        controller.view.frame = self.view.bounds;
        [self.view addSubview:controller.view];
    }
    return self;
}

@end

(I have a few other views like a message that overlays the player, a poster that I display while swapping videos, etc - but this is the basic setup)

When I integrated the IMA SDK, I started to have issues. If you press the pause button on the remote control during an ad, it pauses the ad just fine. But if you press the pause button again it doesn't unpause the ad, but instead unpauses my content player behind the ad. I don't hear any audio, but I know the content player was unpaused because I have ID3 metadata in my video and an NSLog() statement when I hit it, and I begin to see these logs. If I press the pause button again, the logs pause. I press it a fourth time, the logs start up again.

To try and fix this I wanted to bind a listener to the remote's play/pause button and make sure that if I was playing an ad then the ad was resumed, not the content. So I tried adding the following to my init method on my view:

        UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self.view action:@selector(tapped:)];
        [tapRecognizer setAllowedPressTypes:@[ [NSNumber numberWithInt:UIPressTypePlayPause] ]];
        [self.view addGestureRecognizer:tapRecognizer];

I then created the following method:

- (void) tapped: (UITapGestureRecognizer *) sender
{
    NSLog(@"Tapped");
}

This isn't being called. I'm pretty confident I made a simple mistake, but the documentation isn't very clear, so I'm not sure what I should be doing instead. The official documentation on detecting button presses uses Swift and says:

    let tapRecognizer = UITapGestureRecognizer(target: self, action: "tapped:")
    tapRecognizer.allowedPressTypes = [NSNumber(integer: UIPressType.PlayPause.rawValue)];
    self.view.addGestureRecognizer(tapRecognizer)

I believe I translated those three lines well. The documentation then doesn't show what the tapped method should look like, but instead goes on a tangent about working with low-level event handling. So to get the appropriate method signature I looked at the documentation on UITagGestureRecognizer which had the following (Swift) example for writing a handler:

func handleTap(sender: UITapGestureRecognizer) {
    if sender.state == .ended {
        // handling code
    }
}

This is why I went with - (void) tapped: (UITapGestureRecognizer *) sender

Still, after all of that, it's not working.

Quick Update

I tried replacing:

initWithTarget:self.view

With:

initWithTarget:controller.view

And:

self.view addGestureRecognizer

With:

controller.view addGestureRecognizer

And this time it looks like something actually happened when I pressed the play/pause button. The app crashed and Xcode gave me the following error:

2019-12-17 12:16:50.937007-0500 Example tvOS App[381:48776] -[_AVPlayerViewControllerContainerView tapped:]: unrecognized selector sent to instance 0x10194e060

So it seems like (correct me if I'm wrong):

  • The AVPlayerViewController has the focus, not my view
  • The gesture recognizer calls the selector on whatever class you register it to, rather than the class that did the registering

So I guess an alternative question to my original would be: How do I allow my class to handle a gesture on some other class (e.g. AVPlayerViewController)?


Solution

  • The target does not need to equal the view that the gesture recognizer is attached to. You can set the target to MyView but still attach the gesture recognizer to controller.view

    To work around the unrecognized selector crash, you need to make sure you're providing the correct object as the target for your gesture recognizer. The way UIGestureRecognizer works is that when the gesture is recognized, it will invoke the given selector on the given target object. In other words, when gesture fires, it's going to perform the equivalent of:

    [target selector:self];
    

    (You seem to be treating the target as the view that the gesture will be attached to, which isn't how it works)

    So if you implemented tapped: on your class MyView, then the target you pass to the gesture recognizer initializer should be an instance of MyView. You probably want to provide self, not self.view.