I'm trying to use the new Apple Music APIs from 9.3 to add a song to a playlist created by my app, without adding it to the user's library.
Consider the productID 316654632, it's the song Lisztomania by Phoenix, in the US iTunes Store.
Using the following code, I can play the song
MPMusicPlayerController *musicPlayer = [MPMusicPlayerController systemMusicPlayer];
[musicPlayer setQueueWithStoreIDs:#[#"316654632"]];
[musicPlayer play];
Using the following code, I can add the song to my Apple Music library
[[MPMediaLibrary defaultMediaLibrary] addItemWithProductID:#"316654632" completionHandler:^(NSArray<__kindof MPMediaEntity *> * _Nonnull entities, NSError * _Nullable error) {
NSLog(#"%#", error);
}];
Error is nil, and I can see the song in my library.
But trying the same with a playlist doesn't work.
[[MPMediaLibrary defaultMediaLibrary] getPlaylistWithUUID:uuid creationMetadata:[[MPMediaPlaylistCreationMetadata alloc] initWithName:#"Test Playlist"] completionHandler:^(MPMediaPlaylist * _Nullable playlist, NSError * _Nullable error) {
NSLog(#"%#", error);
if (!error) {
[playlist addItemWithProductID:#"316654632" completionHandler:^(NSError * _Nullable error) {
NSLog(#"%#", error);
}];
}
}];
The playlist is created, I can see it in Music.app, but when I try to add the same product ID I played & added to my library to the playlist, I get an error
Error Domain=MPErrorDomain Code=4 "The requested id could not be found" UserInfo={NSLocalizedDescription=The requested id could not be found}
But how could it not be found if I successfully added the same item to my library?
UPDATE
Good news! Apple has fixed rdar://26408683 on 10.2.1!
In my playlist conversion app (mixlib), the only solution I have found to reliably add some tracks to a newly created playlist is to wait.
In my tests, waiting five seconds seems to be enough.
[[MPMediaLibrary defaultMediaLibrary] getPlaylistWithUUID:uuid creationMetadata:[[MPMediaPlaylistCreationMetadata alloc] initWithName:#"Test Playlist"] completionHandler:^(MPMediaPlaylist * _Nullable playlist, NSError * _Nullable error) {
if (!error) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 /*seconds*/ * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)), ^() {
[playlist addItemWithProductID:#"316654632" completionHandler:^(NSError * _Nullable error) {
NSLog(#"%#", error);
}];
}
}];
I suspect it is a server/network related issue because it sometimes works without waiting. The "requested id" which is not found may be the playlist id, not the track id.
When it starts to work for a playlist, then it will always work. So you don't need to wait before adding each additional track, but only before adding the first one.
Related
i´m screen recording with ReplayKit and when the delegate method previewController(_:didFinishWithActivityTypes:) is called when i click the save button, it automatically saves into the camera roll. How can i change that? I want to save the video somwhere on the filesystem on device. I searched all over google and in apple documentation, but i couldn´t find anything relating to that.
If your app supports the app FILES, the replay kit can save file directly to the folder. For example VLC app has a folder in FILES. Otherwise you may need to implement an action extension to do the similar thing.
use this API
- (void)startCaptureWithHandler:(nullable void (^)(CMSampleBufferRef sampleBuffer, RPSampleBufferType bufferType, NSError *_Nullable error))captureHandler completionHandler:(nullable void (^)(NSError *_Nullable error))completionHandler API_AVAILABLE(ios(11.0), tvos(11.0), macos(11.0));
sample code:
-(void)startCapture {
if (#available(iOS 11.0, *)) {
[[RPScreenRecorder sharedRecorder] startCaptureWithHandler:^(CMSampleBufferRef _Nonnull sampleBuffer, RPSampleBufferType bufferType, NSError * _Nullable error) {
if (CMSampleBufferDataIsReady(sampleBuffer) && bufferType == RPSampleBufferTypeVideo) {
NSLog(#"Recording started successfully.");
//save using AVAssetWriter
}
} completionHandler:^(NSError * _Nullable error) {
if (!error) {
NSLog(#"Recording started successfully.");
}else{
NSLog(#"Recording started error %#",error);
}
}];
} else {
// earlier versions issue
}
}
Also: this post may help u somehow
AVAssetWriter Doc
I'm trying to incorporate GKGameSession into my Game Center game. I've tried several combinations of the following code: running the commands asynchronously, chaining them in the completion handlers, etc. Every time I see the same result: I can use saveData just fine until I've called getShareURLWithCompletionHandler. After that, any attempt to saveData throws an error.
Here's the simplest version of code that exhibits the problem:
CKContainer *defaultContainer = [CKContainer defaultContainer];
[GKGameSession createSessionInContainer:defaultContainer.containerIdentifier
withTitle:#"temp title"
maxConnectedPlayers:4
completionHandler:^(GKGameSession * _Nullable session, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
[session getShareURLWithCompletionHandler:^(NSURL * _Nullable url, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
}];
NSData *newData = [NSData dataWithBytesNoCopy:#"abcdefghijklmnopqrstuvwxyz" length:26];
[reSession saveData:newData completionHandler:^(NSData * _Nullable conflictingData, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
}];
}];
In most cases, the saveData call simply crashes:
malloc: *** error for object 0x32df14: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
But sometimes it throws an error:
GKGameSessionErrorDomain:GKGameSessionErrorUnknown
I've tried different kinds of data being saved. I've tried making the calls sequential by chaining all the calls in completion handlers. I've tried doing the URL fetch and data save inside and outside of the creationSession completion handler.
Is there something I'm doing wrong here?
I see the same, but with a more useful error:
The requested operation could not be completed because the session has been updated on the server, causing a conflict.
The save documentation says,
It is up to the developer to decide how to handle save conflicts.
Here though, retrying the save fails every time, forever. So yeah, that's the same state you're in.
However, when the player joining the game enters the URL on their device, their GKGameSessionEventListener's didAddPlayer: is called, and then if they save... they get the same conflict error, but if they then retry the save... it works!
The player creating the link is locked out of saving or updating game state, until joining players have updated the data. When the other player saves, the original player gets a call to session:player:didSave: on the GKGameSessionEventListener.
At that point the original player can then save as expected.
You should put one block inside other. Because blocks may be completed in any order.
I have working code like this:
NSData *newData = [NSData dataWithBytesNoCopy:#"abcdefghijklmnopqrstuvwxyz" length:26];
[reSession saveData:newData completionHandler:^(NSData * _Nullable conflictingData, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
} else {
[session getShareURLWithCompletionHandler:^(NSURL * _Nullable url, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
}];
}
}];
My goal is to be able to save a record using CloudKit when my app gets backgrounded. I first fetch the record I'm trying to modify, modify it, then save it. I'm making this call from my app delegate's applicationDidEnterBackground:
CKDatabase *db = [[CKContainer defaultContainer] privateCloudDatabase];
CKRecordID *recordId = [[CKRecordID alloc] initWithRecordName:#"record_a"];
[db fetchRecordWithID:recordId completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
// Doesn't get here until I foreground the app
if (!record) // Doesn't exist yet so create it
{
record = [[CKRecord alloc] initWithRecordType:#"SaveData" recordID:recordId];
}
NSURL *fileURL = [NSURL fileURLWithPath:#"path/to/file"];
CKAsset *saveAsset = [[CKAsset alloc] initWithFileURL:fileURL];
record[#"saveAsset"] = saveAsset;
[db saveRecord:record completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable saveError) {
// Doesn't get here until I foreground
}];
}];
The completion block for the fetchRecordWithID call doesn't get fired until I foreground the app again, meaning the saveRecord: call doesn't get called until then either.
I've tried using the CKOperation version of fetch and save with modified qualityOfService and queuePriority properties to no avail. Also tried setting the longLived property, but that didn't work either. I've scoured the docs and couldn't find anything that should stop the block from being called when the app is backgrounded.
I have other code that can call through their completion blocks when in the background, so not sure why this would be limited. Any ideas?
I'm trying to start a new GKGameSession and when I use createSession, all I can get so far is nil. Here's my code:
GKGameSession.createSession(inContainer: "test", withTitle: "MyGame", maxConnectedPlayers: 8)
{ (newGameSession, error) in
self.gameSession = newGameSession
print("\(newGameSession)")
newGameSession?.getShareURL(completionHandler: { (url, error) in
print("url: \(url) error: \(error)")
})
}
The only thing it prints is "nil". Any help is appreciated.
If you using the emulator, I suggest using a device instead. GKGameSessions do not play well with the emulator, because they depend on push notifications and an iCloud account that is logged in.
newGameSession is optional. So it seems like something has gone wrong when creating a new session.
I would say newGameSession is likely nil, in that case error will hopefully contain some useful information.
Try replacing print("\(newGameSession)") with print(newGameSession, error) to see what the error var has to say, or set a breakpoint if you know how to do that.
Try to use your app's iCloud container name instead of "test". The container name will be in the format iCloud.com.yourcompany.appname if you have selected the default container option. To ensure your app has an iCloud container you need to enable it in your app's Capabilities.
I'm a jumping in a little late, but I'd triple check the container name you're using to create the session.
I enabled all three options in xCode: key-value storage, iCloud documents and CloudKit.
I don't use iCloud drive.
The following code successfully creates a new session each time I send invites. It prints the new session, then iterates through all existing sessions for that container ID.
-(void)turnBasedMatchmakerViewController:(GKTurnBasedMatchmakerViewController *)viewController didFindMatch:(GKTurnBasedMatch *)match
{
[self dismissViewControllerAnimated:YES completion:nil];
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
NSString *iCloudContainerName = [#"iCloud." stringByAppendingString: bundleIdentifier];
[GKGameSession createSessionInContainer:iCloudContainerName
withTitle:#"test"
maxConnectedPlayers:4
completionHandler:^(GKGameSession * _Nullable session, NSError * _Nullable error)
{
NSLog(#"(1) Session: %#, Error: %#", session.identifier, [error description]);
[GKGameSession loadSessionsInContainer:iCloudContainerName
completionHandler:^(NSArray<GKGameSession *> * _Nullable sessions, NSError * _Nullable error)
{
for (GKGameSession *session in sessions)
{
NSLog(#"(2) Session: %#, Error: %#", session.identifier, [error description]);
}
NSLog(#"-----");
}];
}];
}
In cocoa touch 4.x Its possible create or edit a music playlist into the default/bundle music app for iphone/ipad? if it is, how will be the code?
Finally, I found the solution for iOS 9.3 and above. You can use MPMediaLibrary to achieve that.
// Setup your playlist metadata
__auto_type metadata = [[MPMediaPlaylistCreationMetadata alloc] initWithName:#"Playlist Name"];
metadata.authorDisplayName = #"My app name";
metadata.descriptionText = #"Playlist description";
// Save it to get the same playlist next time
NSUUID *uuid = [NSUUID UUID];
// Items to add to your playlist
NSArray<MPMediaItem *> *mediaItems = self.items;
// Get or create your playlist in Apple Music
[[MPMediaLibrary defaultMediaLibrary] getPlaylistWithUUID:uuid creationMetadata:metadata completionHandler:^(MPMediaPlaylist * _Nullable playlist, NSError * _Nullable error) {
// Handle the error
NSLog(#"Got error — %#", error);
// You can add items to your playlist
[playlist addMediaItems:mediaItems completionHandler:^(NSError * _Nullable addError) {
// Handle the error
NSLog(#"finished! error — %#", addError);
}];
}];
You can access information from the iPod Library (iPod Library Access Programming Guide) by creating MPMediaQuerys. This will allow you to retrieve playlists (using the MPMediaPickerControllerDelegate) but I don't think you'll be able to create playlists as you are only given read-only access (as well as playback).