AVAudioEngine not posting notification in SpriteKit - ios

I have a problem with AVAudioEngine in SpriteKit game - when plugging/unplugging of headphones the engine stops and when the next sound play the app crashes. It is a known bug (or feature?) - with a suggestion to fix it - to use notification center, AVAudioEngine should post notification when it is changing it's states. I have made this code:
let notificationName = Notification.Name("AVAudioEngineConfigurationChange")
NotificationCenter.default.addObserver(self, selector: #selector(self.restartEngine(notification:)), name: notificationName, object: nil)
When I do this:
NotificationCenter.default.post(name: notificationName, object: nil)
My selector gets called. However when I plug/unplug headphones - nothing happens. Swift 3, xcode 8, iOS 9.3
Any suggestions on how to fix it?

The original Cocoa name of the notification is AVAudioEngineConfigurationChangeNotification, but the Swift constant is called AVAudioEngineConfigurationChange. Therefore you can handle the notification by using either:
let notificationName = Notification.Name("AVAudioEngineConfigurationChangeNotification")
or
let notificationName = Notification.Name.AVAudioEngineConfigurationChange
Your selector should then be called when plugging-in/unplugging headphones.
This appears to only be a partial solution for SpriteKit audio flakiness. I tried the following inside the notification handler:
try? self.audioEngine.start()
I was able to stop the crash, but my SKAudioNode wouldn't make any sound after the plug/unplug event, until the process was restarted.

Related

AudioKit handling of AVAudioSessionInterruption

After receiving a phone call or just having the phone ring our background play enabled AudioKit app goes silent for good and I am not sure how to handle that. The only way to restart sound output is to kill and restart the app. Other interruptions like enabling and using Siri work without a hitch and the app's sound is ducked during the event.
Typically an app can register itself to receive notifications (e.g. NSNotification.Name.AVAudioSessionInterruption) to detect an AVAudioSession interruption, but how does one retrieve the AVSession object that is normally passed into the notification?
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.sessionInterrupted(_:)),
name: NSNotification.Name.AVAudioSessionInterruption,
object: MISSING_AK_AVAUDIOSESSION_REF)
Furthermore, if one were able to successfully implement audio interrupt notifications, what happens with AudioKit? It is not designed to be "restarted" or put on hold. Any help would be much appreciated.
This is going to depend on your app how you handle this. At very least, you'll want to do Audiokit.stop() and then Audiokit.start() when the interruption is finished.
You'll want to register for the notification with something like this:
NotificationCenter.default.addObserver(self,
selector: #selector(handleInterruption),
name: .AVAudioSessionInterruption,
object: nil)
Then handle it with something like this:
#objc internal func handleInterruption(_ notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
return
}
//...handle each type here
}

CADisplayLink stutters when built from archive

I have project that is rendering video playback and applying CIFilters to it. I know that I can use video composition to get video with filters, but problems is that filters needs to be swipeable (with preview of next filter so we're using mask for 1st imageview and filtering 2nd one with next filter).
func displayLinkDidRefresh(link: CADisplayLink){
let itemTime = videoOutput.itemTime(forHostTime: CACurrentMediaTime())
if videoOutput.hasNewPixelBuffer(forItemTime: itemTime) {
if let pixelBuffer = videoOutput.copyPixelBuffer(forItemTime: itemTime, itemTimeForDisplay: nil){
unfilteredImage = CIImage(cvImageBuffer: pixelBuffer)
displayFilteredImage(unfilteredImage: unfilteredImage)
}
}
}
This is the code used to create AVPlayer instance and CADisplayLink:
player = AVPlayer(playerItem: item)
player.isMuted = true
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidRefresh(link:)))
displayLink!.preferredFramesPerSecond = 24
displayLink!.add(to: RunLoop.main, forMode: RunLoopMode.commonModes)
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidReachEnd(notification:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player.currentItem)
When running from debugger I experience no stutter (or it is at minimum level), but when running build from archive it is stuttering a lot. What I do to test is deleting app from phone and then instal it on the phone and test, as said debug builds are fine, but archive are creating issues. Any input on this?
EDIT 1:
Managed to get it to work better, but still stuttering after attaching video composition to player item when item and player status is readyToPlay.
After some time playing with this I found the issue, nothing is wrong with the code. Issue was that we have Appsee analytics inside app and we needed to pause it on these screens in order for them to render properly. Reason why we didn't experience these issues in debug mode was the AppDelegate if condition that was preventing Appsee to work in debug environment.

Setting the queue is calling the observer?

I am making a music player, and for some reason when I add the notification center observer and set the queue and play the song it gets called twice. I commented out the play method and it's only called once. I'm not sure how to fix this or whether this is the issue.
didLoad
NotificationCenter.default.addObserver(self, selector: #selector(change), name: .MPMusicPlayerControllerNowPlayingItemDidChange, object: nil)
musicPlayer.beginGeneratingPlaybackNotifications()
change function
#objc func change() {
print(musicPlayer.nowPlayingItem?.title) //called twice
}
function to queue and play
musicPlayer.setQueue(with: queueArr)
musicPlayer.play()

Timer not working on real iPhone

I'm trying to use local notification but something is not working.
I have a class notification that handle all the code related to the notifications.
It's apparently working. What is not working is the way I try to trigger my notification.
When the user clicks on the home button, I call my notification class that starts a NSTimer. It repeats every second, and each 10 seconds I call a webservice.
Everything works great on my simulator, but it doesn't work on my real iPhone.
Here the code:
//as a class variable
let notif = Notification()
func applicationDidEnterBackground(application: UIApplication) {
notif.triggerTimer()
}
The notification class
class Notification: NSObject, WsOrderStatusProtocol, WsPinRequestProtocol {
var timer = NSTimer()
var time = 0
var sendNotification:Bool = true
var wsos = WsOrderStatus()
var wsoc = PinRequest()
override init() {
super.init()
self.wsos.delegate = self
self.wsoc.delegate = self
}
func triggerTimer() {
print("log INFO : class Notification, methode : triggerTimer")
NSNotificationCenter.defaultCenter().addObserver(self, selector:"orderCoupon:", name: "actionOrderCouponPressed", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector:"cancelTimer:", name: "actionCancelTimerPressed", object: nil)
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("launchNotification"), userInfo: nil, repeats: true)
}
func launchNotification() {
print("log INFO : class Notification, methode : launchNotification")
time += 1
print("time \(time)")
if time % 10 == 0 {
print("modulo 10")
wsos.getOrderStatus()
}
}
}
In the simulator, I see the logs et the logs that counts to 10 etc, but with my real iphone, I only see the first log "print("log INFO : class Notification, methode : triggerTimer")" then nothing...
Do you know why ?
As Paul says in his comment, your app only spends a very brief time in the background before being suspended. Suspended means that your code doesn't run at all any more, so timers stop.
The simulator doesn't always follow the same rules. When its behavior is different than that of a device then ignore it. It lies.
If you want to have more time to do work in the background, you can ask for it using the method beginBackgroundTaskWithExpirationHandler. Make that call in your applicationDidEnterBackground method.
From testing I've found that that gives you 3 minutes of extra time. After that your expiration handler block gets executed and then you get suspended.
Apple does not want your app running indefinitely from the background. It drains the battery.
I've found that it is possible to lie and tell the system that you are an app that plays sounds from the background, and write your expiration handler to play a short "sound of silence" and then ask for another background task using beginBackgroundTaskWithExpirationHandler. However, doing that will get you rejected from the app store.

In Swift, how to ivalidate NSTimer in AppDelegate when application going background?

I need to translate my iOS application from obj-c to swift. I have a NStimer in ViewController that loads metadata from shoutcast every 30 seconds, but when application resign active it stops, when enter foreground it runs again.
Edit: OK. Problem solved! I added two observers in viewDidLoad with name UIApplicationWillResignActiveNotification and UIApplicationWillEnterForegroundNotification, like below:
override func viewDidLoad() {
NSLog("System Version is \(UIDevice.currentDevice().systemVersion)");
super.viewDidLoad()
self.runTimer()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "invalidateTimer", name: UIApplicationWillResignActiveNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "runTimer", name: UIApplicationWillEnterForegroundNotification, object: nil)
}
and I made two functions. First one for run timer:
func runTimer(){
loadMetadata()
myTimer.invalidate()
NSLog("timer run");
myTimer = NSTimer.scheduledTimerWithTimeInterval(30.0, target: self, selector: "loadMetadata", userInfo: nil, repeats: true)
let mainLoop = NSRunLoop.mainRunLoop()
mainLoop.addTimer(myTimer, forMode: NSDefaultRunLoopMode)
}
and second to stop it:
func invalidateTimer(){
myTimer.invalidate()
NSLog("timer invalidated %u", myTimer);
}
I hope this can help someone. :)
I suggest you use the appropriate system for your task: https://developer.apple.com/library/ios/documentation/iphone/conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html#//apple_ref/doc/uid/TP40007072-CH4-SW56
Apps that need to check for new content periodically can ask the
system to wake them up so that they can initiate a fetch operation for
that content. To support this mode, enable the Background fetch option
from the Background modes section of the Capabilities tab in your
Xcode project. (You can also enable this support by including the
UIBackgroundModes key with the fetch value in your app’s Info.plist
file.)...
When a good opportunity arises, the system wakes or launches your app
into the background and calls the app delegate’s
application:performFetchWithCompletionHandler: method. Use that method
to check for new content and initiate a download operation if content
is available.

Resources