Media Player Queue Descriptors broken on Mac Catalyst

Hello,

I'm the developer of an Apple Music app called Soor, I've been recently working on adding Catalyst support to the app.

However, I've noticed some severe bugs while setting the queue for playing non-library items on macOS 12.2.

Both MPMusicPlayerPlayParametersQueueDescriptor and MPMusicPlayerStoreQueueDescriptor fail to play items using valid playbackStore identifiers.

The console logs the following errors:

[SDKPlayback] systemMusicPlayer _establishConnectionIfNeeded timeout [ping did not pong]

`[SDKPlayback] Failed to prepareToPlay error: Error Domain=NSOSStatusErrorDomain Code=9205 "(null)"`

I have filed radars for this along with sample projects showcasing the issue. FB9890270 and FB9890331.

Here's a gist of the sample code for which the player either completely fails to set the queue or now playing item stays nil.

    /// These are valid playback store ids retrieved from Apple Music API.
	  /// You may replace them with any valid playback store IDs of your choice.
	  let playbackStoreIDs = ["1588418743", "1604815955", "1596475453", "1562346959", "1596475469", "1596475460", "1580955750", "1591442362", "1607324602", "1531596345"]

	  var playParams = [MPMusicPlayerPlayParameters]()

		for playbackStoreID in playbackStoreIDs {
			let param = MPMusicPlayerPlayParameters(dictionary: ["id": playbackStoreID, "kind": "song"])!
			playParams.append(param)
		}

		let queueDesc = MPMusicPlayerPlayParametersQueueDescriptor(playParametersQueue: playParams)
		queueDesc.startItemPlayParameters = playParams[3]		

		player.setQueue(with: queueDesc)
		player.play()

Has anyone managed to playback music correctly using only playback store ids on Catalyst?

Replies

Hello @Tanmay007,

First, let me make a side comment.

The way you are creating instances of MPMusicPlayerPlayParameters is not recommended. The dictionary to be passed to the initializer is meant to come straight from Apple Music API's playParams attribute for Songs or other types of resources. You are not supposed to craft this dictionary yourself, manually, as you're showing it here in this snippet of code.

If you are decoding Apple Music API resources using a custom Decodable structure, feel free to drop in MPMusicPlayerPlayParameters directly in there, as that type also conforms to Decodable:

struct MySongAttributes: Decodable {
    let name: String?
    let playParams: MPMusicPlayerPlayParameters?
    [...]
}

Initiating playback using play parameters, when used correctly as I just described, is the preferred way to go.

That said, if you do want to have a code-path where you initiate playback using an array of identifiers, as strings, like your playbackStoreIDs, you should just use MPMusicPlayerStoreQueueDescriptor.

However, I should probably also let you know that, based on other feedback we've gotten, such as in this other thread, you might still encounter issues when initiating playback from a Mac Catalyst app.

Best regards,

Add a Comment

This is what I've found out so far:

@available(iOS 15, macCatalyst 15, *)

extension PlayableMusicItem {

    

    func play() {

        /// This works for me, but the artwork is blank

        mpMediaPlayer.setQueue(with: [id.rawValue])

        mpMediaPlayer.prepareToPlay()

    

        /// This does not work, throwing the `9205` error code.

        /// `[SDKPlayback] Failed to prepareToPlay error: Error Domain=NSOSStatusErrorDomain Code=9205 "(null)"`

        Task {

            musicPlayer.queue = [self]

            try await musicPlayer.play()

        }

        

        /// This also doesn't work, throwing the same `9205` error code

        let parameters = MPMusicPlayerPlayParameters(dictionary: ["id": id.rawValue, "kind": "song"])

        guard let parameters = parameters else { return }

        mpMediaPlayer.setQueue(with: MPMusicPlayerPlayParametersQueueDescriptor(playParametersQueue: [parameters]))

        mpMediaPlayer.prepareToPlay()

    }

    

    private var musicPlayer: SystemMusicPlayer { .shared }

    

    private var mpMediaPlayer: MPMusicPlayerController { .systemMusicPlayer }

}

And this is the result using the first method to load the playlist "Taylor Swift essentials":

Hello @eighty_six,

Thank you for the additional information.

The issue you've encountered with MusicKit's SystemMusicPlayer has the same root cause as the other issue with Media Player's MPMusicPlayerPlayParametersQueueDescriptor, the latter being the exact same issue as the one previously reported by @Tanmay007.

That said, let me reiterate something I said before.

The way you are creating instances of MPMusicPlayerPlayParameters is not recommended. The dictionary to be passed to the initializer is meant to come straight from Apple Music API's playParams attribute for Songs or other types of resources. You are not supposed to craft this dictionary yourself, manually, as you're showing it here in this snippet of code.

Regarding the issue with blank artwork, this might actually be a different one. So I would encourage you to file a new ticket on Feedback Assistant about this one, including a sysdiagnose taken shortly after reproducing the issue.

Best regards,

Thanks for the reply! Maybe I'm missing something, but how can we pass the PlayParameters obtained from a Music item to MPMusicPlayerPlayParameters? Is there a way to convert between them?

I've also submitted FB9892700 about the missing artwork issue.

Hello @eighty_six,

There is a way to convert MusicKit's PlayParameters into an instance of Media Player's MPMusicPlayerPlayParameters. But that should only be used for select scenarios that MusicKit cannot handle on its own.

As far as I can tell, in your situation, you are encountering the exact same issue with MPMusicPlayerController and MPMusicPlayerPlayParametersQueueDescriptor as the one with MusicKit's SystemMusicPlayer.

Therefore, it would be pointless for you to resort to complicated measures such as the conversion you're inquiring about.

The reality is that bringing such playback functionality to macOS does present some significant engineering challenges for us, whether with MusicKit's ApplicationMusicPlayer and SystemMusicPlayer, or play parameters based playback with MPMusicPlayerController.

For Mac Catalyst specifically, our early tests of these APIs seemed promising, so we initially didn't see any reason to restrict availability of these APIs for that environment. But evidently, we need to keep investigating issues like the ones you have uncovered to make these APIs work reliably in Mac Catalyst apps.

Best regards,

  • Okay, thanks again! Actually for me the functionality is good enough by setting the IDs to MPMusicPlayerController; the only small issue is the missing artwork, which seems to affect only tracks that have not been played or added to library. I've posted FB9892700 about this issue.

  • Hi @eighty_six,

    Thank you very much for filing this ticket on Feedback Assistant. I have redirected it to the relevant team.

    Best regards,

Add a Comment

Thanks for the super quick response @JoeKun. I tried out a few things and some observations:

  1. MPMusicPlayerPlayParameters object initialised manually using the method I mentioned or initialised using the new MusicKit APIs results in the exact same object signature. MPMusicPlayerPlayParameters existed well before MusicKit and the way to initialise it has always been manually because all responses were in JSON, I simply used the exact response format given out by official Apple Music API docs. You can check the playParams response format by checking out the response tab here: https://developer.apple.com/documentation/applemusicapi/get_a_catalog_song

  2. Even when using MusicKit, a queue set via MPMusicPlayerPlayParametersQueueDescriptor using a dictionary directly from a MusicCatalogResourceRequest exhibits the exact same behavior i.e. it completely fails to do anything on Mac Catalyst. I will attach another sample project showcasing this to the original radar FB9890331.

  3. The code provided above works perfectly on iOS.

Pt.2 and 3 prove that the way we create instances of MPMusicPlayerPlayParameters is not the actual problem but the problem lies elsewhere and is specific to Catalyst/macOS.


The sample code I provided above was just an example of the core issue I'm facing when building a working Mac Catalyst music app i.e. there is just no way to play non-library items reliably using the Media Player framework on Catalyst. I have already provided a sample project via radar FB9890270 showing the issues with MPMusicPlayerStoreQueueDescriptor where in the queue is set correctly in stock Music app (albeit without album arts) but the actual app itself shows the nowPlayingItem and queue as nil.

Going slightly off-topic, I'm really happy to see the way actual Apple devs are helping us out now. It goes a long way in instilling confidence to keep working on our apps and motivate us.

Hi @Tanmay007,

I agree with some of what you just said, and if you read my previous responses carefully, you'll notice that I never said that that the way you create instances of MPMusicPlayerPlayParameters was the cause of the problem; from the beginning, I said it was a side comment.

I also understand how the Apple Music API documentation lead you to think that it was ok for you to craft your own dictionary. I have conveyed to my colleagues working on this documentation that it's not written in a way that explains clearly how developers are meant to use play parameters.

Meanwhile, I hope you'll understand that some of my comments are providing more accurate information compared to some of this old documentation.

However, one aspect of what you said is actually incorrect (emphasis mine):

MPMusicPlayerPlayParameters existed well before MusicKit and the way to initialise it has always been manually because all responses were in JSON

This is not the only way to initialize MPMusicPlayerPlayParameters, and it's certainly not the recommended way.

The recommended way is to leverage the fact that MPMusicPlayerPlayParameters conforms to Decodable.

Here's a more complete example of what I was telling you previously:

struct MySong: Decodable {
    struct Attributes: Decodable {
        let name: String?
        let playParams: MPMusicPlayerPlayParameters?
    }
    
    let id: String
    let attributes: Attributes
}

Using such a Decodable, you can pass it to JSONDecoder, and end up with a valid instance of MPMusicPlayerPlayParameters without ever having to call the dictionary based initializer with a handcrafted dictionary.

I realize we could have done a better job of explaining this in the official documentation. But I'm telling you now.

I hope you'll agree that I'm not denying any issues we might have on our side, nor am I trying to put blame on developers unnecessarily. That is not the point. I'm just trying to give you relevant information about best practices on how to use the APIs as they were designed by the engineering teams at Apple.

Best regards,

  • Thank you for the clarification, always welcome best practices and appreciate the sample code.

    I hope you or the relevant team is able to replicate the issues, if anyone from the team needs any more info/logs please let me know.

  • Hello @Tannay007,

    Sounds great! Thanks for the tickets!

    Best regards,

Add a Comment