EXPORT STATUS 4 Error Domain=AVFoundationErrorDomain Code=-11820 "Cannot Complete Export" UserInfo={NSLocalizedDescription=Cannot Complete Export, NSLocalizedRecoverySuggestion=Try exporting again.}
I'm experiencing an intermittent error when trying to export an
AVMutableComposition containing AVMutableVideoCompositionLayerInstruction (s) and an AVMutableVideoComposition using an AVAssetExportSession.The objective is to merge an unlimited number of videos and applying transitions between clips using layerInstructions.
P.S. The error is not consistent. It works when attempting to merge 5 clips and 18 clips, but doesn't work when attempting to merge 17 clips.
I've posted my code below. Any help is greatly appreciated.
EDIT: It seems the issue is related to the creation of multiple AVMutableCompositionTrack(s). If more than 15 or 16 are created, the error occurs. However, creating multiple AVMutableCompositionTrack(s), I believe, is necessary to overlap all the videos and create overlapping transitions.
EDIT 2: When shorter videos are selected, more videos are processed before the error occurs. Accordingly, it looks like a memory issue whereby tracks are being deallocated. However, there doesn't seem to be a memory leak based on the memory management tool.
-(void)prepareMutableCompositionFor{
AVMutableComposition *mutableComposition = [[AVMutableComposition alloc] init];
AVMutableVideoCompositionInstruction *mainInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
NSMutableArray *instructionsArray = [[NSMutableArray alloc] init];
videoStartTime = kCMTimeZero;
for(int i = 0; i < videoURLsArrray.count; i++){
AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:[videoURLsArrray objectAtIndex:i] options:nil];
CMTime currentVideoDuration = [videoAsset duration];
AVMutableCompositionTrack *videoTrack = [mutableComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
[videoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, currentVideoDuration) ofTrack:[[videoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0] atTime:videoStartTime error:nil];
CGSize videoSize = [videoTrack naturalSize];
if([videoAsset tracksWithMediaType:AVMediaTypeAudio].count > 0){
AVMutableCompositionTrack *audioTrack = [mutableComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
[audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, currentVideoDuration) ofTrack:[[videoAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] atTime:videoStartTime error:nil];
}
/
AVMutableVideoCompositionLayerInstruction *layerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];
int transitionNumber = [[videoTransitionsArray objectAtIndex:i] intValue];
float transitionDuration = [[videoTransitionsDurationArray objectAtIndex:i] floatValue];
if(i == 0){
if(i < videoURLsArrray.count - 1){
[layerInstruction setOpacityRampFromStartOpacity:1.0 toEndOpacity:0.0 timeRange:CMTimeRangeMake(CMTimeSubtract(currentVideoDuration, CMTimeMakeWithSeconds(transitionDuration, 600)), CMTimeMakeWithSeconds(transitionDuration, 600))];
}
else{
[layerInstruction setOpacity:0.0 atTime:CMTimeSubtract(currentVideoDuration, CMTimeMakeWithSeconds(transitionDuration, 600))];
}
}
else{
int previousTransitionNumber = [[videoTransitionsArray objectAtIndex:i - 1] intValue];
float previousTransitionDuration = [[videoTransitionsDurationArray objectAtIndex:i - 1] floatValue];
if(i < videoURLsArrray.count - 1){
[layerInstruction setOpacityRampFromStartOpacity:1.0 toEndOpacity:1.0 timeRange:CMTimeRangeMake(videoStartTime, CMTimeMakeWithSeconds(previousTransitionDuration, 600))];
[layerInstruction setOpacityRampFromStartOpacity:1.0 toEndOpacity:0.0 timeRange:CMTimeRangeMake(CMTimeAdd(videoStartTime, CMTimeSubtract(currentVideoDuration, CMTimeMakeWithSeconds(transitionDuration, 600))), CMTimeMakeWithSeconds(transitionDuration, 600))];
}
else{
[layerInstruction setOpacityRampFromStartOpacity:1.0 toEndOpacity:1.0 timeRange:CMTimeRangeMake(videoStartTime, CMTimeMakeWithSeconds(previousTransitionDuration, 600))];
}
}
[instructionsArray addObject:layerInstruction];
if(i < videoURLsArrray.count - 1){
videoStartTime = CMTimeAdd(videoStartTime, CMTimeSubtract(currentVideoDuration, CMTimeMakeWithSeconds(transitionDuration, 600)));
}
else{
videoStartTime = CMTimeAdd(videoStartTime, currentVideoDuration);
}
}
mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero,videoStartTime);
mainInstruction.layerInstructions = instructionsArray;
AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoCompositionWithPropertiesOfAsset:mutableComposition];
videoComposition.renderSize = CGSizeMake(1920, 1080);
videoComposition.frameDuration = CMTimeMake(1, 30);
videoComposition.instructions = [NSArray arrayWithObjects:mainInstruction,nil];
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *videoOutputPath = [documentsDirectory stringByAppendingPathComponent:@"videoRecordingFinalOutput.mov"];
NSURL *videoOutputURL = [[NSURL alloc] initFileURLWithPath:videoOutputPath];
NSError *error = nil;
if([[NSFileManager defaultManager] fileExistsAtPath:videoOutputPath]){
[[NSFileManager defaultManager] removeItemAtPath:videoOutputPath error:&error];
if(error){
NSLog(@"VIDEO FILE DELETE FAILED");
}
else{
NSLog(@"VIDEO FILE DELETED");
}
}
AVAssetExportSession *videoExportSession = [[AVAssetExportSession alloc] initWithAsset:mutableComposition presetName:AVAssetExportPresetHighestQuality];
videoExportSession.outputURL = videoOutputURL;
videoExportSession.videoComposition = videoComposition;
/
videoExportSession.outputFileType = AVFileTypeQuickTimeMovie;
[videoExportSession exportAsynchronouslyWithCompletionHandler:^{
NSLog(@"EXPORT STATUS %ld %@", (long)videoExportSession.status, videoExportSession.error);
if(videoExportSession.error == NULL){
NSLog(@"VIDEO EXPORT SUCCESSFUL");
[library writeVideoAtPathToSavedPhotosAlbum:videoOutputURL
completionBlock:^(NSURL *assetURL, NSError *error) {
if(error) {
NSLog(@"SAVING ERROR");
NSError *error = nil;
if([[NSFileManager defaultManager] fileExistsAtPath:videoOutputPath]){
[[NSFileManager defaultManager] removeItemAtPath:videoOutputPath error:&error];
if(error){
NSLog(@"VIDEO FILE DELETE FAILED");
}
else{
NSLog(@"VIDEO FILE DELETED");
}
}
}
else{
NSLog(@"SAVING NO ERROR");
NSError *error = nil;
if([[NSFileManager defaultManager] fileExistsAtPath:videoOutputPath]){
[[NSFileManager defaultManager] removeItemAtPath:videoOutputPath error:&error];
if(error){
NSLog(@"VIDEO FILE DELETE FAILED");
}
else{
NSLog(@"VIDEO FILE DELETED");
}
}
}
}];
}
else{
NSError *error = nil;
if([[NSFileManager defaultManager] fileExistsAtPath:videoOutputPath]){
[[NSFileManager defaultManager] removeItemAtPath:videoOutputPath error:&error];
if(error){
NSLog(@"VIDEO FILE DELETE FAILED");
}
else{
NSLog(@"VIDEO FILE DELETED");
}
}
}
}];
}