TruMan1
TruMan1

Reputation: 36078

How to use Swift Concurrency to load AVQueuePlayer?

I have about 200 audio assets about 5 seconds each that are loaded into an AVQueuePlayer. I do this so it plays all the assets as one full song, but allowing the user to view or jump to a specific verse.

The issue is the main thread gets locked when I load them in like this:

let items = urls
    .map(AVAsset.init)
    .map(AVPlayerItem.init)

let player = AVQueuePlayer(items: items)

Then I discovered I must asynchronously load them into the player so tried something like this:

let items = urls
    .map(AVAsset.init)
    .map(AVPlayerItem.init)

let player = AVQueuePlayer()

// Below does NOT compile!!
await withThrowingTaskGroup(of: Void.self) { group in
    for var item in items {
        group.addTask { _ = try await item.asset.load(.duration) }
        for try await result in group {
            player?.insert(item, after: nil)
        }
    }
}

I'm only supporting iOS 16+ so I'm trying to make use of the new Swift Concurrency APIs available in AVFoundation. I tried following this document but there's a lot of gaps I can't quite get. What is the proper way of loading up that many assets into the queue player without locking up the main thread?

Upvotes: 2

Views: 825

Answers (2)

lorem ipsum
lorem ipsum

Reputation: 29271

Since it seems that you are trying to directly affect items and player I would use actor. To keep everything together and synchronized.

To bypass captured var 'item' in concurrently-executing code you can use .enumerated()

actor PlayerLoader{
    let items = [URL(string: "https://v.ftcdn.net/01/53/82/41/700_F_153824165_dN7n9QftImnClb7z1IJIjbLgLlkHyYDS_ST.mp4")!]
        .map(AVAsset.init)
        .map(AVPlayerItem.init)
    
    let player = AVQueuePlayer()
    
    func loadItems() async throws{
        try await withThrowingTaskGroup(of: Void.self) { group in
            for (idx, _) in items.enumerated() {
                group.addTask { [weak self] in
                    _ = try await self?.items[idx].asset.load(.duration)
                }
                for try await _ in group {
                    player.insert(items[idx], after: nil)
                }
            }
        }
    }
}

Then just call loadItems with Task or Task.detached

let loader = PlayerLoader()

let task = Task{ //or Task.detached   depending on where you will be calling this line of code
    do{
        try await loader.loadItems()
        print("total number of items loaded \(loader.player.items().count)")
    }catch{
        print(error)
        throw error
    }
}

Upvotes: 2

cora
cora

Reputation: 2102

How about something like this?

import AVFoundation

let items = [URL(string: "https://google.com")!]

let player = AVQueuePlayer()

try await withThrowingTaskGroup(of: AVPlayerItem.self) { group in
    for item in items {
        group.addTask {
            AVPlayerItem(asset: AVAsset(url: item))
        }
    }
    
    for try await item in group {
        player.insert(item, after: nil)
        print(item)
    }
}

Upvotes: 1

Related Questions