Darren
Darren

Reputation: 10398

Swift data race with AppKit MPMediaItemArtwork function

Consider the code below. It uses the AppKit (macOS) MPMediaItemArtwork function to set the artwork in MPNowPlayingInfoCenter.

During runtime, when compiled with Swift 6, this gives the warning:

warning: data race detected: @MainActor function at MPMediaItemArtwork/ContentView.swift:22 was not called on the main thread

It seems that the requestHandler is not called on the main thread. Adding @MainActor in doesn't seem to make any difference. I may be a little rusty on the Objective-C side, but I can't figure out how to return the image on the main thread as it won't let me use a Task with a return value, as it doesn't support concurrency.

import SwiftUI
import MediaPlayer

struct ContentView: View {
    var body: some View {
        Text("Test")
            .padding()
            .task {
                await MainActor.run {
                    let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
                    var nowPlayingInfo = [String: Any]()
                    let image = NSImage(named: "image")!
                    // warning: data race detected: @MainActor function at MPMediaItemArtwork/ContentView.swift:22 was not called on the main thread
                    nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { _ in
                        // Not on main thread here!
                        return image
                    })
                    
                    nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo
                }
            }
    }
}

Here is a version I have updated to use async/await, however it still gives the same error on the same line:

extension MPMediaItemArtwork: @unchecked Swift.Sendable {
    
    @MainActor
    static func with(image: NSImage) async -> MPMediaItemArtwork {
        await withCheckedContinuation { [image] continuation in
            continuation.resume(returning: MPMediaItemArtwork(boundsSize: image.size) { [image] _ in
                return image
            })
        }
    }
}

.task {
    nowPlayingInfo[MPMediaItemPropertyArtwork] = await MPMediaItemArtwork.with(image: image)
}

Upvotes: 0

Views: 163

Answers (2)

Darren
Darren

Reputation: 10398

It turns out there is an easy fix here. Simply marking the completion handler as @Sendable.

let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { @Sendable _ in
    return image
})

I'm still confused as to why there was no compiler warning or error here, but hopefully that's something Apple can/will fix.

Upvotes: 0

lorem ipsum
lorem ipsum

Reputation: 29614

Try this, it is a AsyncStream so it will return both values separately

@preconcurrency import MediaPlayer

extension MPMediaItemArtwork {
    static func with(image: NSImage) -> AsyncStream<ArtworkReturn> {
        return .init { streamContinuation in
            let artwork = MPMediaItemArtwork(boundsSize: image.size) { (size) -> NSImage in
                defer{
                    streamContinuation.yield(.size(size))
                    streamContinuation.finish()
                }
                return image
            }
            
            streamContinuation.yield(.artwork(artwork))
        }
        
    }
    
    enum ArtworkReturn:Sendable {
        case artwork(MPMediaItemArtwork)
        case size(CGSize)
    }
}

I don't have a project setup but it should work. Usage would look something like

    .task {
        let stream = MPMediaItemArtwork.with(image: .init())
        for await result in stream {
            switch result {
            case .artwork(let mPMediaItemArtwork):
                //Do something with the artwork
                break
            case .size(let cGSize):
                //Do something with the size
                break
            }
        }
        print("Received both artwork and size")
    }

Upvotes: 0

Related Questions