Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When saving recorded video that is too long, app crashes

The problem:

When saving a video that is recorded in my app, if the video size/duration is too big/long, my app crashes without a log/exception.

My Setup:

In my App I use a UIImagePickerController to record videos. Now I have noticed that if I make my videos very long in length (for example 30minutes with UIImagePickerControllerQualityTypeMedium, or more than a minute with UIImagePickerControllerQualityTypeIFrame1280x720), when saving the video, the app crashes. Sometimes with and sometimes without a warning. Now I started to debug and noticed it had something to do with memory (malloc_error).

I used the profiler to check allocations live, and noticed that when it was going to save the video, the allocation suddenly became very big (I guess something to do with temporary memory usage for the video?) before it ultimately crashed. Here is a screenshot from the profiler: Allocations screenshot

The app must be able to record video with a maximum duration of 1 hour (in any quality specified).

What I have tried:

  • Setting the picker.videoMaximumDuration shorter/longer
  • Debug with profiler/instruments
  • Check for leaks
  • Closed all open apps & deleted app on device (for storage cleaning) to get more memory

Code:

- (void)openCamera:(id)sender context:(NSManagedObjectContext*)context {
    self.context = context;
    //Set self as delegate (UIImagePickerControllerDelegate)
    [self.picker setDelegate:self];
    //If the device has a camera
    if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
        self.picker.sourceType = UIImagePickerControllerSourceTypeCamera;
        self.picker.allowsEditing = YES;
        self.picker.videoQuality = [Settings videoQualitySetting];
        //If the camera can record video 
        NSString *desired = (NSString *)kUTTypeMovie;
        if ([[UIImagePickerController availableMediaTypesForSourceType:self.picker.sourceType] containsObject:desired]) {
            //Let the user record video
            self.picker.mediaTypes = [NSArray arrayWithObject:desired];
            self.picker.videoMaximumDuration = MaxDuration;
        }
        else {
            NSLog(@"Can't take videos with this device"); //debug
        }
        //Present the picker fullscreen/in popover
        if ([Settings shouldDisplayFullScreenCamera]){
            [self presentModalViewController:self.picker animated:YES];
            [self.masterPopoverController dismissPopoverAnimated:YES];
        }
        else {
            if (!_popover){
                _popover = [[UIPopoverController alloc] initWithContentViewController:self.picker];
            }
            [_popover presentPopoverFromBarButtonItem:sender permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
        }
    }
    else {
        NSLog(@"Does not have a camera"); //debug
    }    
}

And code when the image is picked:

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
    {
    NSString *mediaType = [info objectForKey: UIImagePickerControllerMediaType];

    // Save the video, and close the overlay
    if (CFStringCompare ((__bridge CFStringRef) mediaType, kUTTypeMovie, 0)
        == kCFCompareEqualTo) {

        self.tempVideoPath = [[info objectForKey:
                                UIImagePickerControllerMediaURL] path];
        [LocalVideoStorage saveVideo:[NSData dataWithContentsOfPath:self.tempVideoPath name:self.videoName];
        [_picker dismissModalViewControllerAnimated: YES];
        [[_picker parentViewController] dismissModalViewControllerAnimated:YES];
        [_popover dismissPopoverAnimated:YES];  
    }
}

And finally, when it is saved:

+ (NSString*)saveVideo:(NSData*)video:(NSString*)videoName {
    NSFileManager *fileManager = [NSFileManager defaultManager];//create instance of NSFileManager

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); //create an array and store result of our search for the documents directory in it

    NSString *documentsDirectory = [paths objectAtIndex:0]; //create NSString object, that holds our exact path to the documents directory

    NSString *fullPath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.MOV", videoName]]; //add our video to the path

    [fileManager createFileAtPath:fullPath contents:video attributes:nil]; //finally save the path (video)

    NSLog(@"Video saved!");
    return fullPath;

}

I am using ARC with iOS 5.1.1

Update: I have put a breakpoint on malloc_error_break, and in instruments I can see it is called from:

#   Address Category    Timestamp   Live    Size    Responsible Library Responsible Caller
0   0x10900000  Malloc 473,29 MB    02:08.951.332   •   496283648   Foundation  -[NSData(NSData) initWithContentsOfFile:]

Solution: As lawicko & john.k.doe said, I tried to load the video from it's path into an NSData variable. This caused the whole video to be loaded into memory. Instead of doing that, I now just move the file (& rename) copyItemAtPath

NSError *error = nil;
if (![fileManager copyItemAtPath:path toPath:fullPath error:&error])
    NSLog(@"Error: %@", error);
like image 781
Thermometer Avatar asked Jun 15 '12 13:06

Thermometer


2 Answers

Your problem is this line:

[NSData dataWithContentsOfPath:self.tempVideoPath]

You are clearly trying to load the contents of this file to memory all at once, but iOS will never let you load that much at one time. Your saveVideo method seems to only copy the file from temporary location to the documents directory. If this is the only thing that you need to do there, then take a look at copyItemAtPath:toPath:error method of NSFileManager. Your could change the saveVideo method to take the temporary file path as a parameter, rather than the data.

like image 132
lawicko Avatar answered Sep 21 '22 12:09

lawicko


why bother pulling the entire contents of the media stream from
[[info objectForKey:UIImagePickerControllerMediaURL] path] into an NSData* and then writing them back out? your media stream is going to be huge!

the reason this is happening at save is because you are recording the data, it is going to "disk", and then you read it into memory to write it back out to another disk file.

have you considered using NSFileManager to copy the file from [[info objectForKey:UIImagePickerControlMediaURL] path] to the name you create (fullPath) ? this should avoid pulling the whole thing into memory; it should be a "file-to-file" transfer.

like image 21
john.k.doe Avatar answered Sep 18 '22 12:09

john.k.doe