Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AVFoundation - why can't I get the video orientation right

I am using AVCaptureSession to capture video from a devices camera and then using AVAssetWriterInput and AVAssetTrack to compress/resize the video before uploading it to a server. The final videos will be viewed on the web via an html5 video element.

I'm running into multiple issues trying to get the orientation of the video correct. My app only supports landscape orientation and all captured videos should be in landscape orientation. However, I would like to allow the user to hold their device in either landscape direction (i.e. home button on either the left or the right hand side).

I am able to make the video preview show in the correct orientation with the following line of code

_previewLayer.connection.videoOrientation = UIDevice.currentDevice.orientation;

The problems start when processing the video via AVAssetWriterInput and friends. The result does not seem to account for the left vs. right landscape mode the video was captured in. IOW, sometimes the video comes out upside down. After some googling I found many people suggesting that the following line of code would solve this issue

writerInput.transform = videoTrack.preferredTransform;

...but this doesn't seem to work. After a bit of debugging I found that videoTrack.preferredTransform is always the same value, regardless of the orientation the video was captured in.

I tried manually tracking what orientation the video was captured in and setting the writerInput.transform to CGAffineTransformMakeRotation(M_PI) as needed. Which solved the problem!!!

...sorta

When I viewed the results on the device this solution worked as expected. Videos were right-side-up regardless of left vs. right orientation while recording. Unfortunately, when I viewed the exact same videos in another browser (chrome on a mac book) they were all upside-down!?!?!?

What am I doing wrong?

EDIT

Here's some code, in case it's helpful...

-(void)compressFile:(NSURL*)inUrl;
{                
    NSString* fileName = [@"compressed." stringByAppendingString:inUrl.lastPathComponent];
    NSError* error;
    NSURL* outUrl = [PlatformHelper getFilePath:fileName error:&error];

    NSDictionary* compressionSettings = @{ AVVideoProfileLevelKey: AVVideoProfileLevelH264Main31,
                                           AVVideoAverageBitRateKey: [NSNumber numberWithInt:2500000],
                                           AVVideoMaxKeyFrameIntervalKey: [NSNumber numberWithInt: 30] };

    NSDictionary* videoSettings = @{ AVVideoCodecKey: AVVideoCodecH264,
                                     AVVideoWidthKey: [NSNumber numberWithInt:1280],
                                     AVVideoHeightKey: [NSNumber numberWithInt:720],
                                     AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill,
                                     AVVideoCompressionPropertiesKey: compressionSettings };

    NSDictionary* videoOptions = @{ (id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] };


    AVAssetWriterInput* writerInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
    writerInput.expectsMediaDataInRealTime = YES;

    AVAssetWriter* assetWriter = [AVAssetWriter assetWriterWithURL:outUrl fileType:AVFileTypeMPEG4 error:&error];
    assetWriter.shouldOptimizeForNetworkUse = YES;

    [assetWriter addInput:writerInput];

    AVURLAsset* asset = [AVURLAsset URLAssetWithURL:inUrl options:nil];
    AVAssetTrack* videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];

    // !!! this line does not work as expected and causes all sorts of issues (videos display sideways in some cases) !!!
    //writerInput.transform = videoTrack.preferredTransform;

    AVAssetReaderTrackOutput* readerOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:videoTrack outputSettings:videoOptions];
    AVAssetReader* assetReader = [AVAssetReader assetReaderWithAsset:asset error:&error];

    [assetReader addOutput:readerOutput];

    [assetWriter startWriting];
    [assetWriter startSessionAtSourceTime:kCMTimeZero];
    [assetReader startReading];

    [writerInput requestMediaDataWhenReadyOnQueue:_processingQueue usingBlock:
     ^{
         /* snip */
     }];
}
like image 681
herbrandson Avatar asked Feb 18 '14 06:02

herbrandson


2 Answers

The problem is that modifying the writerInput.transform property only adds a tag in the video file metadata which instructs the video player to rotate the file during playback. That's why the videos play in the correct orientation on your device (I'm guessing they also play correctly in a Quicktime player as well).

The pixel buffers captured by the camera are still laid out in the orientation in which they were captured. Many video players will not check for the preferred orientation metadata tag and will just play the file in the native pixel orientation.

If you want the user to be able to record video holding the phone in either landscape mode, you need to rectify this at the AVCaptureSession level before compression by performing a transform on the CVPixelBuffer of each video frame. This Apple Q&A covers it (look at the AVCaptureVideoOutput documentation as well): https://developer.apple.com/library/ios/qa/qa1744/_index.html

Investigating the link above is the correct way to solve your problem. An alternate fast n' dirty way to solve the same problem would be to lock the recording UI of your app into only one landscape orientation and then to rotate all of your videos server-side using ffmpeg.

like image 60
hifigi Avatar answered Oct 31 '22 20:10

hifigi


In case it's helpful for anyone, here's the code I ended up with. I ended up having to do the work on the video as it was being captured instead of as a post processing step. This is a helper class that manages the capture.

Interface

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

@interface VideoCaptureManager : NSObject<AVCaptureVideoDataOutputSampleBufferDelegate>
{
    AVCaptureSession* _captureSession;
    AVCaptureVideoPreviewLayer* _previewLayer;
    AVCaptureVideoDataOutput* _videoOut;
    AVCaptureDevice* _videoDevice;
    AVCaptureDeviceInput* _videoIn;
    dispatch_queue_t _videoProcessingQueue;

    AVAssetWriter* _assetWriter;
    AVAssetWriterInput* _writerInput;

    BOOL _isCapturing;
    NSString* _gameId;
    NSString* _authToken;
}

-(void)setSettings:(NSString*)gameId authToken:(NSString*)authToken;
-(void)setOrientation:(AVCaptureVideoOrientation)orientation;
-(AVCaptureVideoPreviewLayer*)getPreviewLayer;
-(void)startPreview;
-(void)stopPreview;
-(void)startCapture;
-(void)stopCapture;

@end

Implementation (w/ a bit of editing and a few little TODO's)

@implementation VideoCaptureManager

-(id)init;
{
    self = [super init];
    if (self) {
        NSError* error;

        _videoProcessingQueue = dispatch_queue_create("VideoQueue", DISPATCH_QUEUE_SERIAL);
        _captureSession = [AVCaptureSession new];

        _videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

        _previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_captureSession];
        [_previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];

        _videoOut = [AVCaptureVideoDataOutput new];
        _videoOut.videoSettings = @{ (id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] };
        _videoOut.alwaysDiscardsLateVideoFrames = YES;

        _videoIn = [AVCaptureDeviceInput deviceInputWithDevice:_videoDevice error:&error];
        // handle errors here

        [_captureSession addInput:_videoIn];
        [_captureSession addOutput:_videoOut];
    }

    return self;
}

-(void)setOrientation:(AVCaptureVideoOrientation)orientation;
{
    _previewLayer.connection.videoOrientation = orientation;
    for (AVCaptureConnection* item in _videoOut.connections) {
        item.videoOrientation = orientation;
    }
}

-(AVCaptureVideoPreviewLayer*)getPreviewLayer;
{
    return _previewLayer;
}

-(void)startPreview;
{
    [_captureSession startRunning];
}

-(void)stopPreview;
{
    [_captureSession stopRunning];
}

-(void)startCapture;
{
    if (_isCapturing) return;

    NSURL* url = put code here to create your output url

    NSDictionary* compressionSettings = @{ AVVideoProfileLevelKey: AVVideoProfileLevelH264Main31,
                                           AVVideoAverageBitRateKey: [NSNumber numberWithInt:2500000],
                                           AVVideoMaxKeyFrameIntervalKey: [NSNumber numberWithInt: 1],
                                        };

    NSDictionary* videoSettings = @{ AVVideoCodecKey: AVVideoCodecH264,
                                     AVVideoWidthKey: [NSNumber numberWithInt:1280],
                                     AVVideoHeightKey: [NSNumber numberWithInt:720],
                                     AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill,
                                     AVVideoCompressionPropertiesKey: compressionSettings
                                    };

    _writerInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
    _writerInput.expectsMediaDataInRealTime = YES;

    NSError* error;
    _assetWriter = [AVAssetWriter assetWriterWithURL:url fileType:AVFileTypeMPEG4 error:&error];
    // handle errors

    _assetWriter.shouldOptimizeForNetworkUse = YES;
    [_assetWriter addInput:_writerInput];
    [_videoOut setSampleBufferDelegate:self queue:_videoProcessingQueue];

    _isCapturing = YES;
}

-(void)stopCapture;
{
    if (!_isCapturing) return;

    [_videoOut setSampleBufferDelegate:nil queue:nil]; // TODO: seems like there could be a race condition between this line and the next (could end up trying to write a buffer after calling writingFinished

    dispatch_async(_videoProcessingQueue, ^{
        [_assetWriter finishWritingWithCompletionHandler:^{
            [self writingFinished];
        }];
    });
}

-(void)writingFinished;
{
    // TODO: need to check _assetWriter.status to make sure everything completed successfully
    // do whatever post processing you need here
}


-(void)captureOutput:(AVCaptureOutput*)captureOutput didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection*)connection;
{
    NSLog(@"Video frame was dropped.");
}

-(void)captureOutput:(AVCaptureOutput*)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    if(_assetWriter.status != AVAssetWriterStatusWriting) {
        CMTime lastSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
        [_assetWriter startWriting]; // TODO: need to check the return value (a bool)
        [_assetWriter startSessionAtSourceTime:lastSampleTime];
    }

    if (!_writerInput.readyForMoreMediaData || ![_writerInput appendSampleBuffer:sampleBuffer]) {
        NSLog(@"Failed to write video buffer to output.");
    }
}

@end
like image 26
herbrandson Avatar answered Oct 31 '22 18:10

herbrandson