1

I have three videos. The first is from the rear camera. The second is from the front camera and the third is again from the rear camera. The videos are always taken in landscape mode with the home button on the right.

The rear facing videos are in correct orientation. The center video, taken using the front camera, is rotated at 180degrees (upside down). I have been researching and trying numerous methods to transform the center video with no luck. I get the same results every time.

I am getting pretty frustrated with this whole process. Everything I read online and the comments/suggestions from the reviewer here should work but it does not work. The video is the same no matter what I try for transformations. It continually acts as if I did not apply any transformations. Nothing. I do not understand why the transformations are ignored on this. I have spent weeks on this and I am at the end - it simply does not work.

Here is the current iteration of my code:

- (void)mergeVideos2:(NSMutableArray *)assets withCompletion:(void (^)(NSString *))completion {

    AVMutableComposition *mutableComposition = [AVMutableComposition composition];
    AVMutableCompositionTrack *videoCompositionTrack = [mutableComposition addMutableTrackWithMediaType:AVMediaTypeVideo
                                                                                       preferredTrackID:kCMPersistentTrackID_Invalid];
    __block NSMutableArray *instructions = [[NSMutableArray alloc] init];
    __block CGSize size = CGSizeZero;
    __block CMTime time = kCMTimeZero;

    __block AVMutableVideoComposition *mutableVideoComposition = [AVMutableVideoComposition videoComposition];

    __block CGAffineTransform transformflip = CGAffineTransformMakeScale(1, -1);
    //    __block CGAffineTransform transformflip = CGAffineTransformMakeRotation(M_PI);

    __block int32_t commontimescale = 600;

    [assets enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

        NSURL *assetUrl = (NSURL *)obj;
        AVAsset *asset = [AVAsset assetWithURL:assetUrl];

        CMTime cliptime = CMTimeConvertScale(asset.duration, commontimescale, kCMTimeRoundingMethod_QuickTime);

        NSLog(@"%s: Number of tracks: %lu", __PRETTY_FUNCTION__, (unsigned long)[[asset tracks] count]);
        AVAssetTrack *assetTrack = [asset tracksWithMediaType:AVMediaTypeVideo].firstObject;

        NSError *error;
        [videoCompositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, cliptime)
                                       ofTrack:assetTrack
                                        atTime:time
                                         error:&error];
        if (error) {
            NSLog(@"%s: Error - %@", __PRETTY_FUNCTION__, error.debugDescription);
        }

        AVMutableVideoCompositionLayerInstruction *videoLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoCompositionTrack];

        CGAffineTransform transform = assetTrack.preferredTransform;
        [videoLayerInstruction setTransform:CGAffineTransformConcat(transform, transformflip) atTime:time];

        // the main instruction set - this is wrapping the time
        AVMutableVideoCompositionInstruction *videoCompositionInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
        videoCompositionInstruction.timeRange = CMTimeRangeMake(time, assetTrack.timeRange.duration);
        if (videoLayerInstruction != nil)
            videoCompositionInstruction.layerInstructions = @[videoLayerInstruction];
        [instructions addObject:videoCompositionInstruction];

        // time increment variables
        time = CMTimeAdd(time, cliptime);

        if (CGSizeEqualToSize(size, CGSizeZero)) {
            size = assetTrack.naturalSize;;
        }

    }];

    mutableVideoComposition.instructions = instructions;

    // set the frame rate to 9fps
    mutableVideoComposition.frameDuration = CMTimeMake(1, 12);

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths firstObject];
    int number = arc4random_uniform(10000);
    self.outputFile = [documentsDirectory stringByAppendingFormat:@"/export_%i.mov",number];
    AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:mutableComposition
                                                                      presetName:AVAssetExportPreset1280x720];

    exporter.outputURL = [NSURL fileURLWithPath:self.outputFile];
    //Set the output file type
    exporter.outputFileType = AVFileTypeQuickTimeMovie;
    exporter.shouldOptimizeForNetworkUse = YES;

    dispatch_group_t group = dispatch_group_create();

    dispatch_group_enter(group);

    [exporter exportAsynchronouslyWithCompletionHandler:^{
        dispatch_group_leave(group);

    }];

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{

        // get the size of the file
        unsigned  long long size= ([[[NSFileManager defaultManager] attributesOfItemAtPath:self.outputFile error:nil] fileSize]);
        NSString *filesize = [NSByteCountFormatter stringFromByteCount:size countStyle:NSByteCountFormatterCountStyleFile];
        NSString *thereturn = [NSString stringWithFormat:@"%@: %@", self.outputFile, filesize];

        NSLog(@"Export File (Final) - %@", self.outputFile);
        completion(thereturn);

    });

}

Any ideas or suggestions?

iPhone Guy
  • 1,031
  • 10
  • 30

1 Answers1

4

Each AVAssetTrack has a preferredTransform property. It contains information on how to rotate and translate the video to display it properly so you don't have to guess. Use each video's preferredTransform in each layer instruction.

Don't set "videoCompositionTrack.preferredTransform = ..."

Remove the transform ramp "[videoLayerInstruction setTransformRampFromStartTransform:..."

In that enumeration, just use:

CGAffineTransform transform = assetTrack.preferredTransform;
[videoLayerInstruction setTransform:transform atTime:time];

I'm assuming your videos are shot with the same dimensions as your output, with the middle video having its width and height reversed. If they are not, you'll have to add the appropriate scaling:

float scaleFactor = ...// i.e. (outputWidth / videoWidth) 
CGAffineTransform scale = CGAffineTransformMakeScale(scaleFactor,scaleFactor)
transform = CGAffineTransformConcat(transform, scale);
[videoLayerInstruction setTransform:transform atTime:time];

EDIT: It appears that the source videos that appeared upside down in the composition were upside down to begin with, but had an identity CGAffineTransform. This code worked to show them in the correct orientation:

- (void)mergeVideos2:(NSMutableArray *)assets withCompletion:(void (^)(NSString *))completion {

        AVMutableComposition *mutableComposition = [AVMutableComposition composition];
        AVMutableCompositionTrack *videoCompositionTrack = [mutableComposition addMutableTrackWithMediaType:AVMediaTypeVideo
                                                                                           preferredTrackID:kCMPersistentTrackID_Invalid];
        __block NSMutableArray *instructions = [[NSMutableArray alloc] init];
        __block CMTime time = kCMTimeZero;
        __block AVMutableVideoComposition *mutableVideoComposition = [AVMutableVideoComposition videoComposition];
        __block int32_t commontimescale = 600;

        // Create one layer instruction.  We have one video track, and there should be one layer instruction per video track.
        AVMutableVideoCompositionLayerInstruction *videoLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoCompositionTrack];

        [assets enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

            NSURL *assetUrl = (NSURL *)obj;
            AVAsset *asset = [AVAsset assetWithURL:assetUrl];

            CMTime cliptime = CMTimeConvertScale(asset.duration, commontimescale, kCMTimeRoundingMethod_QuickTime);

            NSLog(@"%s: Number of tracks: %lu", __PRETTY_FUNCTION__, (unsigned long)[[asset tracks] count]);
            AVAssetTrack *assetTrack = [asset tracksWithMediaType:AVMediaTypeVideo].firstObject;
            CGSize naturalSize = assetTrack.naturalSize;

            NSError *error;
            //insert the video from the assetTrack into the composition track
            [videoCompositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, cliptime)
                                           ofTrack:assetTrack
                                            atTime:time
                                             error:&error];
            if (error) {
                NSLog(@"%s: Error - %@", __PRETTY_FUNCTION__, error.debugDescription);
            }


            CGAffineTransform transform = assetTrack.preferredTransform;

            //set the layer to have this videos transform at the time that this video starts
            if (<* the video is an intermediate video  - has the wrong orientation*>) {
                //these videos have the identity transform, yet they are upside down.
                //we need to rotate them by M_PI radians (180 degrees) and shift the video back into place

                CGAffineTransform rotateTransform = CGAffineTransformMakeRotation(M_PI);
                CGAffineTransform translateTransform = CGAffineTransformMakeTranslation(naturalSize.width, naturalSize.height);
                [videoLayerInstruction setTransform:CGAffineTransformConcat(rotateTransform, translateTransform) atTime:time];

            } else {
                [videoLayerInstruction setTransform:transform atTime:time];
            }

            // time increment variables
            time = CMTimeAdd(time, cliptime);

        }];

        // the main instruction set - this is wrapping the time
        AVMutableVideoCompositionInstruction *videoCompositionInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
        videoCompositionInstruction.timeRange = CMTimeRangeMake(kCMTimeZero,mutableComposition.duration); //make the instruction last for the entire composition
        videoCompositionInstruction.layerInstructions = @[videoLayerInstruction];
        [instructions addObject:videoCompositionInstruction];
        mutableVideoComposition.instructions = instructions;

        // set the frame rate to 9fps
        mutableVideoComposition.frameDuration = CMTimeMake(1, 12);

        //set the rendersize for the video we're about to write
        mutableVideoComposition.renderSize = CGSizeMake(1280,720);

        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *documentsDirectory = [paths firstObject];
        int number = arc4random_uniform(10000);
        self.outputFile = [documentsDirectory stringByAppendingFormat:@"/export_%i.mov",number];

        //let the rendersize of the video composition dictate size.  use quality preset here
        AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:mutableComposition
                                                                          presetName:AVAssetExportPresetHighestQuality];

        exporter.outputURL = [NSURL fileURLWithPath:self.outputFile];
        //Set the output file type
        exporter.outputFileType = AVFileTypeQuickTimeMovie;
        exporter.shouldOptimizeForNetworkUse = YES;
        exporter.videoComposition = mutableVideoComposition;

        dispatch_group_t group = dispatch_group_create();

        dispatch_group_enter(group);

        [exporter exportAsynchronouslyWithCompletionHandler:^{
            dispatch_group_leave(group);

        }];

        dispatch_group_notify(group, dispatch_get_main_queue(), ^{

            // get the size of the file
            unsigned  long long size= ([[[NSFileManager defaultManager] attributesOfItemAtPath:self.outputFile error:nil] fileSize]);
            NSString *filesize = [NSByteCountFormatter stringFromByteCount:size countStyle:NSByteCountFormatterCountStyleFile];
            NSString *thereturn = [NSString stringWithFormat:@"%@: %@", self.outputFile, filesize];

            NSLog(@"Export File (Final) - %@", self.outputFile);
            completion(thereturn);

        });
    }
jlw
  • 3,166
  • 1
  • 19
  • 24
  • What you are saying makes sense. I added the code recommended (see edits above). Unfortunately it is still not working. Same results. Any further suggestions? – iPhone Guy Sep 23 '15 at 13:29
  • Can you post each video's preferredTransform? – jlw Sep 23 '15 at 14:19
  • I posted the updated code (to only apply the transform to the correct videos - see the switch) and the values of the preferredTransform for each asset. – iPhone Guy Sep 23 '15 at 18:54
  • Don't call setTransform:rotateTranslate. You've already called setTransform:transform. Apply the transform for every video. #1,#3,#5... says "rotate this video 90 degrees, then shift it to the right horizontally 1080 pixels". #2,#4,#6,... says "rotate this video 90 degrees, then shift it to the right horizontally 720 pixels". If you set these transforms, your videos should appear in the correct orientation. You'll have to scale down the videos with the 1080 shift, because these are most likely 1920x1080 and your output is 1280x720. Posting the assetTrack.naturalSize will confirm this. – jlw Sep 23 '15 at 19:14
  • I am having a little trouble implementing this: Apply the transform for every video. #1,#3,#5... says "rotate this video 90 degrees, then shift it to the right horizontally 1080 pixels". #2,#4,#6,... says "rotate this video 90 degrees, then shift it to the right horizontally 720 pixels". – iPhone Guy Sep 23 '15 at 20:07
  • And yes -- you are correct about the scale down issue - videos 1,3,5 are 1920x1080 and 2,4,6 are 1280x720 – iPhone Guy Sep 23 '15 at 20:08
  • Get rid of your `switch` statement. Get rid of the `rotateTranslate` transform. Use [videoLayerInstruction setTransform:assetTrack.preferredTransform atTime:time] for each video instead. You'll need to handle scaling (as described in my solution) for any videos that do not have the dimensions of your output. (i.e. 1,3,5 since they are larger than your output of 1280x720) – jlw Sep 23 '15 at 23:14
  • I removed everything and only have the [videoLayerInstruction setTransform:assetTrack.preferredTransform atTime:time], but it is still not working. I feel like it is really close, but I am missing something here. – iPhone Guy Sep 24 '15 at 00:50
  • Copy your videos to your Mac and open them. Are any of them playing upside down? If so, the content of your buffers is upside down and for those videos, you'll need to concatenate a rotation transform of M_PI radians. – jlw Sep 24 '15 at 02:49
  • The concatenation of a rotation transformation of M_PI is not working. It is like none of the transformations we add work at all. – iPhone Guy Sep 24 '15 at 22:31
  • The videos are shot in landscape mode. When I pull the videos from the phone they are shown in portrait. The video that appears correctly in the compilation has the bottom of the video facing the left. The video that appears upside when has the bottom of the video facing right - so it appears that the content of the buffers is upside down - the latest code is updated in the original question. – iPhone Guy Sep 25 '15 at 13:16
  • Your videos were not recorded as you thought. The video on disk is in landscape, but the rotation transform makes it show in portrait. Concatenate a rotation transform of M_PI radians to those videos' preferredTransforms. – jlw Sep 25 '15 at 19:34
  • Ok -- this weekend I changed the buffer to make sure all vides are in LandscapeRight mode. When I download the individual videos all are correct and in LandscapeRight mode. BUT -- when I combine them the front facing video reverts back to landscape let and appears upside down. This is the part that is frustrating - individual videos are correct, combined videos are not. I tried a M_PI rotation, a DregrresToRadians(180) rotation and no rotation. – iPhone Guy Sep 28 '15 at 12:00
  • I have not been able to resolve this issue. It looks like I may need multiple tracks, one for each video to apply instructions. I will post after testing this theory. – iPhone Guy Oct 06 '15 at 19:08
  • You do not need multiple tracks. The issue is with your transforms. See the documentation for CGAffineTransform. This link may help as well http://iphonedevelopment.blogspot.com/2008/10/demystifying-cgaffinetransform.html – jlw Oct 06 '15 at 19:42
  • I have tried every suggestion given for manipulating the transformations and reviewed the documentation referenced. Nothing works. It is as if the transformations are not there. When I tried the transformation for the M_PI rotation and that was the only one applied everything was flipped. That is the only time the transformation worked. – iPhone Guy Oct 06 '15 at 19:48
  • Take a look at another attempt at this: http://stackoverflow.com/questions/32977879/avfoundation-combine-videos-only-one-the-first-is-displayed – iPhone Guy Oct 06 '15 at 19:49
  • I went back and tried this again. I updated the entire class. I pulled out this code from the main project to make this the only thing to test. It is still not working. I do not understand why the transformations simply will not work for me. Could you review and give any additional; suggestions or see if there is something that looks wrong? – iPhone Guy Oct 07 '15 at 15:46
  • Let's continue in chat: http://chat.stackoverflow.com/rooms/91635/discussion-between-jlw-and-iphone-guy – jlw Oct 07 '15 at 17:06
  • This was the only thing that worked for what i was trying to do, i'll post question/answer that better constrains my use case and update with the swift code i'm using in production. – Ryan Romanchuk Oct 02 '18 at 23:31
  • @RyanRomanchuk hi, did you ever post what your q&a? – Lance Samaria Jan 18 '21 at 13:50