iOS AVFoundation reverse 视频倒序存储

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容