美文网首页音视频
iOS AVFoundation reverse 视频倒序存储

iOS AVFoundation reverse 视频倒序存储

作者: 李田迎 | 来源:发表于2019-08-15 17:42 被阅读0次

    AVFoundation reverse play 倒放实现

    一.参考:

    http://www.andyhin.com/post/5/reverse-video-avfoundation
    

    二.几种实现方案思路

    1.预览过程使用AVPlayer的倒放功能

    设置AVPlayer的rate 为-1
    检查AVPlayerItem的canPlayReverse是否是YES
    
    由于我们采用的GPUImage框架,在预览过程没有使用AVPlayer,次方案没有继续调研
    

    2.使用AVAssetComposition把每帧位置翻转

    速度快,不生成临时文件
    必须每帧都是关键帧 否则严重卡顿 掉帧
    无法精确控制insert单帧TimeRange
    

    3.修改GPUImageMovie 倒序读取CVPixelBuffer (不可行)

    基于AVPlayerItemVideoOutput copyPixelBufferForItemTime方法,尝试倒序copy.
    实验发现该函数貌似不支持倒序读取,倒序读取前几秒返回正序的pixelBuffer,之后始终返回空.阅读函数文档也证明了这点.
    

    4.使用AVAssetReader AVAssetWriter 读取出每个CMSampleBuffer反向写入文件 (可行)

    播放非常流畅.
    但需要生成临时文件,处理时间较长.
    

    三.AVAssetReader AVAssetWriter倒序视频方案实现

    AHSVVideoReverse.h

    //
    //  AHSVVideoReverse.h
    //  AHVideoSDKFramework
    //
    //  Created by 李田迎 on 2019/8/13.
    //  Copyright © 2019 Autohome. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    #import <AVFoundation/AVFoundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface AHSVVideoReverse : NSObject
    
    @property (nonatomic, copy) NSDictionary *videoSettings;            //!< 写入视频配置参数 有默认参数
    @property (strong, nonatomic) NSDictionary *readerOutputSettings;   //!< 视频轨道读取出的数据格式
    
    /**
     根据原始视频生成倒放视频
    
     @param origAsset 被倒放视频asset
     @param outputPath 倒放的视频存储路径
     @param handler 回调信息block
     */
    - (void)reverseVideo:(AVAsset *)origAsset
              outputPath:(NSString *)outputPath
           completeBlock:(void (^)(NSError *error))handler;
    @end
    
    NS_ASSUME_NONNULL_END
    
    

    AHSVVideoReverse.m

    //
    //  AHSVVideoReverse.m
    //  AHVideoSDKFramework
    //
    //  Created by 田迎 on 2019/8/13.
    //  Copyright © 2019. All rights reserved.
    //
    
    #import "AHSVVideoReverse.h"
    #import "AVAsset+Addition.h"
    #import "AHVideoRecordCustomConfig.h"
    
    #define kClipMaxContainCount 10
    
    @interface AHSVVideoReverse ()
    @property (nonatomic, strong) AVAsset *origAsset;                   //!< 原始资源对象
    @property (nonatomic, strong) AVAssetReader *assetReader;           //!< 资源读取对象
    @property (nonatomic, strong) AVAssetWriter *assetWriter;           //!< 多媒体文件写入
    @property (nonatomic, strong) AVAssetWriterInput *videoWriterInput; //!< 视频写入 append
    @property (nonatomic, strong) AVAssetReaderTrackOutput *videoTrackOutput;   //!< 视频输出对象
    //adapter 有CVPixelBufferPool缓冲池提高写入效率 可以写入CVPixelBuffer 和时间戳
    @property (nonatomic, strong) AVAssetWriterInputPixelBufferAdaptor *videoPixelBufferAdaptor;
    
    @property (nonatomic, strong) NSURL *outputURL;                     //!< 输出文件Url
    @property (nonatomic, strong) dispatch_queue_t inputQueue;          //!< 多媒体数据写入队列
    @property (nonatomic, assign) CGSize targetSize;                    //!< 导出视频size
    @property (nonatomic, assign) float fps;                            //!< 帧率
    @property (nonatomic, strong) void (^completionHandler)(NSError *); //!< 回调block
    //内部逻辑使用
    @property (nonatomic, strong) NSMutableArray *sampleTimeArray;      //!< 存储采样时间戳数组
    @property (nonatomic, strong) NSMutableArray *clipTimeRangeArray;   //!< 分段处理时间段数组
    @end
    
    @implementation AHSVVideoReverse
    
    #pragma mark -
    #pragma mark LifeCycle Method
    - (instancetype)init {
        if (self = [super init]) {
            
        }
        
        return self;
    }
    
    - (void)dealloc {
        
    }
    
    #pragma mark -
    #pragma mark Public Method
    - (void)reverseVideo:(AVAsset *)origAsset
              outputPath:(NSString *)outputPath
           completeBlock:(void (^)(NSError *error))handler {
        
        self.completionHandler = handler;
        
        if (!origAsset) {
            NSError *error = [NSError errorWithDomain:@"com.avvideo.videoReverse" code:-100 userInfo:@{@"msg":@"参数origAsset 不能为空!"}];
            self.completionHandler(error);
            
            return;
        }
        
        if (!origAsset.videoTrack) {
            NSError *error = [NSError errorWithDomain:@"com.avvideo.videoReverse" code:-101 userInfo:@{@"msg":@"origAsset中不含有视频轨道信息!"}];
            self.completionHandler(error);
            
            return;
        }
        
        if (!outputPath || outputPath.length==0) {
            NSError *error = [NSError errorWithDomain:@"com.avvideo.videoReverse" code:-102 userInfo:@{@"msg":@"参数outputPath 不能为空!"}];
            self.completionHandler(error);
            
            return;
        }
        
        self.outputURL = [NSURL fileURLWithPath:outputPath];
        //本地目标文件清理
        if ([[NSFileManager defaultManager] fileExistsAtPath:outputPath]) {
            [[NSFileManager defaultManager] removeItemAtPath:outputPath error:nil];
        }
        
        self.origAsset = origAsset;
        
        WEAKSELF;
        [self.origAsset loadValuesAsynchronouslyForKeys:@[@"duration", @"tracks"] completionHandler:^{
            dispatch_async(weakSelf.inputQueue, ^{
                [weakSelf startReverseProcess];
            });
        }];
    }
    
    #pragma mark -
    #pragma mark Private Method
    
    - (void)startReverseProcess {
        [self cancelRevese];
        self.targetSize = self.origAsset.videoTrackSize;
        self.fps = self.origAsset.videoTrack.nominalFrameRate;
    
        //1. 生成每帧时间数组与分段数组
        [self generateSampleTimesArray];
        //2. 处理所有分段 正序读取 倒序写入
        [self processReadReverseWriter];
    }
    
    //生成每帧时间数组 用于获取倒序时每个CVPixelBuffer的精确时间戳 以及分段数组
    - (void)generateSampleTimesArray {
        if ([self.assetReader canAddOutput:self.videoTrackOutput]) {
            [self.assetReader addOutput:self.videoTrackOutput];
        }
        [self.assetReader startReading];
        
        CMSampleBufferRef sample;
        NSUInteger processIndex = 0;
        CMTime startTime = kCMTimeZero;
        CMTime endTime = kCMTimeZero;
        CMTime presentationTime = kCMTimeZero;
        
        while((sample = [self.videoTrackOutput copyNextSampleBuffer])) {
            presentationTime = CMSampleBufferGetPresentationTimeStamp(sample);
            NSValue *presentationValue = [NSValue valueWithBytes:&presentationTime objCType:@encode(CMTime)];
            [self.sampleTimeArray addObject:presentationValue];
            
            CFRelease(sample);
            sample = NULL;
            
            if (processIndex == 0) {
                startTime = presentationTime;
                processIndex ++;
                
            } else if (processIndex == kClipMaxContainCount-1) {
                endTime = presentationTime;
                
                CMTimeRange timeRange = CMTimeRangeMake(startTime, CMTimeSubtract(endTime, startTime));
                NSValue *timeRangeValue = [NSValue valueWithCMTimeRange:timeRange];
                [self.clipTimeRangeArray addObject:timeRangeValue];
                
                processIndex = 0;
                startTime = kCMTimeZero;
                endTime = kCMTimeZero;
                
            } else {
                processIndex ++;
            }
        }
        
        //处理不够kClipMaxContainCount数量的帧的timerange
        if (CMTIME_COMPARE_INLINE(kCMTimeZero, !=, startTime) && CMTIME_COMPARE_INLINE(kCMTimeZero, ==, endTime)) {
            
            endTime = presentationTime;
            
            //单独处理最后只剩一帧的情况
            if (CMTIME_COMPARE_INLINE(endTime, ==, startTime) &&
                processIndex == 1) {
                startTime = CMTimeSubtract(startTime, CMTimeMake(1, self.fps));
            }
            
            CMTimeRange timeRange = CMTimeRangeMake(startTime, CMTimeSubtract(endTime, startTime));
            NSValue *timeRangeValue = [NSValue valueWithCMTimeRange:timeRange];
            [self.clipTimeRangeArray addObject:timeRangeValue];
        }
    }
    
    - (void)processReadReverseWriter {
        CMSampleBufferRef sampleBuffer;
        
        //1.保护处理 清理之前可能未读取完的数据
        while((sampleBuffer = [self.videoTrackOutput copyNextSampleBuffer])) {
            CFRelease(sampleBuffer);
        }
        
        //2.为asserWriter添加writerInput 开始读写操作
        if ([self.assetWriter canAddInput:self.videoWriterInput]) {
            [self.assetWriter addInput:self.videoWriterInput];
        }
        [self videoPixelBufferAdaptor];
        BOOL success = [self.assetWriter startWriting];
        if (!success) {
            NSLog(@"self.assetWriter error = %@", self.assetWriter.error);
        }
        [self.assetWriter startSessionAtSourceTime:kCMTimeZero];
        
        NSUInteger clipCount = self.clipTimeRangeArray.count;
        //当前处理帧索引
        NSUInteger frameIndex = 0;
        for (NSInteger i=clipCount-1; i>=0; i--) {
            
            NSValue *clipTimeRangeValue = [self.clipTimeRangeArray objectAtIndex:i];
            [self.videoTrackOutput resetForReadingTimeRanges:@[clipTimeRangeValue]];
            
            //读取分段中所有帧到缓存数组
            NSMutableArray *tempSampleArray = [[NSMutableArray alloc] init];
            while((sampleBuffer = [self.videoTrackOutput copyNextSampleBuffer])) {
                [tempSampleArray addObject:(__bridge id)sampleBuffer];
                CFRelease(sampleBuffer);
            }
            
            //每个分段内的帧 倒序写入writer
            for (NSInteger j=0; j<tempSampleArray.count; j++) {
                //保护处理
                if (frameIndex >= self.sampleTimeArray.count) {
                    continue;
                }
                NSValue *timeValue = [self.sampleTimeArray objectAtIndex:frameIndex];
                CMTime frameTime = [timeValue CMTimeValue];
    //            CMTimeShow(frameTime);
                CVPixelBufferRef pixefBuffer = CMSampleBufferGetImageBuffer((__bridge CMSampleBufferRef)tempSampleArray[tempSampleArray.count - j - 1]);
                
                // append frames to output
                BOOL appendSuccess = NO;
                while (!appendSuccess) {
                    if (self.videoPixelBufferAdaptor.assetWriterInput.readyForMoreMediaData) {
                        appendSuccess = [self.videoPixelBufferAdaptor appendPixelBuffer:pixefBuffer withPresentationTime:frameTime];
                        
                        if (!appendSuccess) {
                            NSLog(@"appendPixelBuffer error at time: %lld", frameTime.value);
                        } else {
                            // NSLog(@"appendPixelBuffer success at time: %f", CMTimeGetSeconds(frameTime));
                        }
                    
                    } else {
                        // adaptor not ready
                        [NSThread sleepForTimeInterval:0.05];
                    }
                }
                
                frameIndex ++;
            }
        }
        
        [self.videoWriterInput markAsFinished];
        WEAKSELF;
        [self.assetWriter finishWritingWithCompletionHandler:^(){
            if (weakSelf.completionHandler) {
                weakSelf.completionHandler(nil);
            }
        }];
    }
    
    - (void)cancelRevese {
        if (!_inputQueue) {
            return;
        }
        
        if (_assetReader && _assetReader.status == AVAssetReaderStatusReading) {
            [self.assetReader cancelReading];
        }
        _assetReader = nil;
        
        if (_assetWriter && _assetWriter.status == AVAssetWriterStatusWriting) {
            [self.assetWriter cancelWriting];
        }
        _assetWriter = nil;
        
        if (_videoTrackOutput) {
            _videoTrackOutput = nil;
        }
        
        if (_videoWriterInput) {
            _videoWriterInput = nil;
        }
        
        if (_videoPixelBufferAdaptor) {
            _videoPixelBufferAdaptor = nil;
        }
        
        if (_clipTimeRangeArray) {
            _clipTimeRangeArray = nil;
        }
        
        if (_sampleTimeArray) {
            _sampleTimeArray = nil;
        }
    }
    
    
    #pragma mark -
    #pragma mark Get Method
    - (AVAssetReader *)assetReader {
        if (!_assetReader) {
            NSError *error;
            _assetReader = [[AVAssetReader alloc] initWithAsset:self.origAsset error:&error];
            if (error) {
                NSLog(@"assetReader 创建失败!! %@", error);
            }
        }
        
        return _assetReader;
    }
    
    - (AVAssetWriter *)assetWriter {
        if (!_assetWriter) {
            NSError *writerError;
            _assetWriter = [AVAssetWriter assetWriterWithURL:self.outputURL fileType:AVFileTypeQuickTimeMovie error:&writerError];
            _assetWriter.shouldOptimizeForNetworkUse = YES;
            if (writerError) {
                NSLog(@"assetWriter 创建失败 %@", writerError);
            }
        }
        
        return _assetWriter;
    }
    
    - (AVAssetReaderTrackOutput *)videoTrackOutput {
        if (!_videoTrackOutput) {
            _videoTrackOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:self.origAsset.videoTrack outputSettings:self.readerOutputSettings];
            //设置支持不按顺序读取数据 设置为YES后 resetForReadingTimeRanges方法才可用
            _videoTrackOutput.supportsRandomAccess = YES;
            //不需要修改sample中的CVPixelBuffer内容 所以不需要copy
            _videoTrackOutput.alwaysCopiesSampleData = NO;
        }
        
        return _videoTrackOutput;
    }
    
    - (AVAssetWriterInput *)videoWriterInput {
        if (!_videoWriterInput) {
            _videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:self.videoSettings];
            _videoWriterInput.expectsMediaDataInRealTime = NO;
            [_videoWriterInput setTransform:self.origAsset.videoTrack.preferredTransform];
        }
        
        return _videoWriterInput;
    }
    
    - (AVAssetWriterInputPixelBufferAdaptor *)videoPixelBufferAdaptor {
        if (!_videoPixelBufferAdaptor) {
            NSDictionary *pixelBufferAttributes = @{
                                                    (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
                                                    (id)kCVPixelBufferWidthKey: @(self.targetSize.width),
                                                    (id)kCVPixelBufferHeightKey: @(self.targetSize.height),
                                                    @"IOSurfaceOpenGLESTextureCompatibility": @YES,
                                                    @"IOSurfaceOpenGLESFBOCompatibility": @YES,
                                                    };
            _videoPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:self.videoWriterInput sourcePixelBufferAttributes:pixelBufferAttributes];
        }
        
        return _videoPixelBufferAdaptor;
    }
    
    - (NSDictionary *)readerOutputSettings {
        if (!_readerOutputSettings) {
            _readerOutputSettings = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange], kCVPixelBufferPixelFormatTypeKey, nil];
        }
        
        return _readerOutputSettings;
    }
    
    - (dispatch_queue_t)inputQueue {
        if (!_inputQueue) {
            _inputQueue = dispatch_queue_create("com.ahvideo.reverseInputQueue", DISPATCH_QUEUE_SERIAL);
        }
        
        return _inputQueue;
    }
    
    - (NSDictionary *)videoSettings {
        if (!_videoSettings) {
            _videoSettings = @{AVVideoCodecKey: AVVideoCodecH264,
                               AVVideoWidthKey: @(self.targetSize.width),
                               AVVideoHeightKey: @(self.targetSize.height),
                               AVVideoCompressionPropertiesKey: @{
                                       AVVideoAverageBitRateKey: @(kDefaultVideoBitRate * 1000),
                                       AVVideoExpectedSourceFrameRateKey : @(kDefaultVideoFrameRate),
                                       AVVideoMaxKeyFrameIntervalKey : @(kDefaultVideoKeyFrameInterval),
                                       AVVideoProfileLevelKey: kDefaultVideoProfileLevel
                                       },
                               };
        }
        
        return _videoSettings;
    }
    
    - (NSMutableArray *)sampleTimeArray {
        if (!_sampleTimeArray) {
            _sampleTimeArray = [[NSMutableArray alloc] initWithCapacity:100];
        }
        
        return _sampleTimeArray;
    }
    
    - (NSMutableArray *)clipTimeRangeArray {
        if (!_clipTimeRangeArray) {
            _clipTimeRangeArray = [[NSMutableArray alloc] initWithCapacity:20];
        }
        
        return _clipTimeRangeArray;
    }
    
    #pragma mark -
    #pragma mark Set Method
    @end
    
    

    相关文章

      网友评论

        本文标题:iOS AVFoundation reverse 视频倒序存储

        本文链接:https://www.haomeiwen.com/subject/jlctsctx.html