I've been writing a camera app for iOS 8 that uses AVFoundation to set up and handle recording and saving (not ImagePickerController). I'm trying to save use the maxRecordedFileSize attribute of the AVCaptureMovieFileOutput class to allow the user to fill up all available space on the phone (minus a 250MB buffer left for apple stuff).
- (unsigned long long) availableFreespaceInMb {
unsigned long long freeSpace;
NSError *error = nil;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSDictionary *dictionary = [[NSFileManager defaultManager] attributesOfFileSystemForPath:[paths lastObject] error: &error];
if (dictionary) {
NSNumber *fileSystemFreeSizeInBytes = [dictionary objectForKey: NSFileSystemFreeSize];
freeSpace = [fileSystemFreeSizeInBytes unsignedLongLongValue];
} else {
NSLog(@"Error getting free space");
//Handle error
}
//convert to MB
freeSpace = (freeSpace/1024ll)/1024ll;
freeSpace -= _recordSpaceBufferInMb; // 250 MB
NSLog(@"Remaining space in MB: %llu", freeSpace);
NSLog(@" Diff Since Last: %llu", (_prevRemSpaceMb - freeSpace));
_prevRemSpaceMb = freeSpace;
return freeSpace;
}
The AVErrorMaximumFileSizeReached is thrown when available space (minus buffer) is reduced to zero, and no save error is thrown, but the video does not appear in the camera roll and is not saved. When I set the maxRecordedDuration field the AVErrorMaximumDurationReached is thrown and the video DOES save. I calculate max time from max size, but I always have plenty of space left due to frame compression.
- (void) toggleMovieRecording
{
double factor = 1.0;
if (_currentFramerate == _slowFPS) {
factor = _slowMotionFactor;
}
double availableRecordTimeInSeconds = [self remainingRecordTimeInSeconds] / factor;
unsigned long long remainingSpace = [self availableFreespaceInMb] * 1024 * 1024;
if (![[self movieFileOutput] isRecording]) {
if (availableSpaceInMb < 50) {
NSLog(@"TMR:Not enough space, can't record");
[AVViewController currentVideoOrientation];
[_previewView memoryAlert];
return;
}
}
if (![self enableRecording]) {
return;
}
[[self recordButton] setEnabled:NO];
dispatch_async([self sessionQueue], ^{
if (![[self movieFileOutput] isRecording])
{
if ([[UIDevice currentDevice] isMultitaskingSupported])
{
[self setBackgroundRecordingID:[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil]];
}
// Update the orientation on the movie file output video connection before starting recording.
[[[self movieFileOutput] connectionWithMediaType:AVMediaTypeVideo] setVideoOrientation: [AVViewController currentVideoOrientation]];//[[(AVCaptureVideoPreviewLayer *)[[self previewView] layer] connection] videoOrientation]];
// Start recording to a temporary file.
NSString *outputFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[@"movie" stringByAppendingPathExtension:@"mov"]];
// Is there already a file like this?
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:outputFilePath]) {
NSLog(@"filexists");
NSError *err;
if ([fileManager removeItemAtPath:outputFilePath error:&err] == NO) {
NSLog(@"Error, file exists at path");
}
}
[_previewView startRecording];
// Set the movie file output to stop recording a bit before the phone is full
[_movieFileOutput setMaxRecordedFileSize:remainingSpace]; // Less than the total remaining space
// [_movieFileOutput setMaxRecordedDuration:CMTimeMake(availableRecordTimeInSeconds, 1.0)];
[_movieFileOutput startRecordingToOutputFileURL:[NSURL fileURLWithPath:outputFilePath] recordingDelegate:self];
}
else
{
[_previewView stopRecording];
[[self movieFileOutput] stopRecording];
}
});
}
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error
NSLog(@"AVViewController: didFinishRecordingToOutputFile");
if (error) {
NSLog(@"%@", error);
NSLog(@"Caught Error");
if ([error code] == AVErrorDiskFull) {
NSLog(@"Caught disk full error");
} else if ([error code] == AVErrorMaximumFileSizeReached) {
NSLog(@"Caught max file size error");
} else if ([error code] == AVErrorMaximumDurationReached) {
NSLog(@"Caught max duration error");
} else {
NSLog(@"Caught other error");
}
[self remainingRecordTimeInSeconds];
dispatch_async(dispatch_get_main_queue(), ^{
[_previewView stopRecording];
[_previewView memoryAlert];
});
}
// Note the backgroundRecordingID for use in the ALAssetsLibrary completion handler to end the background task associated with this recording. This allows a new recording to be started, associated with a new UIBackgroundTaskIdentifier, once the movie file output's -isRecording is back to NO — which happens sometime after this method returns.
UIBackgroundTaskIdentifier backgroundRecordingID = [self backgroundRecordingID];
[self setBackgroundRecordingID:UIBackgroundTaskInvalid];
[[[ALAssetsLibrary alloc] init] writeVideoAtPathToSavedPhotosAlbum:outputFileURL completionBlock:^(NSURL *assetURL, NSError *error) {
if (error) {
NSLog(@"%@", error);
NSLog(@"Error during write");
} else {
NSLog(@"Writing to photos album");
}
[[NSFileManager defaultManager] removeItemAtURL:outputFileURL error:nil];
if (backgroundRecordingID != UIBackgroundTaskInvalid)
[[UIApplication sharedApplication] endBackgroundTask:backgroundRecordingID];
}];
if (error) {
[_session stopRunning];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), _sessionQueue, ^{
[_session startRunning];
});
}
"Writing to photos album" appears when both errors are thrown. I'm completely stumped by this. Any iOS insights?