iOS 自定义AVAssetExportSession

简介:

使用AVAssetWriter AVAssetReader 等相关类,实现了AVAssetExportSession相关功能,最初的目的是为定位AVAssetExportSession导出过程出现卡住,不回调block的bug.

说明:

1.本类编写借鉴了SDAVAssetExportSession

2.本类目前不支持实时进度功能

3.copyNextSampleBuffer 函数不返回,是导致export过程卡住的原因, 需要修改videoComposition的CMTimeScale来避免.尽量不要出现让reader读取不是整数帧时间(CMTime)的情况发生.

例如:CMTime start = CMTimeMakeWithSeconds(CMTimeGetSeconds(_clipRange.start), 30);

简易流程图

image.png

//

//  AHAVAssetExportSession.h

//  demo

//

//  Created by 李田迎 on 2019/7/18.

//  Copyright © 2019. All rights reserved.

//  自定义实现 多媒体数据导出到本地功能

#import

#import

NS_ASSUME_NONNULL_BEGIN

@interfaceAHAVAssetExportSession :NSObject

//入参

@property(nonatomic,copy)NSString*outputFileType;          //!< 导出文件类型

@property(nonatomic,copy)NSURL*outputURL;                  //!< 导出资源路径

@property(nonatomic,copy)NSDictionary*videoSettings;        //!< 视频配置参数 有默认参数

@property(nonatomic,copy)NSDictionary*audioSettings;        //!< 音频配置参数 有默认参数

@property(nonatomic,copy)NSArray*metadata;                  //!< 多媒体数据中的元数据

@property(nonatomic,copy)AVVideoComposition*videoComposition;//!< 视频轨道组合对象

@property(nonatomic,copy)AVAudioMix*audioMix;              //!< 音轨混合对象

@property(nonatomic,assign)CGSizetargetSize;                //!< 导出视频size

@property(nonatomic,assign)BOOLshouldOptimizeForNetworkUse;//!< 是否使用网络优化

//出参

@property (nonatomic, assign, readonly) AVAssetExportSessionStatusstatus;  //!< 导出状态

@property(nonatomic,strong,readonly)NSError*error;                    //!< 导出错误信息

@property(nonatomic,assign,readonly)CGFloatprogress;                  //!< 导出进度 0-1

/**

 初始化方法

 @param asset 多媒体资源

 @return 本类实例对象

 */

- (id)initWithAsset:(AVAsset*)asset;

/**

 导出函数

 @param handler 导出完成回调block

 */

- (void)exportAsynchronouslyWithCompletionHandler:(void(^)(void))handler;

/**

 取消中断导出操作

 */

- (void)cancelExport;

@end

NS_ASSUME_NONNULL_END


//

//  AHAVAssetExportSession.m

//  demo

//

//  Created by 李田迎 on 2019/7/18.

//  Copyright © 2019. All rights reserved.

// 

#import "AHAVAssetExportSession.h"

#import "AHVideoRecordDef.h"

#import "AHVideoRecordCustomConfig.h"

@interface AHAVAssetExportSession()

@property(nonatomic,strong)AVAssetReader*assetReader;      //!< 资源读取对象

//!< 视频组合读取输出对象

@property (nonatomic, strong) AVAssetReaderVideoCompositionOutput *videoOutput;

//!< 音频混合读取输出对象

@property (nonatomic, strong) AVAssetReaderAudioMixOutput *audioOutput;

@property(nonatomic,strong)AVAssetWriter*assetWriter;      //!< 多媒体文件写入

@property (nonatomic, strong) AVAssetWriterInput*videoInput;  //!< 视频写入 append

@property (nonatomic, strong) AVAssetWriterInput*audioInput;  //!< 音频写入对象

@property (nonatomic, strong) AVAssetWriterInputPixelBufferAdaptor *videoPixelBufferAdaptor;

@property(nonatomic,strong)dispatch_queue_tinputQueue;      //!< 多媒体数据写入队列

@property(nonatomic,strong) AVAsset *asset;                  //!< 素材资源对象

@property(nonatomic,strong)void(^completionHandler)(void);  //!< 回调block

@property (nonatomic, assign, readwrite) CGFloat progress;      //!< 处理进度

@end

@implementationAHAVAssetExportSession {

    NSError *_error;

}

#pragma mark LifeCycle Method

- (id)initWithAsset:(AVAsset *)asset {

    if(self= [superinit]) {

        self.asset = asset;

        _targetSize = CGSizeMake(KVideoPixelsWidth, kVideoPixelsHeight);

    }



    return self;

}

- (void)dealloc {



}

#pragma mark Public Method

- (void)exportAsynchronouslyWithCompletionHandler:(void(^)(void))handler {

    [selfcancelExport];

    self.completionHandler = handler;



    //1.为添加assetReader添加视频音频读写对象

    if([self.assetReader canAddOutput:self.videoOutput]) {

        [self.assetReader addOutput:self.videoOutput];

    }

    if([self.assetReader canAddOutput:self.audioOutput]) {

        [self.assetReader addOutput:self.audioOutput];

    }



    //2.为asserWriter添加writerInput

    if([self.assetWriter canAddInput:self.videoInput]) {

        [self.assetWriter addInput:self.videoInput];

    }

    if([self.assetWriter canAddInput:self.audioInput]) {

        [self.assetWriter addInput:self.audioInput];

    }



    //3.开始读写操作

    [self.assetWriter startWriting];

    [self.assetReader startReading];

    [self.assetWriter startSessionAtSourceTime:kCMTimeZero];



    __blockBOOLvideoCompleted =NO;

    __blockBOOLaudioCompleted =NO;

    __weak typeof(self) wself = self;

    //4.请求视频轨道写入数据

    [self.videoInput requestMediaDataWhenReadyOnQueue:self.inputQueue usingBlock:^

     {

         if(![wself encodeReadySamplesFromOutput:wself.videoOutput toInput:wself.videoInput])

         {

             @synchronized(wself)

             {

                 videoCompleted =YES;

                 if(audioCompleted)

                 {

                     [wselffinish];

                 }

             }

         }

     }];



    //5.请求音频轨道写入数据

    [self.audioInput requestMediaDataWhenReadyOnQueue:self.inputQueue usingBlock:^ {

         if (![wself encodeReadySamplesFromOutput:wself.audioOutput toInput:wself.audioInput]) {

             @synchronized(wself) {

                 audioCompleted =YES;

                 if(videoCompleted) {

                     [wselffinish];

                 }

             }

         }

     }];

}

- (void)cancelExport {

    if (!_inputQueue) {

        return;

    }



    dispatch_async(self.inputQueue, ^{

        [self.assetWritercancelWriting];

        [self.assetReadercancelReading];

        [selfcomplete];

        [selfreset];

    });

}

#pragma mark Private Method

- (BOOL)encodeReadySamplesFromOutput:(AVAssetReaderOutput*)output toInput:(AVAssetWriterInput*)input {

    while (input.isReadyForMoreMediaData) {

        CMSampleBufferRef sampleBuffer = [output copyNextSampleBuffer];

        if(sampleBuffer) {

            BOOLhandled =NO;

            BOOLerror =NO;



            if (self.assetReader.status != AVAssetReaderStatusReading || self.assetWriter.status != AVAssetWriterStatusWriting) {

                handled =YES;

                error =YES;

            }



            if(!handled && ![inputappendSampleBuffer:sampleBuffer]) {

                error =YES;

            }

            CFRelease(sampleBuffer);



            if(error) {

                returnNO;

            }



        }else{

            [inputmarkAsFinished];

            returnNO;

        }

    }



    return YES;

}

- (void)reset {

    _error=nil;

    self.progress=0;

    if (_assetReader) {

        _assetReader=nil;

    }

    if(_videoOutput) {

        _videoOutput =nil;

    }

    if(_audioOutput) {

        _audioOutput =nil;

    }



    if(_assetWriter) {

        _assetWriter =nil;

    }

    if(_videoInput) {

        _videoInput =nil;

    }

    if(_videoPixelBufferAdaptor) {

        _videoPixelBufferAdaptor =nil;

    }

    if(_audioOutput) {

        _audioOutput =nil;

    }

    if(_inputQueue) {

        _inputQueue =nil;

    }



    self.completionHandler =nil;

}

- (void)finish {

    // Synchronized block to ensure we never cancel the writer before calling finishWritingWithCompletionHandler

    if(self.assetReader.status == AVAssetReaderStatusCancelled ||self.assetWriter.status == AVAssetWriterStatusCancelled) {

        return;

    }



    if(self.assetWriter.status == AVAssetWriterStatusFailed) {

        [selfcomplete];



    }elseif(self.assetReader.status == AVAssetReaderStatusFailed) {

        [self.assetWriter cancelWriting];

        [selfcomplete];



    }else{

        [self.assetWriter finishWritingWithCompletionHandler:^ {

             [selfcomplete];

        }];

    }

}

- (void)complete {

    if(self.assetWriter.status == AVAssetWriterStatusFailed ||self.assetWriter.status == AVAssetWriterStatusCancelled) {

        [NSFileManager.defaultManager removeItemAtURL:self.outputURL error:nil];

    }



    if(self.completionHandler) {

        self.completionHandler();

        self.completionHandler =nil;

    }

}

- (NSError *)error {

    if(_error) {

        return_error;

    }else{

        returnself.assetWriter.error ? :self.assetReader.error;

    }

}

- (AVMutableVideoComposition *)buildDefaultVideoComposition {

    AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];

    AVAssetTrack *videoTrack = [[self.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];



    // get the frame rate from videoSettings, if not set then try to get it from the video track,

    // if not set (mainly when asset is AVComposition) then use the default frame rate of 30

    floattrackFrameRate =0;

    if(self.videoSettings) {

        NSDictionary *videoCompressionProperties = [self.videoSettings objectForKey:AVVideoCompressionPropertiesKey];



        if(videoCompressionProperties) {

            NSNumber *frameRate = [videoCompressionProperties objectForKey:AVVideoAverageNonDroppableFrameRateKey];

            if(frameRate) {

                trackFrameRate = frameRate.floatValue;

            }

        }



    }else{

        trackFrameRate = [videoTrack nominalFrameRate];

    }



    if(trackFrameRate ==0) {

        trackFrameRate = kDefaultVideoFrameRate;

    }



    videoComposition.frameDuration = CMTimeMake(1, trackFrameRate);

    CGSize targetSize = CGSizeMake([self.videoSettings[AVVideoWidthKey] floatValue], [self.videoSettings[AVVideoHeightKey] floatValue]);

    CGSize naturalSize = [videoTrack naturalSize];

    CGAffineTransform transform = videoTrack.preferredTransform;

    // Workaround radar 31928389, see https://github.com/rs/SDAVAssetExportSession/pull/70 for more info

    if(transform.ty == -560) {

        transform.ty =0;

    }



    if(transform.tx == -560) {

        transform.tx =0;

    }



    CGFloat videoAngleInDegree  = atan2(transform.b, transform.a) *180/ M_PI;

    if(videoAngleInDegree ==90|| videoAngleInDegree == -90) {

        CGFloat width = naturalSize.width;

        naturalSize.width = naturalSize.height;

        naturalSize.height = width;

    }

    videoComposition.renderSize = naturalSize;

    // center inside

    {

        floatratio;

        floatxratio = targetSize.width / naturalSize.width;

        floatyratio = targetSize.height / naturalSize.height;

        ratio = MIN(xratio, yratio);



        floatpostWidth = naturalSize.width * ratio;

        floatpostHeight = naturalSize.height * ratio;

        floattransx = (targetSize.width - postWidth) /2;

        floattransy = (targetSize.height - postHeight) /2;



        CGAffineTransform matrix = CGAffineTransformMakeTranslation(transx / xratio, transy / yratio);

        matrix = CGAffineTransformScale(matrix, ratio / xratio, ratio / yratio);

        transform = CGAffineTransformConcat(transform, matrix);

    }



    // Make a "pass through video track" video composition.

    AVMutableVideoCompositionInstruction *passThroughInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];

    passThroughInstruction.timeRange = CMTimeRangeMake(kCMTimeZero,self.asset.duration);



    AVMutableVideoCompositionLayerInstruction *passThroughLayer = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];



    [passThroughLayer setTransform:transform atTime:kCMTimeZero];



    passThroughInstruction.layerInstructions =@[passThroughLayer];

    videoComposition.instructions =@[passThroughInstruction];



    returnvideoComposition;

}

#pragma mark Get Method

- (AVAssetExportSessionStatus)status {

    switch(self.assetWriter.status) {

        default:

        caseAVAssetWriterStatusUnknown:

            returnAVAssetExportSessionStatusUnknown;

        caseAVAssetWriterStatusWriting:

            returnAVAssetExportSessionStatusExporting;

        caseAVAssetWriterStatusFailed:

            returnAVAssetExportSessionStatusFailed;

        caseAVAssetWriterStatusCompleted:

            returnAVAssetExportSessionStatusCompleted;

        caseAVAssetWriterStatusCancelled:

            returnAVAssetExportSessionStatusCancelled;

    }

}

- (dispatch_queue_t)inputQueue {

    if(!_inputQueue) {

        _inputQueue = dispatch_queue_create("AHSVVideoEncoderInputQueue", DISPATCH_QUEUE_SERIAL);

    }



    return_inputQueue;

}

- (AVAssetReader *)assetReader {

    if(!_assetReader) {

        NSError *error;

        _assetReader = [[AVAssetReader alloc] initWithAsset:self.asset error:&error];

        if(error) {

            NSLog(@"assetReader 创建失败!! %@", error);

        }

    }



    return_assetReader;

}

- (AVAssetReaderVideoCompositionOutput *)videoOutput {

    if(!_videoOutput) {

        NSArray *videoTracks = [self.asset tracksWithMediaType:AVMediaTypeVideo];

        _videoOutput = [[AVAssetReaderVideoCompositionOutput alloc] initWithVideoTracks:videoTracks videoSettings:nil];

        _videoOutput.videoComposition =self.videoComposition;

    }



    return_videoOutput;

}

- (AVAssetReaderAudioMixOutput *)audioOutput {

    if(!_audioOutput) {

        NSArray *audioTracks = [self.asset tracksWithMediaType:AVMediaTypeAudio];

        _audioOutput = [AVAssetReaderAudioMixOutput assetReaderAudioMixOutputWithAudioTracks:audioTracks audioSettings:nil];

        _audioOutput.audioMix =self.audioMix;

    }



    return_audioOutput;

}

- (AVAssetWriterInputPixelBufferAdaptor *)videoPixelBufferAdaptor {

    if(!_videoPixelBufferAdaptor) {

        NSDictionary *pixelBufferAttributes =@{

            (id)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA),

            (id)kCVPixelBufferWidthKey:@(self.videoOutput.videoComposition.renderSize.width),

            (id)kCVPixelBufferHeightKey:@(self.videoOutput.videoComposition.renderSize.height),

            @"IOSurfaceOpenGLESTextureCompatibility": @YES,

            @"IOSurfaceOpenGLESFBOCompatibility": @YES,

        };

        _videoPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:self.videoInput sourcePixelBufferAttributes:pixelBufferAttributes];

    }



    return_videoPixelBufferAdaptor;

}

- (AVVideoComposition *)videoComposition {

    if(!_videoComposition) {

        _videoComposition = [selfbuildDefaultVideoComposition];

    }



    return_videoComposition;

}

- (AVAssetWriter *)assetWriter {

    if(!_assetWriter) {

        NSError *writerError;

        _assetWriter = [AVAssetWriter assetWriterWithURL:self.outputURL fileType:self.outputFileType error:&writerError];

        _assetWriter.shouldOptimizeForNetworkUse =self.shouldOptimizeForNetworkUse;

        if(writerError) {

            NSLog(@"assetWriterc 创建失败 %@", writerError);

        }

    }



    return_assetWriter;

}

- (AVAssetWriterInput *)videoInput {

    if(!_videoInput) {

        _videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:self.videoSettings];

        _videoInput.expectsMediaDataInRealTime =NO;

    }



    return_videoInput;

}

- (AVAssetWriterInput *)audioInput {

    if(!_audioInput) {

        _audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:self.audioSettings];

        _audioInput.expectsMediaDataInRealTime =NO;

    }



    return_audioInput;

}

- (NSDictionary *)videoSettings {

    if(!_videoSettings) {

        _videoSettings =@{AVVideoCodecKey: AVVideoCodecH264,

                          AVVideoWidthKey:@(self.targetSize.width),

                          AVVideoHeightKey:@(self.targetSize.height),

                          AVVideoCompressionPropertiesKey:@{

                                  AVVideoAverageBitRateKey:@(6000000),

                                  AVVideoProfileLevelKey: AVVideoProfileLevelH264High40

                                  },

                          };

    }



    return_videoSettings;

}

- (NSDictionary *)audioSettings {

    if(!_audioSettings) {

        _audioSettings =@{AVFormatIDKey:@(kAudioFormatMPEG4AAC),

                          AVNumberOfChannelsKey:@(2),

                          AVSampleRateKey:@(44100),

                          AVEncoderBitRateKey:@(128000),

                          };

    }



    return_audioSettings;

}

#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