Playing audio freezes the main thread - swift

For some reason the following code freezes the main thread when I try playing audio. I've tried everything from dispatching to another thread but that doesn't do much. I even tried running the queue in async, but then I run into problems when the user clicks another audio file before the first one finishes loading. Any tips on which thread to use?
let serialQueue = DispatchQueue(label: "queuename")
serialQueue.sync {
if (jukeboxPlayer.jukebox != nil) {
if let playingURL = jukeboxPlayer.jukebox?.currentItem?.URL {
jukeboxPlayer.jukebox?.removeItems(withURL: playingURL)
}
jukeboxPlayer.jukebox?.append(item: JukeboxItem(URL: url), loadingAssets: true)
} else {
jukeboxPlayer.jukebox = Jukebox(delegate: self, items: [
JukeboxItem(URL: url),
])
}
print("3.0")
jukeboxPlayer.jukebox?.play()
print("==========Started playing===========")
self.showPlayer()
}
Also tried:
serialQueue.sync {
and
DispatchQueue.global(qos: .background).async {
And here's the framework I use:
https://github.com/teodorpatras/Jukebox

Related

How can I convert to Swift async/await from GCD (DispatchQueue)?

I am following Stanfords' CS193p Developing Apps for iOS online course.
It is using the Grand Central Dispatch (GCD) API for a demo of multithreading.
But they noted, that
"GCD has been mostly replaced by Swift's new built-in async API as of WWDC 2021".
So I wanted to learn how the code from the Lecture would look like after updating it to use this new API.
After watching Apple's WWDC videos, it seems to me like
DispatchQueue.global(qos: .userInitiated).async { } is replaced in this new async API with Task { } or Task(priority: .userInitiated) {}, but I'm not sure, what has DispatchQueue.main.async { } been replaced with?
So, my questions are:
Am I correctly assuming, that DispatchQueue.global(qos: .userInitiated).async { } has been replaced with Task(priority: .userInitiated) {}
What has DispatchQueue.main.async { } been replaced with?
Please help, I want to learn this new async-await API.
Here's the code from the Lecture, using old GCD API:
DispatchQueue.global(qos: .userInitiated).async {
let imageData = try? Data(contentsOf: url)
DispatchQueue.main.async { [weak self] in
if self?.emojiArt.background == EmojiArtModel.Background.url(url) {
self?.backgroundImageFetchStatus = .idle
if imageData != nil {
self?.backgroundImage = UIImage(data: imageData!)
}
// L12 note failure if we couldn't load background image
if self?.backgroundImage == nil {
self?.backgroundImageFetchStatus = .failed(url)
}
}
}
}
The whole function (in case you need to see more code):
private func fetchBackgroundImageDataIfNecessary() {
backgroundImage = nil
switch emojiArt.background {
case .url(let url):
// fetch the url
backgroundImageFetchStatus = .fetching
DispatchQueue.global(qos: .userInitiated).async {
let imageData = try? Data(contentsOf: url)
DispatchQueue.main.async { [weak self] in
if self?.emojiArt.background == EmojiArtModel.Background.url(url) {
self?.backgroundImageFetchStatus = .idle
if imageData != nil {
self?.backgroundImage = UIImage(data: imageData!)
}
// L12 note failure if we couldn't load background image
if self?.backgroundImage == nil {
self?.backgroundImageFetchStatus = .failed(url)
}
}
}
}
case .imageData(let data):
backgroundImage = UIImage(data: data)
case .blank:
break
}
}
If you really are going to do something slow and synchronous, Task.detached is a closer analog to GCD’s dispatching to a global queue. If you just use Task(priority: ...) { ... } you are leaving it to the discretion of the concurrency system to decide which thread to run it on. (And just because you specify a lower priority does not guarantee that it might not run on the main thread.)
For example:
func fetchAndUpdateUI(from url: URL) {
Task.detached { // or specify a priority with `Task.detached(priority: .background)`
let data = try Data(contentsOf: url)
let image = UIImage(data: data)
await self.updateUI(with: image)
}
}
And if you want to do the UI update on the main thread, rather than dispatching it back to the main queue, you would simply add the #MainActor modifier to the method that updates the UI:
#MainActor
func updateUI(with image: UIImage?) async {
imageView.image = image
}
That having been said, this is a pretty unusual pattern (doing the network request synchronously and creating a detached task to make sure you don't block the main thread). We would probably use URLSession’s new asynchronous data(from:delegate:) method to perform the request asynchronously. It offers better error handling, greater configurability, participates in structured concurrency, and is cancelable.
In short, rather than looking for one-to-one analogs for the old GCD patterns, use the concurrent API that Apple has provided where possible.
FWIW, in addition to the #MainActor pattern shown above (as a replacement for dispatching to the main queue), you can also do:
await MainActor.run {
…
}
That is roughly analogous to the dispatching to the main queue. In WWDC 2021 video Swift concurrency: Update a sample app, they say:
In Swift’s concurrency model, there is a global actor called the main actor that coordinates all operations on the main thread. We can replace our DispatchQueue.main.async with a call to MainActor’s run function. This takes a block of code to run on the MainActor. …
But he goes on to say:
I can annotate functions with #MainActor. And that will require that the caller switch to the main actor before this function is run. … Now that we've put this function on the main actor, we don’t, strictly speaking, need this MainActor.run anymore.

DispatchQueue.main.asyncAfter hanging on repeat, but does not hang when using sleep

I am trying to create a Robotic Process Automation tool for Macos using Swift. Users create an Automation that is an array of Step objects and then play it. One of the subclasses of Step is Pause which is supposed to pause the execution for a given number of seconds.
For some reason, execution hangs when I use the DispatchQueue.main.asyncAfter() method in the Pause class. Usually the first run through the automation is fine, but when it goes to repeat, it eventually hangs for much longer. The error goes away when I use sleep() instead.
The other weird thing about this bug is when I open Xcode to try and see what is happening, the hang resolves and execution continues. I am wondering if the process enters background somehow and then the DispatchQueue.main.asyncAfter() doesn't work. I have tried to change the Info.plist "Application does not run in background" to YES, but this doesn't have any effect.
The problem with using sleep() is it blocks the UI thread so users can't stop the automation if they need to. I have tried lots of different variations of threading with DispatchQueue, but it always seems to hang somewhere on repeat execution. I have also tried using a Timer.scheduledTimer() instead of DispatchQueue but that hangs as well. I'm sure I'm missing something simple, but I can't figure it out.
Creating the Step Array and Starting Automation
class AutomationPlayer {
static let shared = AutomationPlayer()
var automation: Automation?
var stepArray: [Step] = []
func play() {
// Create array of steps
guard let steps = automation?.steps, let array = Array(steps) as? [Step] else {
return
}
// If the automation repeats, add more steps to array.
for _ in 0..<(automation?.numberOfRepeats ?? 1) {
for (index, step) in array.enumerated() {
stepArray.append(step)
}
}
// Add small delay to allow window to close before execution.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
self?.execute(index: 0)
}
}
private func execute(index: Int) {
let step = stepArray[index]
executeStep(step: step) { [weak self] success, error in
guard error == nil else { return }
let newIndex = index + 1
if newIndex < self?.stepArray.count ?? 0 {
//Need a small delay between steps otherwise execution is getting messed up.
usleep(400000)
self?.execute(index: newIndex)
} else {
self?.stepArray = []
}
}
}
private func executeStep(step: Step?, completionHandler: #escaping (Bool, Error?) -> Void) -> Void {
step?.execute(completionHandler: { [weak self] success, error in
guard error == nil else {
completionHandler(false, error)
return
}
completionHandler(true, nil)
})
}
Pause Class
#objc(Pause)
public class Pause: Step {
override func execute(completionHandler: #escaping (Bool, Error?) -> Void) {
print("Pause for: \(self.time) seconds")
// This will eventually hang when the automation repeats itself
DispatchQueue.main.asyncAfter(deadline: .now() + Double(self.time)) {
completionHandler(true, nil)
})
// This will also hang
Timer.scheduledTimer(withTimeInterval: self.time, repeats: false) { timer in
completionHandler(true, nil)
}
// If I use this instead, the automation repeats just fine
sleep(UInt32(self.time))
completionHandler(true, nil)
}
}
So I think I figured it out. MacOS was putting my app into AppNap after a certain period of time which would cause the DispatchQueue.main.async() to stop working. For some reason, AppNap does not affect delays when you use sleep()
I found an answer here
This answer was a little older. I am using SwiftUI to build my mac app so I added this my #main struct
#main
struct Main_App: App {
#State var activity: NSObjectProtocol?
var body: some Scene {
WindowGroup("") {
MainWindow()
.onAppear {
activity = ProcessInfo().beginActivity(options: .userInitiated, reason: "Good Reason")
}
}
}
This seems to prevent the app from going into AppNap and the automation continues. It's pretty ugly, but it works.

Swift : telling callback to run on global queue

I'm running a background upload task but I found it's blocking the main thread. After looking I suspect this happens because 3rd party library (Firebase in this case) must be scheduling its async callback on the main thread.
Is there a way to explicitly make the callback run on the global thread?
Here's how I start the task from the main thread:
DispatchQueue.global(qos: .background).async {
PhotoUploadOperation().start()
}
Here's an oversimplified version of the upload task:
class PhotoUploadOperation {
func uploadCameraRoll() {
for element in photos {
self.uploadPhoto(element.image, uid) { url in
// Some work
if let url = url {
let photo = Photo(uid: uid, url: url, creationDate: element.date)
self.sendPhoto(photo: photo) { success in
// Some work
}
}
}
}
}
}
Could you try this solution?
Firebase or other 3party framework use "method swizling" to accomplish some network log or other things. But your scenario is a little bit different.
extension DispatchQueue {
static func background(delay: Double = 0.0, background: (()->Void)? = nil, completion: (() -> Void)? = nil) {
DispatchQueue.global(qos: .background).async {
background?()
if let completion = completion {
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
completion()
})
}
}
}
}
Usage:
DispatchQueue.background(delay: 3.0, background: {
// do something in background
}, completion: {
// when background job finishes, wait 3 seconds and do something in main thread
})
DispatchQueue.background(background: {
// do something in background
}, completion:{
// when background job finished, do something in main thread
})
DispatchQueue.background(delay: 3.0, completion:{
// do something in main thread after 3 seconds
})
And you dont forget allow background processing from Signing and Capabilities

Calling stop() on AVAudioPlayerNode after finished playing causes crash

I have an AVAudioPlayerNode object which sends a callback once it is finished playing. That callback triggers many other functions in the app, one of which sends out a courtesy stop() message. For some reason, calling stop() at the moment the AVAudioPlayerNode finishes causes a crash. In the code here, I have abbreviated it so that the AVAudioPlayerNode just calls stop immediately to demonstrate the effect (rather than including my whole application). You can see clearly that it crashes. I don't understand why. Either a) the node is still playing and stop() stops it or b) it is done playing and stop can be ignored.
My guess is that this is some edge case where it is at the very end of the file buffer, and it is in an ambiguous state where has no more buffers remaining, yet is technically still playing. Perhaps calling stop() tries to flush the remaining buffers and they are empty?
func testAVAudioPlayerNode(){
let testBundle = Bundle(for: type(of: self))
let url = URL(fileURLWithPath: testBundle.path(forResource: "19_lyrics_1", ofType: "m4a")!)
let player = AVAudioPlayerNode()
let engine = AVAudioEngine()
let format = engine.mainMixerNode.outputFormat(forBus: 0)
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: format)
let delegate = FakePlayMonitorDelegate()
do {
let audioFile = try AVAudioFile(forReading: url)
let length = audioFile.length
player.scheduleFile(audioFile, at: nil, completionCallbackType: AVAudioPlayerNodeCompletionCallbackType.dataPlayedBack, completionHandler: {(completionType) in
print("playing = \(player.isPlaying)")
player.stop()
})
try engine.start()
let expectation = self.expectation(description: "playback")
delegate.expectation = expectation
player.play()
self.waitForExpectations(timeout: 6.0, handler: nil)
} catch {
}
}
In my case calling the .stop() method from the Main thread (you can use another) has resolved the issue.
DispatchQueue.main.async {
player.stop()
}
It can be the deadlock. I have noticed that the completionHandler is invoked from the background queue which is a serial queue, let's call it a "render queue".
Seems that the .stop() method is trying to do some work on a "render queue" but at the moment a "render queue" is busy with the completionHandler.

GCD `asyncAfter` does not start when app in background/inactive

I'm using AVAudioPlayer for playing records. Between each playback session I have interval from 0 to 10 sec. To make this interval I'm using AVAudioPlayerDelegate and when playing is finished I'm starting new playback after delay:
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
guard let session = playbackSessionId,
let audioTrack = audioTrack,
let failureHandler = playingFailure,
let successHandler = playingSuccess else {
playingFinished(flag, error: nil)
return
}
print("audioPlayerDidFinishPlaying fired")
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + LoopInterval.currentInterval) { [weak self] in
print("asyncAfter fired")
guard let strongSelf = self,
let currentSession = strongSelf.playbackSessionId, session == currentSession else { return }
strongSelf.startPlayingRecordInLoop(audioTrack, success: successHandler, failure: failureHandler)
}
}
After app goes to the background (home button), audioPlayerDidFinishPlaying fires, but DispatchQueue.global(qos: .utility).asyncAfter not. So in console I see:
audioPlayerDidFinishPlaying fired
As soon as app become active, asyncAfter fires and I see next log message:
asyncAfter fired
When app is active, all works as expected.
Hope it'll help someone. I found problem: when app goes in background it stops background tasks, and fires them only after becomes active. To avoid need you should keep your app running in background and awaiting for your long-running background task.
backgroundTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
})
This method lets your app continue to run for a period of time after it transitions to the background. You should call this method at times where leaving a task unfinished might be detrimental to your app’s user experience. For example, your app could call this method to ensure that had enough time to transfer an important file to a remote server or at least attempt to make the transfer and note any errors. You should not use this method simply to keep your app running after it moves to the background.
After task finished you should call endBackgroundTask. If you won't end background task until backgroundTimeRemaining becomes 0, app will be terminated:
UIApplication.shared.endBackgroundTask(backgroundTaskID)
Each call to this method must be balanced by a matching call to the endBackgroundTask(:) method. Apps running background tasks have a finite amount of time in which to run them. (You can find out how much time is available using the backgroundTimeRemaining property.) If you do not call endBackgroundTask(:) for each task before time expires, the system kills the app. If you provide a block object in the handler parameter, the system calls your handler before time expires to give you a chance to end the task.
That's what I did in my case:
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
guard let session = playbackSessionId,
let audioTrack = audioTrack,
let failureHandler = playingFailure,
let successHandler = playingSuccess else {
playingFinished(flag, error: nil)
return
}
backgroundTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: { [weak self] in
guard let taskId = self?.backgroundTaskID else { return }
UIApplication.shared.endBackgroundTask(taskId)
})
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + LoopInterval.currentInterval) { [weak self] in
guard let strongSelf = self,
let currentSession = strongSelf.playbackSessionId, session == currentSession else { return }
strongSelf.startPlayingRecordInLoop(audioTrack, success: successHandler, failure: failureHandler)
if let backgroundTaskID = strongSelf.backgroundTaskID {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
strongSelf.backgroundTaskID = nil
}
}
}