Pregunta Fundación AV: ¿por qué no puedo obtener la orientación correcta del video?


estoy usando AVCaptureSession para capturar video desde una cámara de dispositivos y luego usar AVAssetWriterInput y AVAssetTrack comprimir / cambiar el tamaño del video antes de subirlo a un servidor. Los videos finales se verán en la web a través de un elemento de video html5.

Me encuentro con varios problemas tratando de corregir la orientación del video. Mi aplicación solo admite orientación horizontal y todos los videos capturados deben tener orientación horizontal. Sin embargo, me gustaría permitir que el usuario sostenga su dispositivo en cualquier dirección horizontal (es decir, botón de inicio en el lado izquierdo o derecho).

Soy capaz de hacer que la vista previa del video muestre la orientación correcta con la siguiente línea de código

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

Los problemas comienzan cuando se procesa el video a través de AVAssetWriterInput y amigos. El resultado no parece tener en cuenta el modo horizontal izquierdo / derecho en el que se capturó el video. IOW, a veces el video sale boca abajo. Después de algunas búsquedas en Google, encontré a muchas personas que sugieren que la siguiente línea de código resolvería este problema

writerInput.transform = videoTrack.preferredTransform;

... pero esto no parece funcionar. Después de un poco de depuración encontré que videoTrack.preferredTransform siempre tiene el mismo valor, independientemente de la orientación en la que se haya capturado el video.

Intenté rastrear manualmente en qué orientación se capturó el video y configurando writerInput.transform a CGAffineTransformMakeRotation(M_PI) según sea necesario. Lo que resolvió el problema !!!

... sorta

Cuando vi los resultados en el dispositivo, esta solución funcionó como se esperaba. Los videos se colocaron correctamente, independientemente de la orientación izquierda / derecha durante la grabación. Desafortunadamente, cuando vi exactamente los mismos videos en otro navegador (Chrome en un libro de mac) estaban todos al revés!?!?!?

¿Qué estoy haciendo mal?

EDITAR

Aquí hay un código, en caso de que sea útil ...

-(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 */
     }];
}

5
2018-02-18 06:33


origen


Respuestas:


El problema es que modificar el writerInput.transform La propiedad solo agrega una etiqueta en los metadatos del archivo de video que le indica al reproductor de video que gire el archivo durante la reproducción. Es por eso que los videos juegan en la orientación correcta en su dispositivo (supongo que también juegan correctamente en un reproductor Quicktime).

Los almacenamientos intermedios de píxeles capturados por la cámara aún están dispuestos en la orientación en la que fueron capturados. Muchos reproductores de video no verificarán la etiqueta de metadatos de orientación preferida y solo reproducirán el archivo en la orientación de píxeles original.

Si desea que el usuario pueda grabar el video que tiene el teléfono en modo horizontal, debe rectificar esto en la AVCaptureSession nivel antes de la compresión realizando una transformación en el CVPixelBuffer de cada cuadro de video. Este Apple Q&A lo cubre (mira el AVCaptureVideoOutput documentación también): https://developer.apple.com/library/ios/qa/qa1744/_index.html

Investigar el enlace de arriba es la forma correcta de resolver su problema. Un suplente rápido y sucio La forma de resolver el mismo problema sería bloquear la UI de grabación de su aplicación en una sola orientación horizontal y luego rotar todos sus videos en el lado del servidor usando ffmpeg.


8
2018-02-21 21:09



Para comprimir / cambiar el tamaño del video, podemos usar AVAssetExportSession.

  • Podemos subir un video de duración de 3.30minutos.
  • Si la duración del video será de más de 3.30 minutos, mostrará una advertencia de memoria.
  • Como aquí no estamos usando ninguna transformación para el video, el video será como está mientras se graba.
  • A continuación se muestra el código de ejemplo para comprimir el video.
  • Podemos comprobar el tamaño del video antes de la compresión y después de la compresión.

{

    -(void)trimVideoWithURL:(NSURL *)inputURL{


NSString *path1 = [inputURL path];
NSData *data = [[NSFileManager defaultManager] contentsAtPath:path1];
NSLog(@"size before compress video is %lu",(unsigned long)data.length);

AVURLAsset *asset = [AVURLAsset URLAssetWithURL:inputURL options:nil];
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:asset presetName:AVAssetExportPreset640x480];

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *outputURL = paths[0];
NSFileManager *manager = [NSFileManager defaultManager];
[manager createDirectoryAtPath:outputURL withIntermediateDirectories:YES attributes:nil error:nil];
outputURL = [outputURL stringByAppendingPathComponent:@"output.mp4"];
fullPath = [NSURL URLWithString:outputURL];

// Remove Existing File

[manager removeItemAtPath:outputURL error:nil];

exportSession.outputURL = [NSURL fileURLWithPath:outputURL];
exportSession.shouldOptimizeForNetworkUse = YES;
exportSession.outputFileType = AVFileTypeQuickTimeMovie;

CMTime start = CMTimeMakeWithSeconds(1.0, 600);
CMTime duration = CMTimeMakeWithSeconds(1.0, 600);
CMTimeRange range = CMTimeRangeMake(start, duration);
exportSession.timeRange = range;

[exportSession exportAsynchronouslyWithCompletionHandler:^(void)
 {
     switch (exportSession.status) {

         case AVAssetExportSessionStatusCompleted:{

             NSString *path = [fullPath path];
             NSData *data = [[NSFileManager defaultManager] contentsAtPath:path];
             NSLog(@"size after compress video is %lu",(unsigned long)data.length);
             NSLog(@"Export Complete %d %@", exportSession.status, exportSession.error);
             /*
              Do your neccessay stuff here after compression
              */

         }
             break;
         case AVAssetExportSessionStatusFailed:
             NSLog(@"Failed:%@",exportSession.error);
             break;
         case AVAssetExportSessionStatusCancelled:
             NSLog(@"Canceled:%@",exportSession.error);
             break;
         default:
             break;
     }
 }];}

1
2018-03-10 12:12



En caso de que sea útil para alguien, aquí está el código con el que terminé. Terminé teniendo que hacer el trabajo en el video mientras se estaba capturando en lugar de hacerlo como un paso de procesamiento posterior. Esta es una clase auxiliar que gestiona la captura.

Interfaz

#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

Implementación (con un poco de edición y algunos pequeños TODO)

@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

0
2018-02-27 20:51