How to manage focus handling in AVPlayerViewController with custom controls? - tvos

I try to add a skip-intro-button (like Netflix) to the AVPlayerViewController of a tvOS movie app. I added it as subview to the contentOverlayView and when I let it appear I force giving it the focus with preferredFocusEnvironments. Everything's fine...
Until the user navigates somewhere else (e.g. seek bar or video asset info view). The button then loses focus (expected) and never can be focused again by user interaction.
I tried to:
add a UIFocusGuide()
put my button directly on the AVPlayerViewController's view
add a own subview, which contains the buttons to the AVPlayerViewController's view
add a own subview, which contains the buttons to the contentOverlayView
add several other buttons next to, below, above my button on the same subview (for each of the cases above)
The last approach shows that none of the other buttons can ever get focus by user interaction, so it seems, that, for the same reason, the skip-intro-button cannot be focused by the user. But what is this reason?
What is the right practice to add custom interaction elements to AVPlayerViewController?
Anyone any ideas?

Try to answer my main question "What is the right practice to add custom interaction elements to AVPlayerViewController?" myself:
Since I posted this question, 1.5 years ago, I didn't change the implementation of the skip intro feature very much. So whenever the user uses the remote, the button will lose focus and the only thing I changed is, that I hide the button whenever this happens, by implementing didUpdateFocus(in:with:), similar to this:
if let previouslyFocusedView = context.previouslyFocusedView {
if previouslyFocusedView == skipIntroButton {
changeSkipIntroButtonVisibility(to: 0.0)//animates alpha value
}
}
(I'm not completely sure, why I don't set it to isHidden = true)
However, in the meantime I had to implement a more complex overlay to our player, i.e. a "Start Next Video / Watch Credits" thing, with a teaser, some buttons, a countdown/progress bar and more. With the problems described above, it is obvious, that I couldn't go with the contentOverlayView approach.
So I decided to implement it the "traditional way", by presenting a complete UIViewController on top of the player's view, like this:
func showNextVideoOverlay() {
guard let nextVideoTeaser = nextVideoTeaser else { return }
let nextVideoOverlay = NextVideoAnnouncementViewController(withTeaser: nextVideoTeaser, player: player)
nextVideoOverlay.nextVideoAnnouncementDelegate = self
nextVideoOverlay.modalPresentationStyle = .overFullScreen
present(nextVideoOverlay, animated: true, completion: nil)
}
Of course the NextVideoAnnouncementViewController is transparent, so video watching is still possible. I turned out that this straight forward approach works pretty well and I really don't know, why I haven't thought about it, when implementing skip intro.
My colleagues in QA found one tricky thing, you should be aware of (and I think, I remember, that this is different with different Devices and different remotes and on different tvOS versions - try it):
The overlying view controller blocks most of the commands coming from the remote, respectively you can navigate inside the view controller without affecting the player - except the play/pause button. That one will pause the player, but it's not possible to resume from there. This is, because a playing (av)player always listens to this button, while a paused one doesn't. So I also had to implement something like this:
func addRemoteButtonRecognizer() {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(playPauseButtonPressed))
tapRecognizer.allowedPressTypes = [NSNumber(value: UIPress.PressType.playPause.rawValue)]
self.view.addGestureRecognizer(tapRecognizer)
}
#objc func playPauseButtonPressed(sender: AnyObject) {
nextVideoAnnouncementDelegate?.remotePlayButtonPressed()
}
func remotePlayButtonPressed() {
if player?.playerStatus == .paused {
player?.play()
} else {
player?.pause()
}
}
I hope this helps some of you, who come by here and find no other answer.

Related

How to override focus engine update sound in tvOS

I am trying to override the sound that plays when a focus change is made on tvOS, but I cannot seem to find anything indicating if this is possible. I have looked through the Apple documentation a bit, and looked at some of the sound API's but none seemed to fit. Does anybody know if this is even possible? If this is possible how can this be achieved?
This can be achieved with soundIdentifierForFocusUpdate, which was added to the SDK in tvOS 11
Using this method, you can customize or remove the default sound of tvOS played on focus updates.
To remove the sound you can return UIFocusSoundIdentifier.none
override func soundIdentifierForFocusUpdate(in context: UIFocusUpdateContext) -> UIFocusSoundIdentifier? {
return UIFocusSoundIdentifier.none
}
To use a different sound insted, you must include the new sound file in your target, and to load as shown here below:
let myPing = UIFocusSoundIdentifier.init(rawValue: "customPing")
let soundURL = Bundle.main.url(forResource: "ping", withExtension: "aif")!
UIFocusSystem.register(_: soundURL, forSoundIdentifier: myPing)
Then you have to return that sound new from soundIdentifierForFocusUpdate:
override func soundIdentifierForFocusUpdate(in context: UIFocusUpdateContext) -> UIFocusSoundIdentifier? {
return myPing
}
Everything is documented by Apple in the following link:
Using Custom Sounds for Focus Movement

Make focus change sound in tvOS

I'm programatically changing focus in a tvOS app in response to a UISwipeGestureRecognizer. This all works fine, except there the nice 'boop' sound that plays when changing focus normally doesn't play, which makes the user experience a bit odd. Is there a way to programatically play this sound, or is there some other way I should be trying to handle the focus change?
You can make the button that is in focus have a background border in a different color or something.
In regards to sounds why don't you run a SKAction or play a AVFoundation file when changing focus.
You said you did all the focus stuff programmatically with gesture recognisers (I have done the same for my SpriteKit game) so I assume in the methods that get called when swiping you need to add the sound effect.
In my game those methods look something like this
func swipedRightTV() {
if menuButton.isFocused {
menuButton.isFocused = false
// play your sound here
playButton.isFocused = true
}
func swipeLeftTV() {
if playButton.isFocused {
playButton.isFocused = false
// play your sound here
menuButton.isFocused = true
}
If I have more than 2 buttons I use more if/else if statements in the methods, for simplicity tho I just used 2 in the example.
Does this help or are your methods totally different?

AVPlayerViewController (tvOS) doesn't pause on viewWillDisappear

I have several AVPlayerViewControllers set as the ViewControllers of a UITabBarController. What I want is having the video of the currently visible AVPlayerViewController automatically pause playing when the user switches to another tab (which is another AVPlayerViewController)
I tried this approach:
override func viewWillDisappear(animated: Bool) {
player?.pause()
super.viewWillDisappear(animated)
}
but the video just keeps running in the background. (audio is still running at least) Debugger says player property is not nil in viewWillDisappear. I already tried implicitly and forced unwrapping, to no avail.
I was able to identify the problem. It seems to be that you can't pause in viewWillDisappear, neither with pause() nor with player?.rate = 0.0. So in order to prevent your video from playing in the background, you have to set AVPlayerViewController's player property to nil in viewWillDisappear.
Which sadly means that you have to write some code for preserving your playback state.
Seems a bit like a bug to me, hope this will get fixed later.

What events are produced by UIControl on tvOS?

With UIButton, tvOS produces UIControlEvents.PrimaryActionTriggered (not .TouchUpInside, as it does on iOS) when the button gets focus and the user taps on the remote.
UIControl's don't seem to produce either of these events, however. I can't see that it produces any events, in fact, when the user focuses on the control and taps on the remote.
How does one use a custom UIControl for tvOS?
A UIControl doesn't emit any control events of its own accord. Your subclass is responsible for emitting events (generally by sending itself sendActionsForControlEvents:).
Since UIControl isn't currently documented for tvOS, it might be that Apple doesn't want you to subclass it.
Anyway, I've only played around with it a little, but apparently to implement your own UIControl subclass, you must override canBecomeFocused to return YES, and you must override pressesEnded:withEvent: to act on the user's press (presumably by sending yourself sendActionsForControlEvents:.).
You might also want to override pressesBegan:withEvent: to highlight your control.
Because UIControl conforms to the UIFocusEnvironment protocol (by way of UIView), you can override didUpdateFocusInContext:withAnimationCoordinator: to update your control's appearance depending on whether it has focus.
I spent the last couple weeks adding tvOS support for the widgets I package with my SVG rendering library SVGgh, so I have somewhat of a feeling for this. The issue is that there is what I think is a new or newish kind of action: UIControlEventPrimaryActionTriggered that gets sent out when the control wants to communicate a commitment of action, as in this response to the completion of a UIPress in my GHButton class. Remember to wire up a selector to this new action in your storyboard.
-(void) pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
{
[self sendActionsForControlEvents:UIControlEventPrimaryActionTriggered];
self.beingPressed = NO;
self.selected = self.selected;
}
But in a segmented control analog, the action isn't set on button push, but when the focused segment gets changed, as in the following (heavily edited for clarity) code from my GHSegmentedControl class:
- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context
{
BOOL result = NO;
if(context.nextFocusedView == self)
{
result = YES;
[self highlight:YES];
if(context.focusHeading == UIFocusHeadingRight)
{
[self incrementValue];
[self.parentContent.control setNeedsFocusUpdate];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
else if(context.focusHeading == UIFocusHeadingLeft)
{
[self decrementValue];
[self.parentContent.control setNeedsFocusUpdate];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
}
return result;
}
Now that I think of it, it's possible I should be sending out a UIControlEventPrimaryActionTriggered action in the segmented control as well but UIControlEventValueChanged seems to work the way I expect.

Pause UI in WinRT 8.1

I can't for the life of me get the following functionality to work:
User taps item
Item's image becomes visible via changing visibility property of image
After a short period of time image becomes invisible again (with no user input) via changing the
visibility property
Or, more simply:
Make visible UI change
Pause so user can see UI change
Reverse step 1's UI change
Step 2 happens before steps 1 and 3 regardless of where the code is because the UI is not updating until the logic finishes (I assume).
I am setting the visibility of the image via data binding with INotifyPropertyChanged. All works as expected except when I'm trying to introduce the pause.
I'm trying to pause with this simple method:
private void Pause()
{
new System.Threading.ManualResetEvent(false).WaitOne(1000);
}
It does pause, but the UI changes wait until after that pause even though a change to the bound data happen befores the pause is called, and the other change after.
I have tried using the dispatcher, but it doesn't change anything, and I don't understand it enough:
await dispatcher.RunAsync(CoreDispatcherPriority.High, () =>
{
clickedCard.IsFaceDown = false; // makes the image visible via data binding
}
);
Pause();
I think I need to do something with threading, but I am going in circles.
Any ideas?
You should never do something like this inside the UI thread of your app:
new System.Threading.ManualResetEvent(false).WaitOne(1000);
There are various reasons for not doing it, but in your particular case the problem is that XAML only re-draws once your event-handler completes. So basically this happens:
The item is invisible
Your event handler is called
You set it to visible (but the UI doesn't refresh yet)
You freeze the thread for a second
You set it to invisible again
The event-handler completes
Now the UI updates based on the current value (which is invisible)
I suggest you look at building a Storyboard to do this - Blend can help. See here.