AVQueuePlayer playback freezes when removing items from queue - cocoa-touch

I successfully use AVQueuePlayer to queue and play AVPlayerItems. However, the problems begin when I alter the players queue with calls to removeItem: method. My assumption was that I can freely modify this queue as long as I don't touch the currently played item, but it seems that each call to removeItem: freezes the playback for a moment.
Consider the following code snippet:
+ (void)setBranch:(BranchType)bType {
NSArray* items = [dynamicMusicPlayer items];
for (AVPlayerItem* item in [items copy]) {
if (item == dynamicMusicPlayer.currentItem) {
continue; // don't remove the currently played item
}
[dynamicMusicPlayer removeItem:item];
}
[self queueNextBlockFromBranchType:bType];
currentBranch = bType;
}
You can guess from this, that it's a dynamic music player that plays the music from different branches. So basically, when I comment the line where I remove items, it plays all ok, but obviously the branch is not changed as soon as I want it to. The freeze occurs exactly at the time the removing happens, so the currently played item is being interrupted, but the transition between the actual items is seamless. Also note that I never have more than 2 items in the queue, so in the loop I basically remove one item only.
So, my question is: is there any way to avoid this freeze? And what is causing the freeze on the first place?
Edit So for the people who encountered the same problem, the solution for me was to stop using AVFoundation and use OrigamiEngine instead.

Instead of removing the item in the loop, you could consider scheduling an NSOperation to do it later.
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// do something here
}];
Ex:
+ (void)setBranch:(BranchType)bType {
NSArray* items = [dynamicMusicPlayer items];
for (AVPlayerItem* item in [items copy]) {
if (item == dynamicMusicPlayer.currentItem) {
continue; // don't remove the currently played item
}
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[dynamicMusicPlayer removeItem:item];
}];
}
[self queueNextBlockFromBranchType:bType];
currentBranch = bType;
}
I don't know if this will help, but it'll at least shift the actual item removal to a scheduled operation that the device can decide when to run.

This is a much hackier solution, but it might help.
Rather than removing the item from dynamicMusicPlayer immediately, you could mark it for removal by adding it to a separately maintained list of things to remove.
Then, when the AVQueuePlayer is about to advance to the next item, sweep through your list of deletable items and take them out. That way the blip occurs in the boundary between items rather than during playback.

Related

UICollectionViewLayout supplementary view duplicate on delete

I have been tinkering with a UICollectionViewLayout for a few days now, and seem to be stuck on getting a Supplementary view to delete with animation using the finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:
I have finally made some headway and discovered this issue. Initially it looked as if it was not animating, because i was not getting a fading animation. so i added a transform on it to get it to rotate... and discovered a supplementary view is in fact being animated, but another one just sits there. until its finished and the unceremoniously disappears not being affected by being told to disappear at all. So to sum up it looks like an exact copy is made and that is animated out for finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath: and the original just sits there unmoving and animating, then both disappear.
This copy or whatever it is does not disappear either until i scroll the view.
Does anyone have any insight into this issue?
Update
So after some tweaking, experimentation and many hypothesis later as to what was causing the issue, it turns out that the culprit was this method
- (NSArray *)indexPathsToDeleteForSupplementaryViewOfKind:(NSString *)kind
{
NSMutableArray *deletedIndexPaths = [NSMutableArray new];
[self.updatedItems enumerateObjectsUsingBlock:^(UICollectionViewUpdateItem *updateItem, NSUInteger idx, BOOL * _Nonnull stop)
{
switch (updateItem.updateAction)
{
case UICollectionUpdateActionInsert:
{
if (_unReadHeaderIndexPath != nil && [self.layoutInformation[ChatroomLayoutElementKindUnreadHeader] count] == 0)
{
[deletedIndexPaths addObject:_unReadHeaderIndexPath];
_unReadHeaderIndexPath = nil;
}
}
break;
case UICollectionUpdateActionDelete:
{
[deletedIndexPaths addObject:updateItem.indexPathBeforeUpdate];
}
break;
default:
break;
}
}];
return deletedIndexPaths;
}
The part to focus on is here
if (_unReadHeaderIndexPath != nil && [self.layoutInformation[ChatroomLayoutElementKindUnreadHeader] count] == 0)
{
[deletedIndexPaths addObject:_unReadHeaderIndexPath];
_unReadHeaderIndexPath = nil;
}
Particularly the _unReadHeaderIndexPath = nil;
What this is doing is making a supplementary view disappear which indicates unread messages when the user inserts any text and then setting it to nil so it is not added multiple times, the logic is sound however the method in which is employed seems to be the issue.
indexPathsToDeleteForSupplementaryViewOfKind: is called multiple times and one can assume it needs multiple passes through it to work properly but since we nil _unReadHeaderIndexPath; it seems like it isn't added to the returned array at the appropriate time.
It also seems like the incorrect method in as I'm checking for insertion action in a method name indexPathsToDeleteForSupplementaryViewOfKind: so the solution was to move all of that logic to the method
prepareForCollectionViewUpdates:
Where we originally set the values for self.updatedItems in the first place. Which means i can get rid of that member variable altogether. And it animates and disappears without any odd copy sticking around.

AVAudioPlayer Number Of Loops only taking effect after being played through once

I'm a little stumped by this weird occurrence:
I have a UIButton, which once tapped either sets a loop for an audio player, or resets it to 0 (no loop). Here is the method -
-(void)changeLoopValueForPlay:(int)tag toValue:(bool)value{
AVAudioPlayer *av = [self.playerArray objectAtIndex:tag];
if(value){
[av setNumberOfLoops:100];
[av prepareToPlay];
}
else{
[av setNumberOfLoops:0];
}
}
Now for some reason, the loop will only take effect after the player plays through the audio one time, meaning that the looping value doesn't take affect immediately, but the "numberOfLoops" value of the player is in fact set to 100 when I check its value before playing. I'm assuming this has something to do with the initialization or loading of the player, but I don't re-initialize it between those two plays (one without loop, the other with). Any idea why this is happening? If you want to see any other code please let me know.
This fixed the problem, however I feel as if this is a work-around instead of a direct solution. What I did is just create a new AVAudioPlayer with the numberOfLoops value set to whatever it is I wanted and replace that player with the existing player, instead of changing the value of the already existing player.
I workaround the issue by abandoning numberOfLoops altogether and doing my own logic instead.
First, set the delegate of the AVAudioPlayer:
self.audioPlayer.delegate = self;
Next, implement -audioPlayerDidFinishPlaying:successfully: of the delegate:
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer*)player successfully:(BOOL)flag
{
if(flag && <#(bool)i_want_to_repeat_playing#>)
{
[self.audioPlayer play];
}
}
Just replace <#(bool)i_want_to_repeat_playing#> with your desired logic, e.g., check if a counter has reached a certain threshold.

In for Loop, inside GCD, pause, get input, continue

I want to know if there was a way to break away from gcd, show in input alert view and then return to the process? THis was my normal routine:
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSMutableArray *array = [NSMutableArray new];
for(NSIndexPath * ip in [self.tableView indexPathsForSelectedRows]){
[array addObject:[CompanyObjectsArray objectAtIndex:ip.row]];
}
//I need to check for a an Object here, if present get input from User.
[self addCompaniesOrCreate:[NSArray arrayWithArray:array]];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:self.view animated:YES];
});
});
I add all objects into core data like that,
But I had a need where'd if a company is "Appl3" , i should get an input from the User, and if i get it, continue and loop with the rest, or else skip the current "Appl3" company.
Is there a way i could do this efficiently, while in the for() loop, pause, get input, continue the for() loop.
thanks for the help guys :)
There is no such way I am aware of. The usual pattern would be to write your own method that takes a block as a completion handler. So basically you pass back to the main thread the block that you want to run when you've got your user input you wanted.
Best regards,
sven.

Perform action after label is updated in ios

I am using a pin screen for login to my app. The pin screen consists of four labels and a hidden text field. When the user enters text via the keypad, I update the labels with a symbol. This works fine, except that the last label does not get actually get updated before login begins, and remains empty while the login process is completed.
These are the relevant bits of code:
//an observer has been added elsewhere
- (void)textDidChange:(NSNotification *)notification
{
UITextField *field = [notification object];
if (field == inputField)
{
NSString *newText = field.text;
if ([newText length] <= pinLength) [self updatePINDisplay];
}
}
-(void)updatePINDisplay
{
if ([pinText length] > pinLength) return;
for (NSInteger ii = 0; ii < [pinText length]; ii++)
{
UILabel *label = [pinFields objectAtIndex:ii];
[label setText:#"x"];
}
for (NSInteger ii = [pinText length]; ii < pinLength; ii++)
{
UILabel *label = [pinFields objectAtIndex:ii];
[label setText:[NSString string]];
}
if ([pinText length] == pinLength) [self login];
}
The problem arises because [self login] launches other processes which happen before the last pin label is updated, so the login occurs while the last box is still empty.
I have worked around the problem by replacing
[self login]
with
[self performSelector:#selector(login) withObject:nil afterDelay:0.1]
but I don't like the arbitrary time delay. I was hoping that maybe there was a delegate method that I could use to launch my login code after the label has been drawn. Something like:
-(void)labelDidGetDrawn
Any other (non-hack) solution is also welcome:-)
Thanks!
Based on your description, it sounds like the problem is that the 4th item doesn't get drawn until after the [self login] finishes, which is indicative that the login procedure takes some time. In iOS, drawing doesn't happen immediately, which is why you're only getting the draw if you defer the login until after the OS has an opportunity to update the display.
You have used one reasonable solution here. Another (arguably less of a hack) is to have your -[self login] spawn the login on a separate thread, or at least using an asynchronous mechanism (such as the asynchronous modes of NSURLConnection, assuming you're making a network request). Then your main thread will quickly return control to iOS and your box will draw.
With Grand Central Dispatch, you could do most of this by having the -[self login] place the network code on a background thread, and have the background thread call back to your main thread when complete. However, this can cause some problems if you want to respond to user events during the login process.
If you can, using NSURLConnection asynchronously, and setting up the delegate to report back to you when the operation is complete is probably the best choice, as it gives you the operation to cancel the NSURLConnection during the login process if the user requests it.
How about:
[label setNeedsDisplay:YES];
if ([pinText length] == pinLength) [self login];
Yes, that notification exists, in a way. The label will be drawn during the next iteration of the run loop. So do your login at the end of the next run loop iteration, for instance using a performSelector:afterDelay:0 or maybe using
dispatch_async (dispatch_get_main_queue (), ^{ [self login]; });
But a) this depends on the order of execution of rendering versus timers and dispatch_queues. If rendering happens before timer execution, you're all set.
And b) don't block the main thread. Try to perform the login in a background thread/concurrent queue, or do it asynchronously on the main thread if you're using, e.g., NSURLConnection.

NSFetchedResultsController not displaying changes from background thread

My app is using an NSFetchedResultsController tied to a Core Data store and it has worked well so far, but I am now trying to make the update code asynchronous and I am having issues. I have created an NSOperation sub-class to do my updates in and am successfully adding this new object to an NSOperationQueue. The updates code is executing as I expect it to and I have verified this through debug logs and by examining the SQLite store after it runs.
The problem is that after my background operation completes, the new (or updated) items do not appear in my UITableView. Based on my limited understanding, I believe that I need to notify the main managedObjectContext that changes have occurred so that they may be merged in. My notification is firing, nut no new items appear in the tableview. If I stop the app and restart it, the objects appear in the tableview, leading me to believe that they are being inserted to the core data store successfully but are not being merged into the managedObjectContext being used on the main thread.
I have included a sample of my operation's init, main and notification methods. Am I missing something important or maybe going about this in the wrong way? Any help would be greatly appreciated.
- (id)initWithDelegate:(AppDelegate *)theDelegate
{
if (!(self = [super init])) return nil;
delegate = theDelegate;
return self;
}
- (void)main
{
[self setUpdateContext:[self managedObjectContext]];
NSManagedObjectContext *mainMOC = [self newContextToMainStore];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:#selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:updateContext];
[self setMainContext:mainMOC];
// Create/update objects with mainContext.
NSError *error = nil;
if (![[self mainContext] save:&error]) {
DLog(#"Error saving event to CoreData store");
}
DLog(#"Core Data context saved");
}
- (void)contextDidSave:(NSNotification*)notification
{
DLog(#"Notification fired.");
SEL selector = #selector(mergeChangesFromContextDidSaveNotification:);
[[delegate managedObjectContext] performSelectorOnMainThread:selector
withObject:notification
waitUntilDone:YES];
}
While debugging, I examined the notification object that is being sent in contextDidSave: and it seems to contain all of the items that were added (excerpt below). This continues to make me think that the inserts/updates are happening correctly but somehow the merge is not being fired.
NSConcreteNotification 0x6b7b0b0 {name = NSManagingContextDidSaveChangesNotification; object = <NSManagedObjectContext: 0x5e8ab30>; userInfo = {
inserted = "{(\n <GCTeam: 0x6b77290> (entity: GCTeam; id: 0xdc5ea10 <x-coredata://F4091BAE-4B47-4F3A-A008-B6A35D7AB196/GCTeam/p1> ; data: {\n changed =
The method that receives your notification must indeed notify your context, you can try something like this, which is what I am doing in my application:
- (void)updateTable:(NSNotification *)saveNotification
{
if (fetchedResultsController == nil)
{
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
//Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
}
else
{
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
// Merging changes causes the fetched results controller to update its results
[context mergeChangesFromContextDidSaveNotification:saveNotification];
// Reload your table view data
[self.tableView reloadData];
}
}
Hope that helps.
Depending on the specifics of what you are doing, you may be going about this the wrong way.
For most cases, you can simply assign a delegate using NSFetchedResultsControllerDelegate. You provide an implementation for one of the methods specified in "respondingToChanges" depending on your needs, and then send the tableView a reloadData message.
The answer turned out to be unrelated to the posted code which ended up working as I expected. For reasons that I am still not entirely sure of, it had something to do with the first launch of the app. When I attempted to run my update operation on launches after the Core Data store was created, it worked as expected. I solved the problem by pre-loading a version of the sqlite database in the app so that it did not need to create an empty store on first launch. I wish I understood why this solved the problem, but I was planning on doing this either way. I am leaving this here in the hope that someone else may find it useful and not lose as much time as I did on this.
I've been running into a similar problem in the simulator. I was kicking off an update process when transitioning from the root table to the selected folder. The update process would update CoreData from a web server, save, then merge, but the data didn't show up. If I browsed back and forth a couple times it would show up eventually, and once it worked like clockwork (but I was never able to get that perfect run repeated). This gave me the idea that maybe it's a thread/event timing issue in the simulator, where the table is refreshing too fast or notifications just aren't being queued right or something along those lines. I decided to try running in Instruments to see if I could pinpoint the problem (all CoreData, CPU Monitor, Leaks, Allocations, Thread States, Dispatch, and a couple others). Every time I've done a "first run" with a blank slate since then it has worked perfectly. Maybe Instruments is slowing it down just enough?
Ultimately I need to test on the device to get an accurate test, and if the problem persists I will try your solution in the accepted answer (to create a base sql-lite db to load from).