Ok, maybe I missed something here. I want to use the black remote with my app and got this code essentially from the WWDC 2017 talk on the issue. It says ...
Consistent and intuitive control of media playback is key to many apps on tvOS, and proper use and configuration of MPNowPlayingInfoCenter and MPRemoteCommandCenter are critical to delivering a great user experience. Dive deeper into these frameworks and learn how to ensure a seamless experience whether your app is being controlled using Siri, the Siri Remote, or the iOS Remote app.
So I added these lines to viewDidLoad of my tvOS app and well they do nothing basically?
var commandCenter = MPRemoteCommandCenter.shared()
override func viewDidLoad() {
super.viewDidLoad()
commandCenter.playCommand.isEnabled = true
commandCenter.pauseCommand.isEnabled = true
commandCenter.playCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in
print("You Pressed play")
return .success
}
commandCenter.pauseCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in
print("You Pressed pause")
return .success
}
}
I run the app, and try the play/pause button on the black remote and nothing is printed to the debugging console? Also added some code the plist related to background mode...Should this work or did I miss the point here somewhere?
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>external-accessory</string>
</array>
The commands in MPRemoteCommandCenter aren't triggered by the Siri Remote when your app is in the foreground. To get events from the remote when you're in the foreground, use UIGestureRecognizer like you're probably already used to.
These commands in MPRemoteCommandCenter are for other ways the system may want to interact with your playback, such as:
Your app is playing audio in the background, and the user presses the pause button on the remote: your app I'll be asked to pause playback.
The user is using the TV Remote app for iOS and is using that app's playback control screen.
Posted the question to Apple support; who pointed me in the right direction, need to use the GCMicroGamepad controller or its related GameKit frameworks. Than found a 2015 example posted by blauzahn who most certainly deserves the credit really for this post. Here is his code slightly modified for Swift 3.0, ios 10.x
import GameController
..
var gamePad: GCMicroGamepad? = nil
NotificationCenter.default.addObserver(self,
selector: #selector(gameControllerDidConnect),
name: NSNotification.Name.GCControllerDidConnect,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(gameControllerDidDisconnect),
name: NSNotification.Name.GCControllerDidDisconnect,
object: nil)
func gameControllerDidConnect(notification : NSNotification) {
if let controller = notification.object as? GCController {
if let mGPad = controller.microGamepad {
// Some setup
gamePad = mGPad
gamePad!.allowsRotation = true
gamePad!.reportsAbsoluteDpadValues = true
print("MicroGamePad connected...")
// Add valueChangedHandler for each control element
if gamePad?.buttonA.isPressed == true {
print("button A pressed")
}
if gamePad?.buttonX.isPressed == true {
print("button X pressed")
}
gamePad!.dpad.valueChangedHandler = { (dpad: GCControllerDirectionPad, xValue: Float, yValue: Float) -> Void in
print("dpad xValue = \(xValue), yValue = \(yValue)")
}
gamePad!.buttonA.valueChangedHandler = { (buttonA: GCControllerButtonInput, value:Float, pressed:Bool) -> Void in
print("\(buttonA)")
}
gamePad!.buttonX.valueChangedHandler = { (buttonX: GCControllerButtonInput, value:Float, pressed:Bool) -> Void in
print("\(buttonX)")
}
}
}
}
// Game controller disconnected
func gameControllerDidDisconnect(notification : NSNotification) {
if let controller = notification.object as? GCController {
if controller.microGamepad != nil {
self.gamePad = nil
print("MicroGamePad disconnected...")
}
}
}
Related
in my application I have one requirement. I am running my application and playing one video using AVPlayerViewcontroller. In middle I received call and I had answer the call and my video will pause.After 5 seconds I will get new url from server to play in AVPlayerViewcontroller. That time new url is playing in background can able to hear the sound along with phone call. In this scenario I want to send phone app to background and want to see the video which is playing in avplayer.
Please let me know is there any way to achieve this.
SWIFT:
Observe for Interruption Notifications
func setupNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(handleInterruption),
name: .AVAudioSessionInterruption,
object: nil)
}
Respond to Interruption Notifications
#objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
// Interruption began, take appropriate actions
}
else if type == .ended {
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// Interruption Ended - playback should resume
} else {
// Interruption Ended - playback should NOT resume
}
}
}
}
Apple documentation
My audio starts and stops as I would expect. When I background the app the music keeps playing, if I activate Siri, the music interrupts but then resumes as I would expect.
The issue I have is that, if my sounds are playing in the background, and I start up Apple Music or Podcasts, the audio mixes together which I don't want however if I use Siri, my audio stops then resumes after.
I want my music to stop and the other to take control of the audio just like it does with Siri. I have tried removing .mixWithOthers but when I do that, it seems that once I background my app and I start Siri, afterwards my audio is no longer able to start again even though the code within the .ended case is called.
func commonInit() {
try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: .mixWithOthers)
NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption), name: .AVAudioSessionInterruption, object: nil)
}
var shouldResume: Bool = false
#objc func handleInterruption(_ notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue)
else { return }
switch type {
case .began:
player?.pause()
case .ended:
guard let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt
else { return }
let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
player?.play()
}
}
}
Ideally I want my app to resume after any interruptions, but I also want my app to stop playing if there are any interruptions.
Thanks
Adding UIApplication.shared.beginReceivingRemoteControlEvents() after setting the category fixed this issue but I'm not sure why.
My app continues to play music even when the app is in the background and I am hoping to send the user a notification when the AVPlayer gets overridden (e.g. if the user uses another app that over rides it).
Currently my solution is to use a checkInTimer that sets up a notification and if the app does not checkIn after x amount of time, then the notification goes off, but if it does checkIn it deletes the notification and sets up another one. But this solution sucks..
Any ideas?
You need to observe audio interruptions:
import AVFoundation
func setup() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(myInterruptionHandler), name: AVAudioSessionInterruptionNotification, object: nil)
}
func myInterruptionHandler(notification: NSNotification) {
var info = notification.userInfo!
var intValue: UInt = 0
(info[AVAudioSessionInterruptionTypeKey] as! NSValue).getValue(&intValue)
if let type = AVAudioSessionInterruptionType(rawValue: intValue) {
switch type {
case .Began:
// interruption began
case .Ended:
// interruption ended
}
}
}
I use AVAudioPlayer to play audio. I have background audio enabled and the audio sessions are configured correctly.
I implemented the audioSessionGotInterrupted method to be informed if the audio session gets interrupted. This is my current code:
#objc private func audioSessionGotInterrupted(note: NSNotification) {
guard let userInfo = note.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
print("interrupted")
// Interruption began, take appropriate actions
player.pause()
saveCurrentPlayerPosition()
}
else if type == .ended {
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
if options == .shouldResume {
print("restored")
// Interruption Ended - playback should resume
setupPlayer()
player.play()
} else {
// Interruption Ended - playback should NOT resume
// just keep the player paused
}
}
}
}
Now I do the following:
Play some audio
Lock the phone
Pause the audio
Wait for some seconds until I see in the XCode debugger that the app has been stopped in background
I hit play in the lockscreen
My commandCenter play() methods gets called as expected. However also the audioSessionGotInterrupted method gets called with type == .began.
How is that possible? I expect to see no notification of that kind or at least .ended
I use iOS 10 beta 8.
Check this
https://developer.apple.com/documentation/avfoundation/avaudiosession/1616596-interruptionnotification
Starting in iOS 10, the system will deactivate the audio session of most apps in response to the app process being suspended. When the app starts running again, it will receive an interruption notification that its audio session has been deactivated by the system. This notification is necessarily delayed in time because it can only be delivered once the app is running again. If your app's audio session was suspended for this reason, the userInfo dictionary will contain the AVAudioSessionInterruptionWasSuspendedKey key with a value of true.
If your audio session is configured to be non-mixable (the default behavior for the playback, playAndRecord, soloAmbient, and multiRoute categories), it's recommended that you deactivate your audio session if you're not actively using audio when you go into the background. Doing so will avoid having your audio session deactivated by the system (and receiving this somewhat confusing notification).
if let reason = AVAudioSessionInterruptionType(rawValue: reasonType as! UInt) {
switch reason {
case .began:
var shouldPause = true
if #available(iOS 10.3, *) {
if let _ = notification.userInfo?[AVAudioSessionInterruptionWasSuspendedKey] {
shouldPause = false
}
}
if shouldPause {
self.pause()
}
break
case .ended:
break
}
}
While the answer above is not wrong it still caused a lot of trouble in my app and a lot of boilerplate code for checking multiple cases.
If you read the description of AVAudioSessionInterruptionWasSuspendedKey it says that the notification is thrown if you didn't deactivate your audio session before your app was sent to the background (which happens every time you lock the screen)
To solve this issue you simply have to deactivate your session if there is no sound playing when app is sent to background and activate it back if the sound is playing. After that you will not receive the AVAudioSessionInterruptionWasSuspendedKey notification.
NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { sender in
guard self.player.isPlaying == false else { return }
self.setSession(active: false)
}
NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { sender in
guard self.player.isPlaying else { return }
self.setSession(active: true)
}
func setSession(active: Bool) -> Bool {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playback, mode: .default)
try session.setActive(active)
return true
} catch let error {
print("*** Failed to activate audio session: \(error.localizedDescription)")
return false
}
}
Note: Activating session is probably not necessary because it is handled by Apple's internal playback classes (like AVPlayer for example) but it is a good practice to do it manually.
I want to build a radio app and so I would like to use the stop button instead of the pause button in the control center like Apple Radio does in the native music app :
Here is what I did in my RadioPlayer class :
private var shoutcastStream = NSURL(string: "http://shoutcast.com:PORT/;stream.mp3")
var playerItem:AVPlayerItem?
var player:AVPlayer?
let commandCenter = MPRemoteCommandCenter.sharedCommandCenter()
override init() {
super.init()
do {
// Allow background audio
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch _ as NSError {
}
// Disable Next, Prev and Pause
commandCenter.pauseCommand.enabled = false
commandCenter.nextTrackCommand.enabled = false
commandCenter.previousTrackCommand.enabled = false
// Enable Play
commandCenter.playCommand.enabled = true
commandCenter.playCommand.addTarget(self, action: #selector(RadioPlayer.play))
// Enable Stop
commandCenter.stopCommand.enabled = true
commandCenter.stopCommand.addTarget(self, action: #selector(RadioPlayer.stop))
} catch _ as NSError {
}
}
It's now working fine but the stop button isn't showing. Instead, I have the Pause button, which doesn't make sense for a radio player haha.
Note that in the above case, even if the control center is showing the pause button, nothing happens when pause button is pressed, because no target is attached to it (I attached it to the stopCommand).
So the question is: how to use that Stop button? Thank you.
EDIT:
I think the "stop" command is only displayed when MPNowPlayingInfoPropertyIsLiveStream = true (available only from iOS 10) /:
It does not matter if you disable the "pause" or "togglePlayPause" commands. From iOS 10, the "stop" command will be displayed if MPNowPlayingInfoPropertyIsLiveStream = true.
You may need to handle the "pause" or the "togglePlayPause" command too (for earlier versions). Good luck!
OK, I also had this doubt and did not find on the internet how to do what I wanted so I started reading more about MPRemoteCommandCenter and MPNowPlayingInfoCenter.
I tried disabling all the buttons I did not use. Also, I read about MPNowPlayingInfoPropertyIsLiveStream and I share in this post in case anyone finds it useful (look at the comments in the code):
Swift 3
MPNowPlayingInfoCenter (for metadata):
var songInfo = [:] as [String : Any]
if NSClassFromString("MPNowPlayingInfoCenter") != nil {
songInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: UIImage(named: "your_image_name")!)
songInfo[MPMediaItemPropertyTitle] = "Title"
songInfo[MPMediaItemPropertyArtist] = "Artist name"
// If is a live broadcast, you can set a newest property (iOS 10+): MPNowPlayingInfoPropertyIsLiveStream indicating that is a live broadcast
if #available(iOS 10.0, *) {
songInfo[MPNowPlayingInfoPropertyIsLiveStream] = true
} else {
// Fallback on earlier versions
}
MPNowPlayingInfoCenter.default().nowPlayingInfo = songInfo
} // end if MPNowPlayingInfoCenter
MPRemoteCommandCenter:
if #available(iOS 9.1, *) {
let center = MPRemoteCommandCenter.shared()
// Disable all buttons you will not use (including pause and togglePlayPause commands)
[center.pauseCommand, center.togglePlayPauseCommand, center.nextTrackCommand, center.previousTrackCommand, center.changeRepeatModeCommand, center.changeShuffleModeCommand, center.changePlaybackRateCommand, center.seekBackwardCommand, center.seekForwardCommand, center.skipBackwardCommand, center.skipForwardCommand, center.changePlaybackPositionCommand, center.ratingCommand, center.likeCommand, center.dislikeCommand, center.bookmarkCommand].forEach {
$0.isEnabled = false
}
// For "play" command
center.playCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in
// play the song here
return MPRemoteCommandHandlerStatus.success
}
// For "stop" command
center.stopCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in
// stop the song here
return MPRemoteCommandHandlerStatus.success
}
} else {
// Fallback on earlier versions
}
I have done. I hope I have helped you and others (:
according to this question-answer , Apparently ControlCenter isn't customizable(At least until now).