Search code examples
iosxcodeavfoundationavassetexportsessionavmutablecomposition

AVAssetExportSession intermittent error 11820 "Cannot Complete Export" Suggestion=Try exporting again


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)prepareMutableCompositionForPlayback{
AVMutableComposition *mutableComposition = [[AVMutableComposition alloc] init];
AVMutableVideoCompositionInstruction *mainInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
mainInstruction.backgroundColor = [[UIColor blackColor] CGColor];

NSMutableArray *instructionsArray = [[NSMutableArray alloc] init];

videoStartTime = kCMTimeZero;

for(int i = 0; i < videoAssetsArray.count; i++){
    AVAsset *videoAsset = [videoAssetsArray objectAtIndex:i];
    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];
    }

    //INSTRUCTIONS - TRANSITIONS
    AVMutableVideoCompositionLayerInstruction *layerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];

    int transitionNumber = [[videoTransitionsArray objectAtIndex:i] intValue];
    float transitionDuration = [[videoTransitionsDurationArray objectAtIndex:i] floatValue];

    if(i == 0){
        [layerInstruction setOpacityRampFromStartOpacity:1.0 toEndOpacity:0.0 timeRange:CMTimeRangeMake(CMTimeSubtract(currentVideoDuration, CMTimeMakeWithSeconds(transitionDuration, 600)), CMTimeMakeWithSeconds(transitionDuration, 600))];
    }
    else{
        int previousTransitionNumber = [[videoTransitionsArray objectAtIndex:i - 1] intValue];
        float previousTransitionDuration = [[videoTransitionsDurationArray objectAtIndex:i - 1] floatValue];

        if(i < videoAssetsArray.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 < videoAssetsArray.count - 1){
        //TAKING INTO ACCOUNT THE TRANSITION DURATION TO OVERLAP VIDEOS
        videoStartTime = CMTimeAdd(videoStartTime, CMTimeSubtract(currentVideoDuration, CMTimeMakeWithSeconds(transitionDuration, 600)));
    }
    else{
        //TRANSITION NOT APPLIED TO THE END OF THE LAST CLIP
        videoStartTime = CMTimeAdd(videoStartTime, currentVideoDuration);
    }
}

mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero,videoStartTime);
mainInstruction.layerInstructions = instructionsArray;

AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
videoComposition.instructions = [NSArray arrayWithObjects:mainInstruction,nil];
videoComposition.frameDuration = CMTimeMake(1, 30);
videoComposition.renderSize = CGSizeMake(1920, 1080);

NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *videoOutputPath = [documentsDirectory stringByAppendingPathComponent:@"videoRecordingFinalOutput.mov"];
NSURL *videoOutputURL = [[NSURL alloc] initFileURLWithPath:videoOutputPath];

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(@"EXPORT SUCCESSFUL");

        [library writeVideoAtPathToSavedPhotosAlbum:videoOutputURL
                                    completionBlock:^(NSURL *assetURL, NSError *error) {
                                        if(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");
                                                }
                                            }
                                        }
                                    }];
    }
    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");
            }
        }
    }
}];
}

Solution

  • Instead of creating a new videoTracks for each clip, Why dont you try using just 2 videoTracks and insert timeRanges in these 2. And give transitions between the 2 tracks

    so first video will be inserted in videoTrack1 the second on videoTrack2, so that transition can be applied then insert the third clip in track one again and so on.