seekToTime messes up simultaneous playback of two AVPlayers - objective-c

Following problem. I have two AVPlayers, each initialized with a different AVPlayerItem.
Once the AVPlayerItem is successfully loaded, I add the AVPlayerLayer to the view layer. As you can see in the code below.
- (void)viewDidLoad
{
[super viewDidLoad];
self.playerItem = [[AVPlayerItem alloc] initWithURL:[self.video getVideoPath]];
[self.playerItem addObserver:self forKeyPath:#"status" options:0 context:nil];
[self.playerItem addObserver:self forKeyPath:#"playbackBufferEmpty" options:0 context:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(videoEnded)
name:AVPlayerItemDidPlayToEndTimeNotification
object:nil];
self.player = [[AVPlayer alloc] initWithPlayerItem:playerItem];
self.overlayPlayerItem = [[AVPlayerItem alloc] initWithURL:[self.compareVideo getVideoPath]];
[self.overlayPlayerItem addObserver:self forKeyPath:#"status" options:0 context:nil];
[self.overlayPlayerItem addObserver:self forKeyPath:#"playbackBufferEmpty" options:0 context:nil];
if (self.overlayPlayer==nil) {
self.overlayPlayer = [[AVPlayer alloc] initWithPlayerItem:self.overlayPlayerItem];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([object isKindOfClass:[AVPlayerItem class]])
{
AVPlayerItem *item = (AVPlayerItem *)object;
//playerItem status value changed?
if ([keyPath isEqualToString:#"status"])
{ //yes->check it...
switch(item.status)
{
case AVPlayerItemStatusFailed:
NSLog(#"player item status failed");
break;
case AVPlayerItemStatusReadyToPlay:
{
NSLog(#"player item status is ready to play");
if (item == self.playerItem && !videoLayerAdded) {
videoLayerAdded = YES;
AVPlayerLayer* layer = [AVPlayerLayer playerLayerWithPlayer:player];
layer.frame = self.videoContainer.bounds;
[self.videoContainer.layer insertSublayer:layer atIndex:0];
} else if (item == self.overlayPlayerItem && !overlayLayerAdded){
overlayLayerAdded = YES;
AVPlayerLayer* layer = [AVPlayerLayer playerLayerWithPlayer:overlayPlayer];
layer.frame = self.videoOverlayContainer.bounds;
[self.videoOverlayContainer.layer insertSublayer:layer atIndex:0];
}
break;
}
case AVPlayerItemStatusUnknown:
NSLog(#"player item status is unknown");
break;
}
}
else if ([keyPath isEqualToString:#"playbackBufferEmpty"])
{
if (item.playbackBufferEmpty)
{
NSLog(#"player item playback buffer is empty");
}
}
}
}
When I hit the play button the videos get played. If I hit it again they stop, so far so good.
- (IBAction)playButtonClicked:(id)sender{
//is playing
if(self.player.rate>0.0 || self.overlayPlayer.rate>0.0){
[self.player pause];
[self.overlayPlayer pause];
} else {
[self.player play];
[self.overlayPlayer play];
}
}
I have two UISlider that allow me to go forth and back through each video. I use seekToTime to jump to a certain time in a video.
- (IBAction)sliderChanged:(id)sender{
UISlider *slider = (UISlider *)sender;
//stop any video when using the slider
if(self.player.rate>0.0){
[self.player pause];
}
if(self.overlayPlayer.rate>0.0){
[self.overlayPlayer pause];
}
if (slider.tag == 1) {
double time = (self.playerTimeSlider.value * CMTimeGetSeconds(self.player.currentItem.asset.duration));
[self.player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
} else if (slider.tag == 2){
double time = (self.overlayTimeSlider.value * CMTimeGetSeconds(self.overlayPlayer.currentItem.asset.duration));
[self.overlayPlayer seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
}
Videos can have different length as well. However, if a video ends, I set it back to the start, also with seekToTime.
My problem is that I can initially play both videos. I can pause and resume them without problem. But once I pause and use seekToTime for any video and resume or if the videos are at the end I reset them with seekToTime and hit play again my status observer fires AVPlayerItemStatusFailed for both PlayerItems.
Then the duration of those items becomes 0. I checked though, the PlayerItems are not nil.
I don't get a crash but I just can't play the videos anymore. When I only use one player I can jump through it with seekToTime and also reset it at the end of the video without problem.
I read through the forum and people say you can apparently use up to 4 AVPlayers, so I guess I should be save with 2. I also read about that other apps can use up video render pipelines, but I made sure that I don't have any other apps in the background on my iPad.
Any help as to why this is not working for me or even better, a fix, is highly appreciated.
Update:
I actually logged the error from the AVPlayerItem object now:
error: Error Domain=AVFoundationErrorDomain Code=-11819 "Cannot Complete Action"
UserInfo=0x19e370 {NSLocalizedRecoverySuggestion=Try again later.,
NSLocalizedDescription=Cannot Complete Action}
The code -11819 stands for AVErrorMediaServicesWereReset. Does that help anyone to help me?

This might be a long shot but take a look at the following page of the documentation:
AV Foundation Programming Guide - Seeking—Repositioning the Playhead
My hint is that by giving a tolerance of zero (before and after) you are probably causing the decoders to work their way out of a key frame calculating the exact requested frame.
This, in conjunction with playing two items at the same time, can cause the exhaustion of resources allocated to the iOS internal Media Services. Thus causing a reset.
I guess it depends on numerous factors being the most important the quality and format of the videos being played.
If you do not require precise time control in the scrubbing (!) try to play with the tolerance. I suggest that you try with values slightly larger than the maximum keyframe interval.
That value (the maximum keyframe interval) is, most likely, movie dependent.
If you have control of the movies and can live with insane movie file sizes try to export them using a max keyframe interval of 1 (!) and give it a spin.

Related

Wait until UI has updated before removing UIActivityIndicator

I am having an issue with an iPad app I am developing. I have the following code:
CFTimeInterval startTime = CFAbsoluteTimeGetCurrent();
[self showSpinnerWithMessage:#"Loading settings..."]; // Shows a UIActivityIndicator on screen
dispatch_async(dispatch_get_global_queue(0, 0), ^
{
//Updating items on screen here
[label1 setText:#"Test1"];
[label2 setText:#"Test3"];
//...
CFTimeInterval difference = CFAbsoluteTimeGetCurrent() - startTime;
if (difference < MINIMUM_INDICATOR_SECONDS)
{
[NSThread sleepForTimeInterval:MINIMUM_INDICATOR_SECONDS - difference];
}
dispatch_async(dispatch_get_main_queue(), ^(void)
{
[self removeSpinner]; // Removes UIActivityIndicator from screen
});
};
The problem is that sometimes the UI takes a while to update, and in these particular instances the spinner (UIActivityIndicator) goes away before the UI has actually updated. I realize this is because the items on screen do not actually update until the next run loop so my question is, how can make my [self removeSpinner] call wait until the UI has updated?
Try putting the UI-updating code in the block with removeSpinner.
dispatch_async(dispatch_get_main_queue(), ^(void)
{
[label1 setText:#"Test1"];
[label2 setText:#"Test3"];
[self removeSpinner]; // Removes UIActivityIndicator from screen
});
Put a [self.view setNeedsDisplay]; just before the [self removeSpinner].

Keyboard hiding UIView iOS

I know this question has been asked a few times. I understand how to move the view up or a textfield up when the keyboard is presented, however I am running into a glitch that I cannot seem to figure out.
The view I am trying to move up is inside a UIViewController that acts as a container for two views. This container is itself a view inside a sliding view controller (kind of like the one Facebook implements).
The text field moves up fine when you first load the view however if you go to a different view and come back, the keyboard causes the view to disappear.
Here is the code I am using to move the view up:
- (void) animateTextField:(BOOL)up {
const int movementDistance = 350; // tweak as needed
const float movementDuration = 0; // tweak as needed
int movement = (up ? -movementDistance : movementDistance);
[UIView beginAnimations: #"anim" context: nil];
[UIView setAnimationBeginsFromCurrentState: YES];
[UIView setAnimationDuration: movementDuration];
self.sendingToolbar.frame = CGRectOffset(self.sendingToolbar.frame, 0, movement);
self.messagesTable.frame = CGRectOffset(self.messagesTable.frame, 0, movement);
//[splitController moveViewUp:up];
[UIView commitAnimations];
}
- (void)registerForKeyboardNotifications
{
NSLog(#"was this method called");
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWasShown:)
name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillBeHidden:)
name:UIKeyboardWillHideNotification object:nil];
}
// Called when the UIKeyboardDidShowNotification is sent.
- (void)keyboardWasShown:(NSNotification*)aNotification
{
[self animateTextField:YES];
}
// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillBeHidden:(NSNotification*)aNotification
{
[self animateTextField:NO];
}
Any ideas why this glitch might be happening? Thanks
After adding the suggestion for a boolean to check if the text field is already up the view disappears all the time when the keyboard is shown and not just when you leave the view and come back. Here is the revised method:
- (void) animateTextField:(BOOL)up {
const int movementDistance = 350; // tweak as needed
const float movementDuration = 0; // tweak as needed
int movement = movementDistance;
[UIView beginAnimations: #"anim" context: nil];
[UIView setAnimationBeginsFromCurrentState: YES];
[UIView setAnimationDuration: movementDuration];
if(textFieldIsUp && (up == YES)) {
NSLog(#"Case 1");
// Do nothing since text field is already up
}
else if(textFieldIsUp && (up == NO)) {
NSLog(#"Case 2");
// Move the text field down
self.sendingToolbar.frame = CGRectOffset(self.sendingToolbar.frame, 0, -movement);
self.messagesTable.frame = CGRectOffset(self.messagesTable.frame, 0, -movement);
textFieldIsUp = NO;
}
else if((textFieldIsUp == NO) && (up == YES)) {
NSLog(#"Case 3");
// Move the text field up
self.sendingToolbar.frame = CGRectOffset(self.sendingToolbar.frame, 0, movement);
self.messagesTable.frame = CGRectOffset(self.messagesTable.frame, 0, movement);
textFieldIsUp = YES;
}
else if((textFieldIsUp == NO) && (up == NO)) {
NSLog(#"Case 4");
// Do nothing since the text field is already down
}
else {
NSLog(#"Default");
// Default catch all case. Does nothing
}
[UIView commitAnimations];
}
Here are some more details about my setup:
The view controller is a messaging center for the app. The view controller contains two subviews the subview on the left side is a menu for picking the conversation and the right menu is a subview with the messages within that conversation. Since I want the messages to load from the bottom up, the table view is rotated 180 degrees and the cells are also rotated 180 degrees the opposite direction. Also the table view reloads every 5 seconds using an NSTimer so that the messages can be updated with any new messages.
The textfield has already been moved up but didn't move back down when you left the view. When you revisit the view it gets moved up again - and off the screen.
I've had this problem before; solved it by keeping a boolean variable the indicated whether the textfield was in the up or down position. Check the variable to make sure the textfield isn't already up before you move it up again.
How I did it. I'm using an NSNumber property on my view to store whether the view has been pushed up or not so that other views can communicate whether they have pushed the view up or down.
//In viewDidLoad
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardDidShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardDidHide:) name:UIKeyboardWillHideNotification object:nil];
//Pushes the view up if one of the table forms is selected for editing
- (void) keyboardDidShow:(NSNotification *)aNotification
{
if ([isRaised boolValue] == NO)
{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.25];
self.view.center = CGPointMake(self.view.center.x, self.view.center.y-moveAmount);
[UIView commitAnimations];
isRaised = [NSNumber numberWithBool:YES];
}
}
//Push view back down
- (void) keyboardDidHide:(NSNotification *)aNotification
{
if ([isRaised boolValue])
{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.25];
self.view.center = CGPointMake(self.view.center.x, self.view.center.y+moveAmount);
[UIView commitAnimations];
isRaised = [NSNumber numberWithBool:NO];
}
}
-EDIT-
Looking at your code it appears that you're subtracting from the y-coordinate of a frame to move frame down. The iOS coordinate system for CGRect is not the same as a normal coordinate system - the y-axis is flipped (this is relatively common for graphic systems). You're going to want to do the opposite of what you're doing.

dismissViewControllerAnimated doesn't dismiss using ZbarSDK qr code reader

I'm using the zbarSDK QR code reader http://zbar.sourceforge.net/iphone/sdkdoc/
The SDK is very great but i encountered a little problem. I need the user to visualize a view before to start scanning (containing information about how to scan), than him press a button which made the scan start (showing the camera) and when the qr code has been scanned to Segue to another View showing something linked to the specific qr code.
I've done this, but it goes all well when i frame the qr code after starting the scan, but not if when i start the scan the qr code is already framed in the videocamera view.
When i start scan i do this
-(IBAction)StartScan:(id) sender
{
ZBarReaderViewController *reader = [ZBarReaderViewController new];
reader.readerDelegate = self;
reader.readerView.torchMode = 0;
ZBarImageScanner *scanner = reader.scanner;
// present and release the controller
[self presentModalViewController: reader
animated: YES];
[reader release];
}
using
[self presentModalViewController: reader
animated: YES];
to show the videocamera and scan the qr code.
and then when the qr code has been scanned i do this:
- (void) imagePickerController: (UIImagePickerController*) reader didFinishPickingMediaWithInfo: (NSDictionary*) info
{
id<NSFastEnumeration> results = [info objectForKey: ZBarReaderControllerResults];
ZBarSymbol *symbol = nil;
for(symbol in results)
hiddenData=[NSString stringWithString:symbol.data];
[reader dismissViewControllerAnimated:YES completion:^{ NSLog(#"Test"); }];
[self performSegueWithIdentifier:#"aDettaglioOpera" sender:self];
}
dismissing the camera view with
[reader dismissViewControllerAnimated:YES completion:^{ NSLog(#"Test"); }];
and presenting the view linked to the following segue.
Now all goes well if a point the camera to a point where there is not the qrcode and then point it to the qrcocde, instead if when i start scan i have already a qr code in the frame of the camera the imagePickerController get executed (i checked) but the dismissViewControllerAnimated:YES doesn't dismiss anything and doesn't execute the block after "completion" (which is instead correctly executed and nslogging "test")
What's the problem? The presentModalViewController has not the time it needs to allow the dismissViewControllerAnimated to function? And if the problem is this how can i avoid it?
I had the same problem and found a work around for this.
Set scanCrop property in reader as below before presentViewController and set it back back to default (0, 0, 1, 1) after 1 second. It works!!!
reader.scanCrop = CGRectMake(0, 0, 0.5, 0.5);
[self performSelector:#selector(changeScanCrop) withObject:nil afterDelay:1.0];
-(void)changeScanCrop {
reader.scanCrop = CGRectMake(0, 0, 1, 1);
}

MPMoviePlayerViewController can't set current playback time

I am trying to set currentPlaybackTime property of the MPMoviePlayerController in MPMoviePlayerViewController to make it resume playing video (HLS stream) from the time, it was stopped when the app resigned active. Here's my code:
//the functinon that sets playback time
- (void)setCurrentPlayTime:(NSNumber *)time {
if (self.moviePlayer.currentPlaybackTime < [time floatValue] - 10.0) {
[self.moviePlayer setCurrentPlaybackTime:(NSTimeInterval)[time floatValue]];
}
}
//app did become active callback
- (void) applicationDidBecomeActiveNotification:(NSNotification*)notification {
if (!isnan(_curPlayTime) && _curPlayTime > 0.0) {
[self performSelector:#selector(setCurrentPlayTime:) withObject:[NSNumber numberWithFloat:_curPlayTime] afterDelay:0.1];
}
}
//player load state did change callback
-(void)playerLoadStateDidChange:(NSNotification *)notification {
MPMoviePlayerController *player = notification.object;
MPMovieLoadState loadState = player.loadState;
if (loadState & MPMovieLoadStatePlaythroughOK) {
if (!isnan(_curPlayTime) && _curPlayTime > 0.0) {
[self performSelector:#selector(setCurrentPlayTime:) withObject:[NSNumber numberWithFloat:_curPlayTime] afterDelay:0.1];
_curPlayTime = 0.0;
}
}
When I just tap Home button and then reopen the app, and also if I get incoming call but decline it, it works. But if I answer an incoming call, after I finish the call, playing starts from the 0.0 ignoring setCurrentPlaybackTime method call. Does anybody know, where's the problem and may be any example how it should be done to work correct?
Not sure if this is the issue but the selector you are searching for is setCurrentPlayTime not setCurrentPlay*back*Time.

How do I view the progress for converting a Movie with QTKit?

How do I view the progress for converting a Movie with the following QTKit code?
NSDictionary *dict = [NSDictionary
dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], QTMovieExport,
[NSNumber numberWithLong:kQTFileType3GPP],
QTMovieExportType, nil];
[[movieView movie] writeToFile:#"/tmp/sample.3gp"
withAttributes:dict];
i.e. I want to view the progress of the movie conversion such that I can display it in a progress bar.
Taken from this site: http://www.mactech.com/articles/mactech/Vol.21/21.08/Threads/index.html
If the movie is very large, this method could take quite a while to complete. During that time, the user would be unable to do anything with the application except move windows around. Not very exciting.
A slightly better solution involves using the movie:shouldContinueOperation:withPhase:atPercent:withAttributes: delegate method. This is a wrapper around QuickTime's movie progress function, which will be used to display a dialog box showing the progress of the export and to allow the user to cancel the operation.
Here try this
- (BOOL)movie:(QTMovie *)movie
shouldContinueOperation:(NSString *)op
withPhase:(QTMovieOperationPhase)phase
atPercent:(NSNumber *)percent
withAttributes:(NSDictionary *)attributes
{
OSErr err = noErr;
NSEvent *event;
double percentDone = [percent doubleValue] * 100.0;
switch (phase) {
case QTMovieOperationBeginPhase:
// set up the progress panel
[progressText setStringValue:op];
[progressBar setDoubleValue:0];
// show the progress sheet
[NSApp beginSheet:progressPanel
modalForWindow:[movieView window] modalDelegate:nil
didEndSelector:nil contextInfo:nil];
break;
case QTMovieOperationUpdatePercentPhase:
// update the percent done
[progressBar setDoubleValue:percentDone];
[progressBar display];
break;
case QTMovieOperationEndPhase:
[NSApp endSheet:progressPanel];
[progressPanel close];
break;
}
// cancel (if requested)
event = [progressPanel
nextEventMatchingMask:NSLeftMouseUpMask
untilDate:[NSDate distantPast]
inMode:NSDefaultRunLoopMode dequeue:YES];
if (event && NSPointInRect([event locationInWindow],
[cancelButton frame])) {
[cancelButton performClick:self];
err = userCanceledErr;
}
return (err == noErr);
}
Hope this helps.
If you need any help do let me know.
let me know if this helped a lil.
PK