AVFoundation开发秘籍笔记-08读取与写入媒体

一、综述

AVFoundation定义了一组功能可以用于创建媒体应用程序时遇到的大部分用例场景。
还有一些功能不受AVFoundation框架的内置支持,需要使用框架的AVAssetReaderAVAssetWriter类提供的低级功能,可以直接处理媒体样本。

1、AVAssetReader

用于从AVAsset中读取媒体样本,通常会配置一个或多个AVAssetReaderOutput实例,并通过copyNextSampleBuffer方法访问音频样本和视频帧。

AVAssetReaderOutput是一个抽象类,不过框架定义了具体实例来从指定的AVAssetTrack中读取解码的媒体样本,从多音频轨道中读取混合输出,或者从多视频轨道总读取组合输出。

  • AVAssetReaderAudioMixOutput
  • AVAssetReaderTrackOutput
  • AVAssetReaderVideoCompositionOutput
  • AVAssetReaderSampleReferenceOutput

一个资源读取器内部通道都是以多线程的方式不断提取下一个可用样本的,这样可以在系统请求资源时最小化时延。尽管提供了低时延的检索操作,还是不倾向于实时操作,比如播放。

AVAssetReader只针对于带有一个资源的媒体样本,如果需要同时从多个基于文件的资源中读取样本,可将它们组合到一个AVAsset子类AVComposition中。

NSURL *fileUrl ;
AVAsset *asset = [AVAsset assetWithURL:fileUrl];
AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
    
NSError *serror;
self.assetReader = [[AVAssetReader alloc] initWithAsset:asset error:&serror];
    
NSDictionary *readerOutputSetting = @{(id)kCVPixelBufferPixelFormatTypeKey :@(kCVPixelFormatType_32BGRA)};

AVAssetReaderTrackOutput *trackOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:readerOutputSetting];
//从资源视频轨道中读取样本,将视频帧解压缩为BGRA格式。

if ([self.assetReader canAddOutput:trackOutput]) {
    [self.assetReader addOutput:trackOutput];
}
[self.assetReader startReading];

2、AVAssetWriter

对媒体资源进行编码并将其写入到容器文件中,日服一个MPEG-4文件或一个QuickTime文件。

它由一个或多个AVAssetWriterInput对象配置,用于附加将包含要写入容器的媒体样本的CMSampleBuffer对象。

AVAssetWriterInput被配置为可以处理指定的媒体类型,比如音频或视频,并且附加在其后的样本会在最终输出时生成一个独立的AVAssetTrack。当使用一个配置了处理视频样本的AVAssetWriterInput时,会常用到一个专门的适配器对象AVAssetWriterInputPixelBufferAdaptor,这个类在附加被包装为CVPixelBuffer对象的视频样本时提供最优性能。

输入信息也可以通过使用AVAssetWriterInputGroup组成互斥的参数,可以创建特定资源,包含在播放时使用AVMediaSelectionGroupAVMediaSelectionOption类选择的指定语言媒体轨道。

AVAssetWriter可以自动支持交叉媒体样本。AVAssetWriterInput提供一个readyForMoreMediaData属性来指示在保持所需的交错情况下输入信息是否还可以附加更多数据,只有在这个属性值为YES时才可以将一个新的样本添加到输入信息中。

AVAssetWriter可用于实时操作和离线操作两种情况。对于每个场景中都有不同的方法将样本buffer添加到写入对象的输入中。

  • 实时:处理实时资源时,比如从AVCaptureVideoDataOutput写入捕捉的样本时,AVAssetWriter应该另expectsMediaDataInRealTime为YES来确保readyForMoreMediaData值被正确计算。从实时资源写入数据优化了写入器,与维持理想交错效果相比,快速写入样本具有更高的优先级。
  • 离线:当从离线资源中读取媒体资源时,比如从AVAssetReader读取样本buffer,在附加样本前仍需写入器输入的readyForMoreMediaData属性的状态,不过可以使用requestMediaDataWhenReadyOnQueue:usingBlock:方法控制数据的提供。传到这个方法中的代码块会随写入器输入准备附加更多的样本而不断被调用。添加样本时需要检索数据并从资源中找到下一个样本进行添加。
NSURL *outputUrl ;
    
NSError *wError;
self.assetWriter = [[AVAssetWriter alloc] initWithURL:outputUrl fileType:AVFileTypeQuickTimeMovie error:&wError];
    
NSDictionary *writerOutputSettings =
@{
AVVideoCodecKey:AVVideoCodecH264,
AVVideoWidthKey:@1280,
AVVideoHeightKey:@720,
AVVideoCompressionPropertiesKey:@{
        AVVideoMaxKeyFrameIntervalKey:@1,
        AVVideoAverageBitRateKey:@10500000,
        AVVideoProfileLevelKey:AVVideoProfileLevelH264Main31,
        }
    
};
    
AVAssetWriterInput *writerInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:writerOutputSettings];
if ([self.assetWriter canAddInput:writerInput]) {
    [self.assetWriter addInput:writerInput];
}
[self.assetWriter startWriting];

与AVAssetExportSession相比,AVAssetWriter明显的优势是它对输出进行编码时能够进行更加细致的压缩设置控制。可以指定关键帧间隔、视频比特率、H.264配置文件、像素宽高比和纯净光圈等设置。

3、示例,从非实时资源中写入样本

dispatch_queue_t dispatchQueue = dispatch_queue_create("com.writerQueue", NULL);
    
[self.assetWriter startSessionAtSourceTime:kCMTimeZero];
//创建一个新的写入会话,传递资源样本的开始时间。
    
/**
 在写入器输入准备好添加更多样本时,被不断调用。
 每次调用期间,输入准备添加更多数据时,再从轨道的输出中复制可用的样本,并附加到输入中。

 所有样本从轨道输出中复制后,标记AVAssetWriterInput已经结束并指明添加操作已完成。
 
 **/
    
[writerInput requestMediaDataWhenReadyOnQueue:dispatchQueue usingBlock:^{
    BOOL complete = NO ;
    
    while ([writerInput isReadyForMoreMediaData] && !complete) {
        CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer];
        if (sampleBuffer) {
            BOOL result = [writerInput appendSampleBuffer:sampleBuffer];
            CFRelease(sampleBuffer);
            complete = !result;
        } else {
            [writerInput markAsFinished];
            complete = YES;
        }
    }
    
    if (complete) {
        [self.assetWriter finishWritingWithCompletionHandler:^{
            AVAssetWriterStatus status = self.assetWriter.status;
            if (status == AVAssetWriterStatusCompleted) {
                //
            } else {
                
            }
        }];
    }
}];

二、创建音频波形(waveform)视图

绘制波形三个步骤:

  • 1、读取,读取音频样本进行渲染。需要读取或可能解压缩音频数据。
  • 2、缩减,实际读取到的样本数量要远比在屏幕上渲染的多。缩减过程必须作用域样本集,这一过程包括样本总量分为小的样本块,并在每个样本块上找到最大的样本、所有样本的平均值或min/max值。
  • 3、渲染,将缩减后的样本呈现在屏幕上。通常用到Quartz框架,可以使用苹果支持的绘图框架。如何绘制这些数据的类型取决于如何缩减样本的。采用min/max对,怎为它的每一对绘制一条垂线。如果使用每个样本块平均值或最大值,使用Quartz Bezier路径绘制波形。

1、读取音频样本 --提取全部样本集合

  • 1、加载AVAsset资源轨道数据
  • 2、加载完成之后,创建AVAssertReader,并配置AVAssetReaderTrackOutput
  • 3、AVAssertReader读取数据,并将读取到的样本数据添加到NDSdata实例后面。
+ (void)loadAudioSamplesFromAsset:(AVAsset *)asset
                  completionBlock:(THSampleDataCompletionBlock)completionBlock {
    
    // Listing 8.2
    
    NSString *tracks = @"tracks";
    [asset loadValuesAsynchronouslyForKeys:@[tracks] completionHandler:^{
        AVKeyValueStatus status = [asset statusOfValueForKey:tracks error:nil];
        NSData *sampleData = nil;
        
        if (status == AVKeyValueStatusLoaded) { //资源已经加载完成
            sampleData = [self readAudioSamplesFromAsset:asset];
        }
        
        dispatch_async(dispatch_get_main_queue(), ^{
            completionBlock(sampleData);
        });
    }];
}

+ (NSData *)readAudioSamplesFromAsset:(AVAsset *)asset {

    // Listing 8.3
    NSError *error = nil;
    
    AVAssetReader *assetReader = [[AVAssetReader alloc] initWithAsset:asset error:&error];
    //创建一个AVAssetReader实例,并赋给他一个资源读取。
    
    if (!assetReader) {
        NSLog(@"error creating asset reader :^%@",error);
        return nil;
    }
    
    AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeAudio] firstObject];
    //获取资源找到的第一个音频轨道,根据期望的媒体类型获取轨道。
    
    
    NSDictionary *outputSettings =
  @{
    AVFormatIDKey:@(kAudioFormatLinearPCM),//样本需要以未压缩的格式被读取
    AVLinearPCMIsBigEndianKey:@NO,
    AVLinearPCMIsFloatKey:@NO,
    AVLinearPCMBitDepthKey:@(16)
    };
    //创建NSDictionary保存从资源轨道读取音频样本时使用的解压设置。
    
    AVAssetReaderTrackOutput *trackOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:outputSettings];
    if ([assetReader canAddOutput:trackOutput]) {
        [assetReader addOutput:trackOutput];
    }
    //创建新的AVAssetReaderTrackOutput实例,将创建的输出设置传递给它,
    //将其作为AVAssetReader的输出并调用startReading来允许资源读取器开始预收取样本数据。
    
    [assetReader startReading];
    
    NSMutableData *sampleData = [NSMutableData data];
    while (assetReader.status == AVAssetReaderStatusReading) {
        CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer];
        //调用跟踪输出的方法开始迭代,每次返回一个包含音频样本的下一个可用样本buffer。
        
        if (sampleBuffer) {
            CMBlockBufferRef blockBufferRef = CMSampleBufferGetDataBuffer(sampleBuffer);
            //CMSampleBuffer中的音频样本包含在一个CMBlockBuffer类型中
            //CMSampleBufferGetDataBuffer函数可以方位block buffer
            
            size_t length = CMBlockBufferGetDataLength(blockBufferRef);
            SInt16 sampleBytes[length];
            //确定长度并创建一个16位带符号整型数组来保存音频样本
            
 CMBlockBufferCopyDataBytes(blockBufferRef, 0, length, sampleBytes);
            //生成一个数组,数组中元素为CMBlockBuffer所包含的数据
            
            [sampleData appendBytes:sampleBytes length:length];
            //将数组数据内容附加在NDSData实例后面。
            
            CMSampleBufferInvalidate(sampleBuffer);
            //指定样本buffer已经处理和不可再继续使用
            
            CFRelease(sampleBuffer);
            //释放CMSampleBuffer副本来释放内容
        }
    }
    
    if (assetReader.status == AVAssetReaderStatusCompleted) {
        //数据读取成功,返回包含音频样本数据的NData
        return sampleData;
    } else {
        NSLog(@"Failed to read audio samples from asset");
        return nil;
    }
    return nil;
}

2、缩减音频样本

根据指定压缩空间,压缩样本。即将,总样本分块,取每块子样本最大值,重新组成新的音频样本集合。

//指定尺寸约束筛选数据集
- (NSArray *)filteredSamplesForSize:(CGSize)size {

    NSMutableArray *filterDataSamples = [[NSMutableArray alloc] init];
    NSUInteger sampleCount = self.sampleData.length/sizeof(SInt16);
    //样本总长度
    NSUInteger binSize = sampleCount/size.width;
    //子样本长度
    
    SInt16 *bytes = (SInt16 *)self.sampleData.bytes;
    SInt16 maxSample = 0;
    
    for (NSUInteger i = 0; i < sampleCount; i += binSize) {
        //迭代所有样本集合
        SInt16 sampleBin[binSize];
        for (NSUInteger j = 0; j < binSize; j ++) {
            sampleBin[j] = CFSwapInt16LittleToHost(bytes[i+j]);
            //CFSwapInt16LittleToHost确保样本是按主机内置的字节顺序处理
        }
        SInt16 value = [self maxValueInArray:sampleBin ofSize:binSize];
        
        [filterDataSamples addObject:@(value)];
        //找到样本最大绝对值。
        
        if (value > maxSample) {
            maxSample = value;
        }
    }
    
    CGFloat scaleFactor = (size.height/2) / maxSample;
    //所有样本中的最大值,计算筛选样本使用的比例因子
    
    for (NSUInteger i = 0; i < filterDataSamples.count; i ++) {
        filterDataSamples[i] = @([filterDataSamples[i] integerValue] *scaleFactor);
    }
    
    return filterDataSamples;
}

- (SInt16)maxValueInArray:(SInt16[])values ofSize:(NSUInteger)size {
    
    SInt16 maxValue = 0;
    for (int i = 0; i < size; i ++) {
        if (abs(values[i]) > maxValue) {
            maxValue = abs(values[i]);
        }
    }
    return maxValue;
}

3、渲染音频样本

将筛选出来的音频样本数据,绘制成波形图。这里使用Quartz的Bezier绘制。

- (void)setAsset:(AVAsset *)asset {
    if (_asset != asset) {
        _asset = asset;
        [THSampleDataProvider loadAudioSamplesFromAsset:asset completionBlock:^(NSData *sampleData) {
            self.filter = [[THSampleDataFilter alloc] initWithData:sampleData];
            [self.loadingView stopAnimating];
            [self setNeedsDisplay];
        }];
    }
}

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    //在视图内呈现这个波形,首先基于定义的宽和高常量来缩放图像上下文
    CGContextScaleCTM(context, THWidthScaling, THHeightScaling);
    
    //计算x,y偏移量,转换上下文,在缩放上下文中适当调整便宜
    CGFloat xOffset = self.bounds.size.width-self.bounds.size.width*THWidthScaling;
    CGFloat yOffset = self.bounds.size.height-self.bounds.size.height*THHeightScaling;
    CGContextTranslateCTM(context, xOffset/2, yOffset/2);
    
    //获取筛选样本,并传递视图边界的尺寸。
    //实际可能希望在drawRect方法之外执行这一检索操作,这样在筛选样本时会有更好的优化效果
    NSArray *filteredSamples = [self.filter filteredSamplesForSize:self.bounds.size];
    
    CGFloat midY = CGRectGetMidY(rect);
    
    //创建一个新的CGMutablePathRef,用来绘制波形Bezier路径的上半部
    CGMutablePathRef halfPath = CGPathCreateMutable();
    CGPathMoveToPoint(halfPath, NULL, 0.0f, midY);
    
    for (NSUInteger i = 0; i < filteredSamples.count; i ++) {
        float sample = [filteredSamples[i] floatValue];
        //每次迭代,向路径中添加一个点,索引i作为x坐标,样本值作为y坐标
        CGPathAddLineToPoint(halfPath, NULL, i, midY-sample);
    }
    
    //创建第二个CGMutablepathRef,是Bezier路径绘制完整波形
    CGPathAddLineToPoint(halfPath, NULL, filteredSamples.count, midY);
    
    CGMutablePathRef fullPath = CGPathCreateMutable();
    CGPathAddPath(fullPath, NULL, halfPath);
    
    //要绘制波形下半部,需要对上半部路径应用translate和scale变化,是的上半部路径翻转到下面,填满整个波形
    CGAffineTransform transform = CGAffineTransformIdentity;
    transform = CGAffineTransformTranslate(transform, 0, CGRectGetHeight(rect));
    transform = CGAffineTransformScale(transform, 1.0, -1.0);
    CGPathAddPath(fullPath, &transform, halfPath);
    
    //将完整路径添加到图像上下文,根据指定的waveColor设置填充色。并绘制路径到图像上下文。
    CGContextAddPath(context, fullPath);
    CGContextSetFillColorWithColor(context, self.waveColor.CGColor);
    CGContextDrawPath(context, kCGPathFill);
    
    //创建Quartz对象,在使用之后释放相应内存。
    CGPathRelease(halfPath);
    CGPathRelease(fullPath);
}

三、捕捉录制的高级方法

AVCaptureVideoDataOutput捕捉的CVPixelBuffer对象最为OpenGL ES的贴图来呈现,这是一个强大的功能,不过使用AVCaptureVideoDataOutput的一个问题是会失去AVCaptureMovieFileOutput来记录输出的便利性。

AVCaptureVideoDataOutputAVCaptureAudioDataOutput如果需要对数据进行更复杂的处理,要为每一个使用单独的队列。

1、实现捕捉会话配置

self.captureSession = [[AVCaptureSession alloc] init];
    self.captureSession.sessionPreset = AVCaptureSessionPresetMedium;

AVCaptureDevice *videoDevice =
    [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

AVCaptureDeviceInput *videoInput =
    [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
if (videoInput) {
    if ([self.captureSession canAddInput:videoInput]) {
        [self.captureSession addInput:videoInput];
        self.activeVideoInput = videoInput;
    } else {
        
    }
} else {
        
}

// Setup default microphone
AVCaptureDevice *audioDevice =
    [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];

AVCaptureDeviceInput *audioInput =
    [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:error];
if (audioInput) {
    if ([self.captureSession canAddInput:audioInput]) {
        [self.captureSession addInput:audioInput];
    } else {
        
    }
} else {

}

self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    
//设置输出格式kCVPixelFormatType_32BGRA,结合OpenGL ES和CoreImage时这一格式非常适合。
NSDictionary *outputSettigns = @{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)};

self.videoDataOutput.videoSettings = outputSettigns;
    
//要记录输出内容,所以通常我们希望捕捉全部的可用帧
//设置alwaysDiscardsLateVideoFrames为NO,会给委托方法一些额外的时间来处理样本buffer
self.videoDataOutput.alwaysDiscardsLateVideoFrames = NO;
    
[self.videoDataOutput setSampleBufferDelegate:self queue:self.dispatchQueue];
    
if ([self.captureSession canAddOutput:self.videoDataOutput]) {
    [self.captureSession addOutput:self.videoDataOutput];
} else {
    NSLog(@"add video data output error");
}
    
    
//捕捉音频样本
self.audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];
[self.audioDataOutput setSampleBufferDelegate:self queue:self.dispatchQueue];
if ([self.captureSession canAddOutput:self.audioDataOutput]) {
    [self.captureSession addOutput:self.audioDataOutput];
} else {
    NSLog(@"add audio data output error");
}

NSString *fileType = AVFileTypeQuickTimeMovie;
    NSDictionary *videoSettings = [self.videoDataOutput recommendedVideoSettingsForAssetWriterWithOutputFileType:fileType];
    NSDictionary *audioSettings = [self.audioDataOutput recommendedAudioSettingsForAssetWriterWithOutputFileType:fileType];
    self.movieWriter = [[THMovieWriter alloc] initWithVideoSettings:videoSettings audioSettings:audioSettings dispatchQueue:self.dispatchQueue];
    self.movieWriter.delegate = self;
    

保存视频到相册

- (void)didWriteMovieAtURL:(NSURL *)outputURL {

    // Listing 8.17
    
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
    if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:outputURL]) {
        //检验是否可以写入
        
        ALAssetsLibraryWriteVideoCompletionBlock completionBlock;
        completionBlock = ^(NSURL *assetURL, NSError *error) {
            if (error) {
                [self.delegate assetLibraryWriteFailedWithError:error];
            } else {
                
            }
        };
        
        [library writeVideoAtPathToSavedPhotosAlbum:outputURL completionBlock:completionBlock];
        
    }
    
}

2、代理回调方法处理

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection {

    //处理视频帧,并写入
    [self.movieWriter processSampleBuffer:sampleBuffer];
    
    // Listing 8.11
    if (captureOutput == self.videoDataOutput) {
        //获取基础CVPixelBuffer
        CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
        //从CVPixelBuffer中创建一个新的CIImage,并将它传递给需要在屏幕上呈现的图片目标
        CIImage *sourceImage = [CIImage imageWithCVPixelBuffer:imageBuffer options:nil];
        
        //将图片在preview上展示,这个时候可以对图片做相关处理。加滤镜的内容后面再加。
        [self.imageTarget setImage:sourceImage];
    }
    
}

3、创建文件写入

创建一个对象,通过AVAssetWriter执行视频编码和文件写入。

将功能封装
.h

#import <AVFoundation/AVFoundation.h>

@protocol THMovieWriterDelegate <NSObject>
- (void)didWriteMovieAtURL:(NSURL *)outputURL;
@end

@interface THMovieWriter : NSObject

/**
 *  实例化,
 *  videoSettings,audioSettings两个字典用来描述基础AVAssetWriter的配置参数
 *  dispatchQueue 调度队列
 */
- (id)initWithVideoSettings:(NSDictionary *)videoSettings
              audioSettings:(NSDictionary *)audioSettings
              dispatchQueue:(dispatch_queue_t)dispatchQueue;

/**
 *  写入进程开始
 */
- (void)startWriting;

/**
 *  写入进程停止
 */
- (void)stopWriting;

/**
 *  工作状态监听
 */
@property (nonatomic) BOOL isWriting;

/**
 *  定义委托协议,监听写入磁盘时间
 */
@property (weak, nonatomic) id<THMovieWriterDelegate> delegate;

/**
 *  捕捉到新的样本,调用这个方法
 */
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer;

.m文件

#import "THMovieWriter.h"
#import <AVFoundation/AVFoundation.h>
#import "THContextManager.h"
#import "THFunctions.h"
#import "THPhotoFilters.h"
#import "THNotifications.h"

static NSString *const THVideoFilename = @"movie.mov";

@interface THMovieWriter ()

@property (strong, nonatomic) AVAssetWriter *assetWriter;
@property (strong, nonatomic) AVAssetWriterInput *assetWriterVideoInput;
@property (strong, nonatomic) AVAssetWriterInput *assetWriterAudioInput;
@property (strong, nonatomic)
    AVAssetWriterInputPixelBufferAdaptor *assetWriterInputPixelBufferAdaptor;

@property (strong, nonatomic) dispatch_queue_t dispatchQueue;

@property (weak, nonatomic) CIContext *ciContext;
@property (nonatomic) CGColorSpaceRef colorSpace;
@property (strong, nonatomic) CIFilter *activeFilter;

@property (strong, nonatomic) NSDictionary *videoSettings;
@property (strong, nonatomic) NSDictionary *audioSettings;

@property (nonatomic) BOOL firstSample;

@end

@implementation THMovieWriter

- (id)initWithVideoSettings:(NSDictionary *)videoSettings
              audioSettings:(NSDictionary *)audioSettings
              dispatchQueue:(dispatch_queue_t)dispatchQueue {

    self = [super init];
    if (self) {

        // Listing 8.13
        
        _videoSettings = videoSettings;
        _audioSettings = audioSettings;
        _dispatchQueue = dispatchQueue;
        
        //得到Core Image上下文,这个对象受OpenGL ES的支持,并用于筛选传进来的视频样本
        //最后得到一个VCPixelBuffer
        _ciContext = [THContextManager sharedInstance].ciContext;
        _colorSpace = CGColorSpaceCreateDeviceRGB();
        
        _activeFilter = [THPhotoFilters defaultFilter];
        _firstSample = YES;
        
        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
        //切换滤镜通知监听器
        [nc addObserver:self selector:@selector(filterChanged:) name:THFilterSelectionChangedNotification object:nil];

    }
    return self;
}

- (void)dealloc {

    // Listing 8.13
    CGColorSpaceRelease(_colorSpace);
    [[NSNotificationCenter defaultCenter] removeObserver:self];

}

- (void)filterChanged:(NSNotification *)notification {

    // Listing 8.13
    self.activeFilter = [notification.object copy];

}

- (void)startWriting {

    // Listing 8.14
    //开始录像,避免卡顿,异步调度到dispatchQueue队列,设置AVAssetWriter对象
    dispatch_async(self.dispatchQueue, ^{
        NSError *error = nil;
        NSString *fileType = AVFileTypeQuickTimeMovie;
        
        //创建新的AVAssetWriter实例
        self.assetWriter = [AVAssetWriter assetWriterWithURL:[self outputURL]
                                                    fileType:fileType
                                                       error:&error];
        if (!self.assetWriter || error) {
            NSLog(@"Could not create AVAssetWriter: %@",error);
            return ;
        }
        
        //创建一个新的AVAssetWriterInput,附加从AVCaptureVideoDataOutput中得到的样本
        self.assetWriterVideoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:self.videoSettings];
        //设置YES指明这个输入针对实时性进行优化
        self.assetWriterVideoInput.expectsMediaDataInRealTime = YES;
        
        //判断用户界面方向,为输入设置一个合适的转换。
        //写入会话期间,方向会按照这一设定保持不变。
        UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
        self.assetWriterVideoInput.transform = THTransformForDeviceOrientation(orientation);
        
        NSDictionary *attributes =
        @{
            (id)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA),
            (id)kCVPixelBufferWidthKey:self.videoSettings[AVVideoWidthKey],
            (id)kCVPixelBufferHeightKey:self.videoSettings[AVVideoHeightKey],
            (id)kCVPixelFormatOpenGLESCompatibility:(id)kCFBooleanTrue,
        
        };
        //创建AVAssetWriterInputPixelBufferAdaptor
        //提供了一个优化的CVPixelBufferPool,使用它可以创建CVPixelBuffer对象来渲染筛选视频帧。
        self.assetWriterInputPixelBufferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc] initWithAssetWriterInput:self.assetWriterVideoInput sourcePixelBufferAttributes:attributes];
        
        if ([self.assetWriter canAddInput:self.assetWriterVideoInput]) {
            [self.assetWriter addInput:self.assetWriterVideoInput];
        } else {
            NSLog(@"Unable to add video input");
            return ;
        }
        
        //创建AVAssetWriterInput附加AVCaptureAudioDataOutput样本
        self.assetWriterAudioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:self.audioSettings];
        self.assetWriterAudioInput.expectsMediaDataInRealTime = YES;
        
        if ([self.assetWriter canAddInput:self.assetWriterAudioInput]) {
            [self.assetWriter addInput:self.assetWriterAudioInput];
        } else {
            NSLog(@"Unable to add audio input");
            return;
        }
        
        self.isWriting = YES;
        self.firstSample = YES;
    });
    

}

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer {

    // Listing 8.15
    
    if (!self.isWriting) {
        return ;
    }
    
    //这个方法可以处理音频和视频两种样本,所以需要确定样本的媒体类型才能附加到正确的写入器输入。
    //查看buffer的CMFormatDescription
    CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
    //使用CMFormatDescriptionGetMediaType判断媒体类型
    CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDesc);
    
    if (mediaType == kCMMediaType_Video) {
        CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
        //如果开始捕捉后,正在处理的是第一个视频样本
        //调用资源写入器的startWriting启动一个新的写入会话
        //startSessionAtSourceTime: 将样本呈现时间作为源时间传递到方法中。
        if (self.firstSample) {
            if ([self.assetWriter startWriting]) {
                [self.assetWriter startSessionAtSourceTime:timestamp];
            } else {
                NSLog(@"failed to start writing");
            }
            self.firstSample = NO;
        }
        
        //从像素buffer适配器池中创建一个空的CVPixelBuffer
        //使用该像素buffer渲染筛选好的视频帧的输出
        CVPixelBufferRef outputRenderBuffer = NULL;
        CVPixelBufferPoolRef pixelBufferPool = self.assetWriterInputPixelBufferAdaptor.pixelBufferPool;
        
        OSStatus err = CVPixelBufferPoolCreatePixelBuffer(NULL, pixelBufferPool, &outputRenderBuffer);
        if (err) {
            NSLog(@"Unable to obtain a pixel buffer from thr pool.");
            return ;
        }
        
        //获取当前视频样本的CVPixelBuffer
        CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
        
        //根据像素buffer窗机啊一个新的CIImage并将他设置为筛选器的kCIInputImageKey值。
        CIImage *sourceImage  =[CIImage imageWithCVPixelBuffer:imageBuffer options:nil];
        
        [self.activeFilter setValue:sourceImage forKey:kCIInputImageKey];
        //通过筛选器得到输出图片,会返回一个封装了CIFilter操作的CIImage对象
        CIImage *filterImage = self.activeFilter.outputImage;
        if (!filterImage) {
            filterImage = sourceImage;
        }
        //将筛选好的CIImage的输出渲染到outputRenderBuffer
        [self.ciContext render:filterImage toCVPixelBuffer:outputRenderBuffer bounds:filterImage.extent colorSpace:self.colorSpace];
        
        if (self.assetWriterVideoInput.readyForMoreMediaData) {
            //如果输入的readyForMoreMediaData为YES
            //将像素buffer连同当前样本的时间附加到AVAssetWriterPixelBUfferAdaptor。
            if (![self.assetWriterInputPixelBufferAdaptor appendPixelBuffer:outputRenderBuffer withPresentationTime:timestamp]) {
                NSLog(@"Error Appending pixel buffer.");
            }
        }
        //完成对当前视频样本的处理,释放像素buffer
        CVPixelBufferRelease(outputRenderBuffer);
    } else if (!self.firstSample && mediaType == kCMMediaType_Audio) {
        //如果第一个样本处理完成并且当前的CMSampleBuffer是一个音频样本。
        if (self.assetWriterAudioInput.isReadyForMoreMediaData) {
            if (![self.assetWriterAudioInput appendSampleBuffer:sampleBuffer]) {
                NSLog(@"Error appending audio sample buffer");
            }
        }
    }
    

}

- (void)stopWriting {

    // Listing 8.16
    
    //设置为NO,processSampleBuffer:mediaType:就不会再处理更多的样本
    self.isWriting = NO;
    dispatch_async(self.dispatchQueue, ^{
        //终止写入会话并关闭磁盘上的文件
        [self.assetWriter finishWritingWithCompletionHandler:^{
            
            //判断资源写入器状态
            if (self.assetWriter.status == AVAssetWriterStatusCompleted) {
                //回调到主线程,调用委托的方法。
                dispatch_async(dispatch_get_main_queue(), ^{
                    NSURL *fileUrl = [self.assetWriter outputURL];
                    [self.delegate didWriteMovieAtURL:fileUrl];
                    //回调 保存到相册
                });
            } else {
                NSLog(@"Failed to write movie: %@",self.assetWriter.error);
            }
            
        }];
    });
    
}

// 定义outPutUrl配置AVAssetWriter实例。
- (NSURL *)outputURL {
    NSString *filePath =
        [NSTemporaryDirectory() stringByAppendingPathComponent:THVideoFilename];
    NSURL *url = [NSURL fileURLWithPath:filePath];
    if ([[NSFileManager defaultManager] fileExistsAtPath:url.path]) {
        [[NSFileManager defaultManager] removeItemAtURL:url error:nil];
    }
    return url;
}

@end

通过AVAssetWriter和AVAssetReader实现视频文件的读去和写入,同时可以再录制过程中对视频进行更多可扩展性的处理。

书中的示例中实现,滤镜视频的录制处理,使用到CoreImage对图片处理,后面也要学习这方面的内容。

这一节只是熟悉AVAssetWriter和AVAssetReader的基本用法,有所了解,它们还有更多更深入的功能,后期需要更多的时间去学习。

推荐阅读更多精彩内容