AVFoundation开发秘籍笔记:第8章 读取和写入媒体

8.1 综述

AV Foundation定义了一组功能可以用于创建媒体应用程序时遇到的大部分用例场景。在使用框架时,开发者首选的方法还是使用专门的高级功能,不过随着我们开发更复杂的媒体应用程序,就可能遇到一些用例,它们需要的功能并不受AV Foundation框架的内置支持。这意味着我们不够幸运并需要到其他地方寻找解决方案吗?不,这时需要使用框架的AVAssetReader和AVAssetWriter类提供的低级功能(如图8-1所示)。这些类可以让开发者直接处理媒体样本,打开了机会之窗。


8.1.1 AVAssetReader

AVAssetReader用于从AVAsset实例中读取媒体样本。通常会配置一个或多个AVAssetReaderOutput实例,并通过copyNextSampleBuffer方法可以访问音频样本和视频帧。AVAssetReaderOutput是一个抽象类,不过框架定义了3个具体实例来从指定的AVAssetTrack中读取解码的媒体样本,从多音频轨道中读取混合输出,或者从多视频轨道中读取组合输出。一个资源读取器的内部通道都是以多线程的方式不断提取下一个可用样本的,这样可以在系统请求资源时最小化时延。尽管提供了低时延的检索操作,还是不倾向于实时操作,比如播放。

注意:
AVAssetReader只针对带有一个资源的媒体样本。如果需要同时从多个基于文件的资源中读取样本,可将它们组合到一个AVAsset子类AVComposition 中,下一章我们会讲到相关内容。

8.1.2 AVAssetWriter

AVAssetWriter是AVAssetReader对应的兄弟类,它用于对媒体资源进行编码并将其写入到容器文件中,比如一个MPEG-4文件或一个QuickTime文件。它由一个或多个AVAssetWriterInput对象配置,用于附加将包含要写入容器的媒体样本的CMSampleBuffer对象。AVAssetWriterInput被配置为可以处理指定的媒体类型,比如音频或视频,并且附加在其后的样本会在最终输出时生成一个独立的AVAssetTrack。当使用一个配置 了处理视频样本的AVAssetWriterInput时,开发者会经常用到一个专门的适配器对象AVAssetWriterInputPixelBufferAdaptor。这个类在附加被包装为CVPixelBuffer对象的视频样本时提供最优性能。输入信息也可以通过使用AVAssetWriterInputGroup组成互斥的参数。这就让开发者能够创建特定资源,其中包含在播放时使用AVMediaSelectionGroup和AVMediaSelectionOption类选择的指定语言媒体轨道,第4章介绍过这些类。

AVAssetWriter可以自动支持交叉媒体样本。将样本写入磁盘的一种方式是按照顺序写入,如图8-2所示。需要将所有的媒体样本都捕捉好,不过这会导致数据的低效率排列,因为本应该整体呈现的样本数据可能会彼此分开。这就使得存储设备更难有效地读取数据,并在播放和寻找资源时产生负面效果和性能问题。


一种更好安排这些样本的方法是使用交错模式,如图8-3所示。为保持一个合适的交错模式,AVAssetWriterInput提供一个readyForMoreMediaData属性来指示在保持所需的交错情况下输入信息是否还可以附加更多数据。只有在这个属性值为YES时才可以将一个 新的样本添加 到写入输入信息中。


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

●实时:当处理实时资源时,比如从AVCaptureVideoDataOutput写入捕捉的样本时,AVAssetWriterInput应该令expectsMediaDataInRealTime属性为YES来确保readyForMoreMediaData值被正确计算。从实时资源写入数据优化了写入器,这样一来,与维持理想交错效果相比,快速写入样本具有更高的优先级。这一优化效果不错,视频和音频样本以大致相同的速率捕捉,传入数据自然交错。

●离线:当从离线资源中读取媒体资源时,比如从AVAssetReader读取样本buffer, 在附加样本前仍然需要观察写入器输入的readyForMoreMediaData属性的状态,不过可以使用requestMediaDataWhenReadyOnQueue:usingBlock:方法控制数据的提供。 传到这个方法中的代码块会随写入器输入准备附加更多的样本而不断被调用,添加样本时开发者需要检索数据并从资源中找到下一个样本进行添加。

8.1.3 读写示例

下面通过一个基础示例学习在一个离线场景下如何使用AVAssetReader和AVAssetWriter。在示例中,我们用AVAssetReader从 资源的视频轨道读取样本,并使用AVAssetWriter将它们写入到一个新的QuickTime电影文件中。虽然这是一个虚构的示例,不过它展示了在同时使用这些类时所涉及的一些基本步骤。下面首先来设置和配置AVAssetReader。

AVAsset *asset = // Asynchronously loaded video asset
AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeVideo] firstobject];
self.assetReader = [[AVAssetReader alloc] initWithAsset:asset error:nil];
NSDictionary *readerOutputSettings = @{
    (id) kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA)
};
AVAssetReaderTrackOutput *trackOutput = [[AVAsse tReaderTrackOutput alloc] initwithTrack:track
                                                                          outputSettings: readerOutputSettings];
[self.assetReader addOutput:trackOutput];

[self.assetReader startReading];

首先创建一个新的AVAssetReader,传递读取的AVAsset实例。创建一个AVAssetReaderTrackOutput从资源的视频轨道中读取样本,将视频帧解压缩为BGRA格式。添加输出到读取器,并调用startReading方法开始读取过程。

接下来创建并配置AVAssetWriter。

NSURL *outputURL = // Destination output URL
self.assetWriter = [[AVAssetWriter alloc] initwithURL:outputURL
                                             fileType:AVFileTypeQuickTimeMovie
                                                 error:nil];
NSDictionary *writerOutputSettings = @{
        AVVideoCodecKey:AVVideoCodecH264,
        AVVideoWidthKey:@1280,
        AVVideoHeightKey:@720,
        AVVideoCompressionPropertiesKey:@{
                AVVideoMaxKeyF rameIntervalKey:@1
                AVVideoAverageBitRateKey:@10500000,
                AVVideoProfileLevelKey:AVVideoProfileLeve1H264Main31,
        }
};
AVAssetWriterInput *writerInput = [[AVAssetWriterInput alloc] initwithMediaType:AVMediaTypeVideo
                                                                 outputSettings:writerOutputSettings];
[self.assetWriter addInput:writerInput];
[self.assetWriter startWriting];

示例中创建了一个新的AVAssetWriter对象,并传递了一个新文件写入目的地的输出URL和所希望的文件类型。创建了一个新的AVAssetWriterInput,它带有相应的媒体类型和输出设置, 以便创建一个720p H.264格式的视频。将输入添加到写入器并调用startWriting方法。

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

在完成AVAssetReader和AVAssetWriter对象的设置后,是时候创建一个新的写入会话来从资源中读取样本并将它们写入到新位置。示例中使用的是拉模式(pull model),即当写入器输入准备附加更多的样本时从资源中拉取样本。这是当我们从一个非实时资源中写入样本时所使用的模式。

// Serial Queue
dispatch_queue_t dispatchQueue = dispatch_queue_create("com.tapharmonic.WriterQueue", NULL);
[self.assetWriter startSessionAtSourceTime:kCMTimeZero];
[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 ma rkAsFinished];
            complete = YES;
        }
    }
    if (complete) {
        [self.assetWriter fini shWritingWithCompletionHandler:^{
            AVAssetWriterStatus status = self. assetWriter .status;
            if (status == AVAssetWriterStatusCompleted) {
                // Handle success case
            } else {
                // Handle failure case
            }
        }];
    }
}];

示例中首先使用startSessionAtSourceTime:方法创建了一个新的写入会话,并传递kCMTimeZero参数作为资源样本的开始时间。传给requestMediaDataWhenReadyOnQueue:usingBlock:方法的代码块在写入器输入准备好添加更多样本时会不断被调用。在每次调用期间,输入准备添加更多数据时,再从轨道的输出中复制可用的样本,并将其附加到输入中。当所有样本都从轨道输出中复制后,需要标记AVAssetWriterInput已结束并指明添加操作已经完成。最后,调用finishWritingWithCompletionHandler:关闭写入会话。资源写入器的status属性可在completion handler中查询, 来确定写入会话是否成功完成、失败或取消。

现在我们对AVAssetReader和AVAssetWriter这两个类有了更多了解。上述示例中已经给出了我们使用这些类处理离线资源时所使用的基础模式。下面继续学习并讨论一些更加具体、真实的情况,这样会更好地理解AVAssetReader和AVAssetWriter的价值。

8.2 创建音频波形视图

对于大部分音频和视频应用程序,一个常见的需求就是提供图像化显示的音频波形(waveform),如图8-4所示。这个功能可以让用户更简单地查看音频轨道,也就更容易操作滑动条或选定希望的位置进行编辑。本节将讨论如何使用AVFoundation来实现这个功能。


绘制一个波形的基本技巧包括以下三个步骤:

(1)读取:第一步是读取音频样本进行渲染。需要读取或可能解压音频数据,比如对于线性PCM资源。回顾第1章所讲到的,线性PCM是一种未压缩的音频样本格式。

(2)缩减:实际读取到的样本数量要远比我们在屏幕上渲染的多。考虑到单声道音频文件是以44.1kHz比率进行采样的,所得到的样本要比我们具有的像素多得多。缩减的过程必须作用于这个样本集。这一过程通常包括将样本总量分为小的样本块,并在每个样本块上找到最大的样本、所有样本的平均值或min/max值。

(3)渲染:在这一步我们将缩减后的样本呈现在屏幕上。通常会用到Quartz框架,不过也可以使用任何苹果公司支持的绘图框架。如何绘制这些数据的类型取决于开发者是如何缩减样本的。如果采用min/max对,则为它的每一对绘制一 条垂线。如果使用每个样本块的平均值或最大值,会发现使用Quartz Bezier路径绘制波形是最合适的。

在Chapter 8目录下可以找到名为THWaveformView_Starter的示例项目。这里我们创建了一个UIView的子类来展示如图8-5所示的波形效果。下面从第一步开始吧。


8.2.1 读取音频样本

要创建的第一个类称为THSampleDataProvider,这个类使用AVAssetReader实例从AVAsset中读取音频样本并返回一个NSData对象。代码清单8-1给出了这个类的接口。

代码清单8-1 THSampleDataProvider 接口

#import 

typedef void(^THSampleDataCompletionBlock)(NSData *);

@interface THSampleDataProvider : NSObject

+ (void)loadAudioSamplesFromAsset:(AVAsset *)asset
                  completionBlock:(THSampleDataCompletionBlock)completionBlock;

@end

这个类的接口最直接的关注点就是loadAudioSamplesFromAsset:completionBlock:类方法,调用这个方法可以读取音频样本。下 面看一下该类的具体实现,如代码清单8-2所示。

代码清单8-2 THSampleDataProvider 实现

#import "THSampleDataProvider.h"

@implementation THSampleDataProvider

+ (void)loadAudioSamplesFromAsset:(AVAsset *)asset
                  completionBlock:(THSampleDataCompletionBlock)completionBlock {
    
    NSString *tracks = @"tracks";
    
    [asset loadValuesAsynchronouslyForKeys:@[tracks] completionHandler:^{   // 1
        
        AVKeyValueStatus status = [asset statusOfValueForKey:tracks error:nil];
        
        NSData *sampleData = nil;
        
        if (status == AVKeyValueStatusLoaded) {                             // 2
            sampleData = [self readAudioSamplesFromAsset:asset];
        }
        
        dispatch_async(dispatch_get_main_queue(), ^{                        // 3
            completionBlock(sampleData);
        });
    }];
}

+ (NSData *)readAudioSamplesFromAsset:(AVAsset *)asset {
   
    // To be implemented
    
    return nil;
}

@end

(1)首先对资源所需的键执行标准的异步载入操作,这样在访问资源的tracks属性时就不会遇到阻碍。
(2)如果tracks键 成功载入,则调用私有方法readAudioSamplesFromAsset:从资源音频轨道中读取样本。
(3)由于载入操作可能发生在任意后台队列上,所以我们希望调度回主队列,并调用带有检索到的音频样本的completion block,如果没有读取成功则为nil。

现在我们讨论readAudioSamplesFromAsset:方法的实现。上一章讨论了使用CMSampleBuffer访问AVCaptureVideoDataOutput对象渲染的视频帧。当我们处理未压缩的视频数据时,可以使用CMSampleBufferGetlmageBuffer函数检索包含有帧的像素信息的基础CVImageBufferRef。从一个资源中读取音频样本时,会再次用到CMSampleBuffer,不过本例基础数据将会以Core Media类型CMBlockBuffer的形式提供。Block buffer用于在Core Media通道中传送任意字节的数据。根据开发者对音频样本的使用目的,有多种方法可以访问带有音频数据的block buffer。使用CMSampleBufferGetDataBuffer函 数得到一个到block buffer的不可保留(unretained)引用,这个方法适用于我们只需要访问数据而不进行后续处理的情况。相反,如果需要对音频数据进行处理,比如把它传递到Core Audio, 可以使用CMSample-BufferGetAudioBufferListWithRetainedBlockBuffer函数将数据作为AudioBufferList访问。这会返回Core Audio的AudioBufferList,并带有保留的CMBlockBuffer用于管理所包含的样本的生命周期。由于我们已经检索了样本并将它们复制到NSData中,就可以使用之前的方法检索音频数据了,如代码清单8-3所示。

代码清单8-3读取资源的音频样本

+ (NSData *)readAudioSamplesFromAsset:(AVAsset *)asset {
    
    NSError *error = nil;
    
    AVAssetReader *assetReader =                                            // 1
        [[AVAssetReader alloc] initWithAsset:asset error:&error];
    
    if (!assetReader) {
        NSLog(@"Error creating asset reader: %@", [error localizedDescription]);
        return nil;
    }
    
    AVAssetTrack *track =                                                   // 2
        [[asset tracksWithMediaType:AVMediaTypeAudio] firstObject];
    
    NSDictionary *outputSettings = @{                                       // 3
        AVFormatIDKey               : @(kAudioFormatLinearPCM),
        AVLinearPCMIsBigEndianKey   : @NO,
        AVLinearPCMIsFloatKey       : @NO,
        AVLinearPCMBitDepthKey      : @(16)
    };
    
    
    AVAssetReaderTrackOutput *trackOutput =                                 // 4
        [[AVAssetReaderTrackOutput alloc] initWithTrack:track
                                         outputSettings:outputSettings];
    
    [assetReader addOutput:trackOutput];
    
    [assetReader startReading];
    
    NSMutableData *sampleData = [NSMutableData data];
    
    while (assetReader.status == AVAssetReaderStatusReading) {
        
        CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer];// 5
        
        if (sampleBuffer) {
            
            CMBlockBufferRef blockBufferRef =                               // 6
                CMSampleBufferGetDataBuffer(sampleBuffer);
            
            size_t length = CMBlockBufferGetDataLength(blockBufferRef);
            SInt16 sampleBytes[length];
            CMBlockBufferCopyDataBytes(blockBufferRef,                      // 7
                                       0,
                                       length,
                                       sampleBytes);
            
            [sampleData appendBytes:sampleBytes length:length];
            
            CMSampleBufferInvalidate(sampleBuffer);                         // 8
            CFRelease(sampleBuffer);
        }
    }
    
    if (assetReader.status == AVAssetReaderStatusCompleted) {               // 9
        return sampleData;
    } else {
        NSLog(@"Failed to read audio samples from asset");
        return nil;
    }
}

(1)创建一个新的AVAssetReader实例,并赋给它一个资源来读取。如果在初始化对象时出错,就将这个错误信息打印到控制台并返回nil。
(2)获取资源中找到的第一个音频轨道。包含在示例项目中的音频文件只含有一个轨道,不过最好总是根据期望的媒体类型获取轨道。
(3)创建一个NSDictionary来保存 从资源轨道读取音频样本时使用的解压设置。样本需要以未压缩的格式被读取,所有我们指定kAudioFormatLinearPCM作为格式键。我们还希望确保以16位、lttle-endian字 节顺序的有符号整型方式读取。这些设置对于示例项目已经足够了,不过我们可以在AVAudioSettings.h文件中找到许多额外的键,它们可以对格式转换进行更详细的控制。
(4)创建一个新的AVAssetReaderTrackOutput实例,并将上一步我们创建的输出设置传递给它。将其作为AVAssetReader的输出并调用startReading来允许资源读取器开始预收取样本数据。.
(5)调用跟踪输出的copyNextSampleBuffer方法开始每个迭代,每次都返回一个包含音频样本的下一个可用样本buffer.
(6) CMSampleBuffer中 的音频样本被包含在一个CMBlockBuffer类型中。使用CMSampleBufferGetDataBuffer函数可以访问这个block buffer。 使用CMBlockBufferGetDataLength函数确定其长度并创建一个 16位的带符号整型数组来保存这些音频样本。
(7)使用CMBlockBufferCopyDataBytes函数生成一个 数组,数组中的元素为CMBlock-Buffer所包含的数据,并将数组的内容附加在NSData实例后。
(8)用CMSampleBufferInvalidate函 数来指定样本buffer已经处理和不可再继续使用。此外,需要释放CMSampleBuffer副本来释放内容。
(9)如果资源读取器的status值等于AVAssetReaderStatusCompleted,则数据被成功读取,返回包含音频样本数据的NSData即可。如果出现错误,则返回nil。

第一步就这样完成了,现在我们学会了如何从不同的音频格式中成功读取音频样本。下一步就是对数据进行缩减以满足可在屏幕上进行绘制的要求。

8.2.2 缩减音频样本

THSampleDataProvider将从一个给定 的视频资源中提取全部的样本集合。即使是非常小的音频文件,都可能有数十万个样本,远大于在屏幕上进行绘制所需的样本。我们需要定义一个筛选方法来得到最终在屏幕上呈现的值集合。要实现这一缩减操作,需要创建一个THSampleDataFilter对象。这个类的接口如代码清单8-4所示。

代码清单8-4 THSampleDataFilter 接口

@interface THSampleDataFilter : NSObject

- (id)initWithData:(NSData *)sampleData;

- (NSArray *)filteredSamplesForSize:(CGSize)size;

@end

用一个带有音频样本信息的NSData来初始化这个类的实例。提供filteredSamplesForSize:方法按照指定的尺寸约束来筛选数据集。

共分两步来处理这个数据。首先将样本分成“箱”,找到每个箱里面的最大样本。当所有箱都处理完成后,对这些与传递给filteredSamplesForSize:方法的尺寸约束有关的样本应用比例因子。下面看下这个类的具体实现,如代码清单8-5所示。

代码清单8-5 THSampleDataFilter 实现

#import "THSampleDataFilter.h"

@interface THSampleDataFilter ()
@property (nonatomic, strong) NSData *sampleData;
@end

@implementation THSampleDataFilter

- (id)initWithData:(NSData *)sampleData {
    self = [super init];
    if (self) {
        _sampleData = sampleData;
    }
    return self;
}

- (NSArray *)filteredSamplesForSize:(CGSize)size {

    NSMutableArray *filteredSamples = [[NSMutableArray alloc] init];        // 1
    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++) {                          // 2
            sampleBin[j] = CFSwapInt16LittleToHost(bytes[i + j]);
        }
        
        SInt16 value = [self maxValueInArray:sampleBin ofSize:binSize];     // 3
        [filteredSamples addObject:@(value)];

        if (value > maxSample) {                                            // 4
            maxSample = value;
        }
    }

    CGFloat scaleFactor = (size.height / 2) / maxSample;                    // 5

    for (NSUInteger i = 0; i < filteredSamples.count; i++) {               
        filteredSamples[i] = @([filteredSamples[i] integerValue] * scaleFactor);
    }

    return filteredSamples;
}

- (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;
}

@end

(1)首先创建一个NSMutableArray来保存筛选的音频样本数组。我们还确定了要处理的样本总数并计算与传入方法的尺寸约束相应的“箱”尺寸值。一个箱包含一个需要被筛选的样本子集。
(2)迭代全部音频样本集合,在每个迭代中构建一个需要处理的数据箱。当处理音频样 本时,要时刻记得字节的顺序,所以用到了CFSwapInt16LittleToHost函数来确保样本是按主机内置的字节顺序处理的。
(3)对于每个箱,调用maxValueInArray:方法找到最大样本。 这个方法会迭代箱中的所有样本并找到最大绝对值。结果值被添加到fiteredSamples数组。
(4)当我们遍历所有音频样本时,在筛选结果中计算最大值。这个值作为我们筛选样本所使用的比例因子。
(5)在返回筛选样本前,需要相对于传递给方法的尺寸约束来缩放值。这会得到一个浮点值的数组,这些值可以在屏幕上呈现。当这些值完成缩放后,就可以将数组返回给调用方法。

THSampleDataFilter类完成了,下 面我们准备讨论如何创建视图来渲染这些音频样本。

8.2.3 渲染音频样本

我们需要创建一个UIView子类来渲染结果。下面先来看这个类的接口,如代码清单8-6所示。

代码清单8-6 THWaveformView 接口

@class AVAsset;

@interface THWaveformView : UIView

@property (strong, nonatomic) AVAsset *asset;
@property (strong, nonatomic) UIColor *waveColor;

@end

视图提供了一个简单接口来设置AVAsset和波形绘制时所用的颜色。下面看下这个类的具体实现,如代码清单8-7所示。一些UIView样板代码被我们省略掉了。要查看全部的实现代码,可以去看项目源代码。

代码清单8-7实现 setAsset:方法

#import "THWaveformView.h"
#import "THSampleDataProvider.h"
#import "THSampleDataFilter.h"
#import 

static const CGFloat THWidthScaling = 0.95;
static const CGFloat THHeightScaling = 0.85;

@interface THWaveformView ()
@property (strong, nonatomic) THSampleDataFilter *filter;
@property (strong, nonatomic) UIActivityIndicatorView *loadingView;
@end

@implementation THWaveformView

...

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

- (void)drawRect:(CGRect)rect {
    // To be implemented
}

@end

(1)首先调用THSampleDataProvider类的loadAudioSamplesFromAssetcompletionBlock:方法开始载入音频样本。
(2)当样本被载入后,构建一个新的THSampleDataFilter实例, 为其传递包含音频样本的NSData。
(3)将视图的载入图标移除并调用setNeedsDisplay来对视图进行清理,此时会调用drawRect:方法。

下面看一下drawRect方法的实现,学习数据是如何在屏幕上绘制的,如代码清单8-8所示。

代码清单8-8实现 drawRect:方法

- (void)drawRect:(CGRect)rect {
    
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextScaleCTM(context, THWidthScaling, THHeightScaling);            // 1

    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);

    NSArray *filteredSamples =                                              // 2
        [self.filter filteredSamplesForSize:self.bounds.size];

    CGFloat midY = CGRectGetMidY(rect);

    CGMutablePathRef halfPath = CGPathCreateMutable();                      // 3
    CGPathMoveToPoint(halfPath, NULL, 0.0f, midY);

    for (NSUInteger i = 0; i < filteredSamples.count; i++) {
        float sample = [filteredSamples[i] floatValue];
        CGPathAddLineToPoint(halfPath, NULL, i, midY - sample);
    }

    CGPathAddLineToPoint(halfPath, NULL, filteredSamples.count, midY);

    CGMutablePathRef fullPath = CGPathCreateMutable();                      // 4
    CGPathAddPath(fullPath, NULL, halfPath);

    CGAffineTransform transform = CGAffineTransformIdentity;                // 5
    transform = CGAffineTransformTranslate(transform, 0, CGRectGetHeight(rect));
    transform = CGAffineTransformScale(transform, 1.0, -1.0);
    CGPathAddPath(fullPath, &transform, halfPath);

    CGContextAddPath(context, fullPath);                                    // 6
    CGContextSetFillColorWithColor(context, self.waveColor.CGColor);
    CGContextDrawPath(context, kCGPathFill);
    
    CGPathRelease(halfPath);                                                // 7
    CGPathRelease(fullPath);
}

(1)我们希望在视图内呈现这个波形,所以首先基于定义的高和宽常量来缩放图像上下文。还要计算x和y偏移量,转换上下文,在缩放上下文中适当调整偏移。
(2)从THSampleDataFilter实例 获取筛选的样本,并传递视图边界的尺寸。在实际代码中,开发者可能希望在drawRec:方法之外执行这一检索操作,这样在筛选样本时会有更好的优化效果。不过在本例中,上面的方法已经足以满足要求了。
(3)创建一个新的CGMutablePathRef,用来绘制波形Bezier路径的上半部。迭代筛选的样本,对每次迭代调用CGPathAddLineToPoint向路径中添加一个点。利用循环索引作为x坐标,样本值作为y坐标。
(4)创建第二个CGMutablePathRef,传递第4步构建的Bezier路径。使用这个Bezier路径绘制完整的波形。
(5)要绘制波形的下半部,需要对上半部路径应用translate和Iscale变化。这会使得上半部路径翻转到下面,填满整个波形。
(6)将完整的路径添加到图像上下文,根据指定的waveColor值设置填充颜色,并调用CGContextDrawPath(context, kCGPathFill)将填充好的路径绘制到图像上下文。
(7)每当创建Quartz对象后,开发者都有责任在使用完之后释放相应的内存,所以最后一.步就是在创建的路径对象上调用CGPathRelease。

应用程序的视图控制器用来绘制两个视图,如图8-5所示。可以打开应用程序的视图控制器并尝试其他颜色,或在Estoryboard中修改视图。 现在,如果在应用程序中需要用到波形器,就可以重用我们本章的类了。

8.3 捕捉录制的高级方法

上一章最后讨论了将AVCaptureVideoDataOutput捕捉的CVPixelBuffer对象作为OpenGLES的贴图来呈现。这是一个非常强大的功能,可以使我们开发出很多有趣的应用程序。不过使用AVCaptureVideoDataOutput的一一个问题在于会失去AVCaptureMovieFileOutput来记录输出 的便捷性。无论多么神奇的特效,如果应用程序无法记录输出内容,并把它分享到全世界,那都是严重的缺陷。在本节中,我们会学到如何使用AVAssetWriter创建一个与AVCapture-MovieFileOutput类似的可重用类从高级捕捉输出中记录输出。

看一下,上一章介绍的有关OpenGL ES的应用。我们可以重用CubeKamera应用程序,不过为了避免重复,我们创建一个新的相机应用程序,使用Core Image框架 来处理视频帧应用实时视频效果。

在Chapter 8目录中可以找到名为KameraWriter_Starter的示例项目。KameraWriter是一个带有实时效果的视频录制应用程序。用户可从屏幕最上的筛选器选择可用的效果。如图8-6所示。


从iOS 7开始,内置的相机应用程序在拍摄静态图片时就可以应用筛选器了。这些筛选器可以在Core Image框架中找到,并且在最新的iOS设备中它们还可用于将实时视频效果添加到应用程序中。Core Image框架的详细内容不在本书的讨论范围内,不过在开发本章这个功能时,也与它进行最小限度的交互。
第一步创建摄像头控制器。我们创建一个与 上一章一样的基础结构,这样可以减少在创建过程中样板的代码量。

先从THCameraController接口开始,如代码清单8-9所示。

代码清单8-9 THCameraController 接口

#import "THImageTarget.h"
#import "THBaseCameraController.h"

@interface THCameraController : THBaseCameraController

- (void)startRecording;
- (void)stopRecording;
@property (nonatomic, getter = isRecording) BOOL recording;

@property (weak, nonatomic) id  imageTarget;

@end

这个接口定义了开始录制、停止录制以及确定录制状态的方法。还提供了一个imageTarget属性,作为Core Image的CIImage对象的可视化输出。

下一步就是完成摄像头控制器的实现,首先配置捕捉会话输出,如代码清单8- 10所示。

代码清单8-10配置会话输出

#import "THCameraController.h"
#import 

@interface THCameraController () 

@property (strong, nonatomic) AVCaptureVideoDataOutput *videoDataOutput;
@property (strong, nonatomic) AVCaptureAudioDataOutput *audioDataOutput;

@end

@implementation THCameraController

- (BOOL)setupSessionOutputs:(NSError **)error {
    
    self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];         // 1
    
    NSDictionary *outputSettings =
        @{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)};
    
    self.videoDataOutput.videoSettings = outputSettings;
    self.videoDataOutput.alwaysDiscardsLateVideoFrames = NO;                // 2
    
    [self.videoDataOutput setSampleBufferDelegate:self
                                            queue:self.dispatchQueue];
    
    if ([self.captureSession canAddOutput:self.videoDataOutput]) {
        [self.captureSession addOutput:self.videoDataOutput];
    } else {
        return NO;
    }
    
    self.audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];         // 3
    
    [self.audioDataOutput setSampleBufferDelegate:self
                                            queue:self.dispatchQueue];
    
    if ([self.captureSession canAddOutput:self.audioDataOutput]) {
        [self.captureSession addOutput:self.audioDataOutput];
    } else {
        return NO;
    }
    
    return YES;
}

- (NSString *)sessionPreset {                                               // 4
    return AVCaptureSessionPresetMedium;
}

- (void)startRecording {
    // To be implemented
}

- (void)stopRecording {
    // To be implemented
}


#pragma mark - Delegate methods

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection {
    
    // To be implemented
    }
}

@end

(1)首先创建一个新的AVCaptureVideoDataOutput。用上一章的方法对它进行配置,设置它的输出格式为kCVPixelFormatType_32BGRA。当结合OpenGL ES和Corelmage时这一格式非常适合。
(2)设置alwaysDiscardsL ateVideoFrames为NO。由于我们要记录输出内容,所以通常我们希望捕捉全部的可用帧。设置这个属性为NO会给委托方法一些额外的时间来处理样本buffer,不过这会增加内存消耗。不过在处理每个样本buffer时都要做到尽可能高效,这样才能保障实时性能。
(3)还创建了一个新的AVCaptureAudioDataOutput实例。这个类是AVCaptureVideoDataOutput的兄弟类,用于从带有活动AVCaptureSession的音频设备中捕捉音频样本。
(4)设置会话预设值为AVCaptureSessionPresetMedium。开发这个应用程序时最好从一个简单的预设值开始,等我们熟悉了更多功能后,再一点点增加难度来满足更高级的需求。

捕捉会话的输出配置完毕后,就需要实现委托回调方法了,如代码清单8-11所示。

代码清单8-11捕捉输出委托

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

    if (captureOutput == self.videoDataOutput) {                            // 1
        
        CVPixelBufferRef imageBuffer =                                      // 2
            CMSampleBufferGetImageBuffer(sampleBuffer);
        
        CIImage *sourceImage =                                              // 3
            [CIImage imageWithCVPixelBuffer:imageBuffer options:nil];
        
        [self.imageTarget setImage:sourceImage];
    }
}

(1) captureOutput:didOutputSampleBuffer:fromConnection:方法是两类捕捉输出的委托回调。目前我们只处理视频样本,所以我们仅限于处理来自AVCaptureVideoDataOutput实例的回调。
(2)使用CMSampleBufferGetImageBuffer函 数从样本buffer中获取基础CVPixelBuffer。
(3)从CVPixelBuffer中创建一个新的CIlmage, 并将它传递给需要在屏幕上呈现的图片目标。

基本的捕捉功能已经完成了,现在可以在设备上编译和运行应用程序来实际体验筛选的功能。点击筛选器名称旁边的箭头来进行切换。现在我们已经成功开发了一个应用于视频帧的实时视频筛选器,感谢Core Image框架的帮助!

注意:
示例应用程序为AVCaptureVideoDataOutput和AVCaptureAudioDataOutput两个实例使用了一个调度队列。这对于我们的示例应用程序来说是足够的,不过如果希望对数据进行更复杂的处理,可能需要考虑为每一个使用单独的队列。苹果公司有- -个示例应用程序称为RosyWriter(在ADC上可用)用的就是这种方法。它还给出了一些供有效处理CMSampleBuffers的更高级性能选项。

KameraWriter应用程序看起来还不错,基本功能都也都实现了,不过还不能记录输出内容。要解决这一问题, 需要创建一个THMovieWriter对象,该对象和AVCaptureMoviceFileOutput的功能类似,不过它使用AVAssetWriter来执行视频编码和文件写入。下面看一下这个对象的接口定义,如代码清单8-12所示。

代码清单8-12 THMovieWriter 接口

#import 

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

@interface THMovieWriter : NSObject

- (id)initWithVideoSettings:(NSDictionary *)videoSettings                   // 1
              audioSettings:(NSDictionary *)audioSettings
              dispatchQueue:(dispatch_queue_t)dispatchQueue;

- (void)startWriting;
- (void)stopWriting;
@property (nonatomic) BOOL isWriting;

@property (weak, nonatomic) id delegate;             // 2

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer;                // 3

@end

(1) THMovieWriter实例带有两个字典,用来描述基础AVAssetWriter实例的配置参数和调度队列。定义了写入进程的开始和停止方法及监听其工作状态的方法。
(2)这个类定义了一个委托协议THMovieWriterDelegate来表示影片文件什么时候被写入 磁盘,之后委托会收到通知并采取相应的行动。
(3) THMovieWriter的一个关键 方法是processSampleBuffer,每当有新的样本被捕捉输出对象捕捉到时,都会调用这个方法。

对于AVAssetWriter,即使是简单的示例也会比较复杂。所以我们将这一开发过程分解成一些小段。首先我们处理有关生命周期的方法,如代码清单8-13所示。

代码清单8-13 THMovieWriter 生命周期方法

#import "THMovieWriter.h"
#import 
#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;                   // 1
@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) {
        _videoSettings = videoSettings;
        _audioSettings = audioSettings;
        _dispatchQueue = dispatchQueue;

        _ciContext = [THContextManager sharedInstance].ciContext;           // 2
        _colorSpace = CGColorSpaceCreateDeviceRGB();

        _activeFilter = [THPhotoFilters defaultFilter];
        _firstSample = YES;

        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];    // 3
        [nc addObserver:self
               selector:@selector(filterChanged:)
                   name:THFilterSelectionChangedNotification
                 object:nil];
    }
    return self;
}

- (void)dealloc {
    CGColorSpaceRelease(_colorSpace);
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)filterChanged:(NSNotification *)notification {
    self.activeFilter = [notification.object copy];
}

- (void)startWriting {
    // To be implemented
}

- (void) processSampleBuffer:(CMSampleBufferRef)sampleBuffer
                   mediaType:(CMMediaType)mediaType {
    // To be implemented
}

- (void)stopWriting {
    // To be implemented
}

- (NSURL *)outputURL {                                                         // 4
    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

(1)在类扩展中我们为AVAssetWriter创建了一些属性和相关的对 象。每当startWriting方法被调用时,就创建对象的图片并在写入会话的持续时间内保持对它们的强引用关系。
(2)从THContextManager对象得到分享的Core Image上下文。这个对象受OpenGL ES的支持并用于筛选传进来的视频样本,最后得到一一个CVPixelBuffer。
(3)注册THFilterSelectionChangedNotification通 知的监听器。当用户切换可用筛选器列表时就会从用户界面发送该通知。每当通知发送时,filterChanged:方法就 会被调用,并相应地 更新activeFilter属性。
(4)定义一个outputURL方法来配置AVAssetWriter实例。这个方法在临时目录中定义了一个NSURL,并将之前的同名文件删除。

有关生命周期的配置完成后,我们再来看一下startWriting方法的实现,如代码清单8-14所示。

代码清单8-14设置AVAssetWriter图片

- (void)startWriting {
    dispatch_async(self.dispatchQueue, ^{                                   // 1

        NSError *error = nil;

        NSString *fileType = AVFileTypeQuickTimeMovie;
        self.assetWriter =                                                  // 2
            [AVAssetWriter assetWriterWithURL:[self outputURL]
                                     fileType:fileType
                                        error:&error];
        if (!self.assetWriter || error) {
            NSString *formatString = @"Could not create AVAssetWriter: %@";
            NSLog(@"%@", [NSString stringWithFormat:formatString, error]);
            return;
        }

        self.assetWriterVideoInput =                                        // 3
            [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo
                                           outputSettings:self.videoSettings];

        self.assetWriterVideoInput.expectsMediaDataInRealTime = YES;

        UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
        self.assetWriterVideoInput.transform =                              // 4
            THTransformForDeviceOrientation(orientation);

        NSDictionary *attributes = @{                                       // 5
            (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
            (id)kCVPixelBufferWidthKey : self.videoSettings[AVVideoWidthKey],
            (id)kCVPixelBufferHeightKey : self.videoSettings[AVVideoHeightKey],
            (id)kCVPixelFormatOpenGLESCompatibility : (id)kCFBooleanTrue
        };

        self.assetWriterInputPixelBufferAdaptor =                           // 6
            [[AVAssetWriterInputPixelBufferAdaptor alloc]
                initWithAssetWriterInput:self.assetWriterVideoInput
             sourcePixelBufferAttributes:attributes];


        if ([self.assetWriter canAddInput:self.assetWriterVideoInput]) {    // 7
            [self.assetWriter addInput:self.assetWriterVideoInput];
        } else {
            NSLog(@"Unable to add video input.");
            return;
        }

        self.assetWriterAudioInput =                                        // 8
            [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio
                                           outputSettings:self.audioSettings];

        self.assetWriterAudioInput.expectsMediaDataInRealTime = YES;

        if ([self.assetWriter canAddInput:self.assetWriterAudioInput]) {    // 9
            [self.assetWriter addInput:self.assetWriterAudioInput];
        } else {
            NSLog(@"Unable to add audio input.");
        }

        self.isWriting = YES;                                              // 10
        self.firstSample = YES;
    });
}

(1)让用户点击Record按钮是为了避免卡顿,以异步方式调度到dispatchQueue队列,以便设置AVAssetWriter对象。
(2)创建一个新的AVAssetWriter实例,将写入目的文件的输出URL、文件类型常量和一个NSError传递给它。在创建对象时如果出现错误,则将错误信息输出到控制台并返回。
(3)创建一个新的AVAssetWriterInput,以附加从AVCaptureVideoDataOuput中得到的样本。传递给初始化方法一个 AVMediaTypeVideo媒体类型和创建THMovieWriter的视频设置。设置expectsMediaDataInRealTime属性为YES来指明这个输入应该针对实时性进行优化。
(4)应用程序的用户界面锁定为垂直方向,不过我们希望捕捉应该可以支持任何方向。判断用户界面的方向并使用THTransformForDeviceOrientation函数为输入设置一个 合适的转换。在写入会话期间,方向会按照这一设定保持不变。
(5) 定义属性的NSDictionary用于配置将在下一步中创建的AVAssetWriterInput-PixelIBufferAdaptor。要保证最大效率,字典中的值应该对应于在配置AVCaptureVideoData-Output时所使用的原像素格式。
(6)创建一个新的AVAssetWriterInputPixelBufferAdaptor,传递上一步中创建的属性给它。这个对象提供了一个优化的CVPixelBufferPool,使用它可以创建CVPixelBuffer对象来渲染筛选视频帧。
(7)将视频输入添加到资源写入器,如果输入不能被添加,将错误信息输出到控制台并返回。
(8)创建AVAssetWriterInput,用于附加来自AVCaptureAudioDataOutput的样本。给初始化方法传递一个AVMediaTypeAudio媒体类型和创建THMovieWriter的音频设置。设置expectsMediaDatalnRealTime属性为YES来指明这个输入应该针对实时性进行优化。
(9)将音频输入添加到资源写入器。如果输入不能被添加,则将错误信息输出到控制台并返回。
(10)设置isWriting 和firstSample属性为YES,就可以开始附加样本了。

接下来,下面看一下procesSampleBuffer:方法的实现,这个方法里我们将附加从捕捉输出得到的CMSampleBuffer对象,如代码清单8-15所示。

代码清单8-15 处理样本 buffer

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    
    if (!self.isWriting) {
        return;
    }
    
    CMFormatDescriptionRef formatDesc =                                     // 1
        CMSampleBufferGetFormatDescription(sampleBuffer);
    
    CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDesc);

    if (mediaType == kCMMediaType_Video) {

        CMTime timestamp =
            CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
        
        if (self.firstSample) {                                             // 2
            if ([self.assetWriter startWriting]) {
                [self.assetWriter startSessionAtSourceTime:timestamp];
            } else {
                NSLog(@"Failed to start writing.");
            }
            self.firstSample = NO;
        }
        
        CVPixelBufferRef outputRenderBuffer = NULL;
        
        CVPixelBufferPoolRef pixelBufferPool =
            self.assetWriterInputPixelBufferAdaptor.pixelBufferPool;
        
        OSStatus err = CVPixelBufferPoolCreatePixelBuffer(NULL,             // 3
                                                          pixelBufferPool,
                                                          &outputRenderBuffer);
        if (err) {
            NSLog(@"Unable to obtain a pixel buffer from the pool.");
            return;
        }

        CVPixelBufferRef imageBuffer =                                      // 4
            CMSampleBufferGetImageBuffer(sampleBuffer);

        CIImage *sourceImage = [CIImage imageWithCVPixelBuffer:imageBuffer
                                                       options:nil];

        [self.activeFilter setValue:sourceImage forKey:kCIInputImageKey];

        CIImage *filteredImage = self.activeFilter.outputImage;

        if (!filteredImage) {
            filteredImage = sourceImage;
        }

        [self.ciContext render:filteredImage                                // 5
               toCVPixelBuffer:outputRenderBuffer
                        bounds:filteredImage.extent
                    colorSpace:self.colorSpace];


        if (self.assetWriterVideoInput.readyForMoreMediaData) {             // 6
            if (![self.assetWriterInputPixelBufferAdaptor
                            appendPixelBuffer:outputRenderBuffer
                         withPresentationTime:timestamp]) {
                NSLog(@"Error appending pixel buffer.");
            }
        }
        
        CVPixelBufferRelease(outputRenderBuffer);
        
    }
    else if (!self.firstSample && mediaType == kCMMediaType_Audio) {        // 7
        if (self.assetWriterAudioInput.isReadyForMoreMediaData) {
            if (![self.assetWriterAudioInput appendSampleBuffer:sampleBuffer]) {
                NSLog(@"Error appending audio sample buffer.");
            }
        }
    }
}

(1)这个方法可以处理音频和视频两类样本,所以我们需要确定样本的媒体类型才能附加到正确的写入器输入。查看样本buffer的CMFormatDescription并使用CMFormatDescriptionGetMediaType方法来判断它的媒体类型。
(2)如果当用户点击Record按钮后正在处理的是第一个 视频样本,则调用资源写入器的startWriting和startSessionAtSourceTime:方法启动一个新的写入会话,将样本的呈现时间作为源时间传递到方法中。
(3)从像素buffer适配器池中创建一个空的CVPixelBuffer, 使用该像素buffer渲染筛选好的视频帧的输出。
(4)使用CMSampleBufferGetImageBuffer函 数获取当前视频样本的CVPixelBuffer。根据像素buffer创建-一个新的CIlmage并将它设置为活动筛选器的kCIInputImageKey值。通过筛选器得到输出图片,会返回一个封装了CIFilter操作的CIlmage对象。如果因为某种原因filteredIlmage为nil,则设置CIImage的引用为原始的sourceImage。
(5)将筛选好的CIlmage的输出渲染到第3步创建的CVPixelBuffer中。
(6)如果视频输入的readyForMoreMediaData属性为YES,则将像素buffer连同当前样本的呈现时间都附加到AVAssetWriterPixelBufferAdaptor。现在就完成了对当前视频样本的处理,所以此时应该调用CVPixelBufferRelease函数释放像素buffer。
(7)如果第一个样本处理完成并且当前的CMSampleBuffer是一个音频样本,则询问音频AVAssetWriterInput是否准备接收更多的数据。如果可以,则将它添加到输入。

processSampleBuffer:方法就完成了,最后一个需要实现的功能是stopWriting方法,如代码清单8- 16所示。

代码清单8-16完成写入会话

- (void)stopWriting {

    self.isWriting = NO;                                                    // 1

    dispatch_async(self.dispatchQueue, ^{

        [self.assetWriter finishWritingWithCompletionHandler:^{             // 2

            if (self.assetWriter.status == AVAssetWriterStatusCompleted) {
                dispatch_async(dispatch_get_main_queue(), ^{                // 3
                    NSURL *fileURL = [self.assetWriter outputURL];
                    [self.delegate didWriteMovieAtURL:fileURL];
                });
            } else {
                NSLog(@"Failed to write movie: %@", self.assetWriter.error);
            }
        }];
    });
}

(1)设置isWriting的标志为NO,这样processSampleBuffer:mediaType:方法就不会再处理更多的样本。
(2)调用finishWritingWithCompletionHandler:方法来终 止写入会话并关闭磁盘上的文件。
(3)判断资源写入器的状态。如果status等 于AVAssetWriterStatusCompleted,则表示文件成功写入,调度回到主线程调用委托的didWriteMovieAtURL:方法。如果status等 于其他值,则将资源写入器的错误信息输出到控制台。

THMovieWriter类的实现就全部完成了,最后一步就是将它整合到THCameraController中,如代码清单8-17所示。

代码清单8-17应用THMovieWriter

#import "THCameraController.h"
#import 
#import "THMovieWriter.h"
#import 

@interface THCameraController () 

@property (strong, nonatomic) AVCaptureVideoDataOutput *videoDataOutput;
@property (strong, nonatomic) AVCaptureAudioDataOutput *audioDataOutput;

@property (strong, nonatomic) THMovieWriter *movieWriter;                       // 1

@end

@implementation THCameraController

- (BOOL)setupSessionOutputs:(NSError **)error {
    
    // AVCaptureVideoDataOutput and AVCaptureAudioDataOutput set up and
    // configuration previously covered in Listing 8.x
    
    NSString *fileType = AVFileTypeQuickTimeMovie;
    
    NSDictionary *videoSettings =                                               // 2
        [self.videoDataOutput
         recommendedVideoSettingsForAssetWriterWithOutputFileType:fileType];
    
    NSDictionary *audioSettings = [self.audioDataOutput
                                   recommendedAudioSettingsForAssetWriterWithOutputFileType:fileType];
    
    self.movieWriter =                                                          // 3
        [[THMovieWriter alloc] initWithVideoSettings:videoSettings
                                       audioSettings:audioSettings
                                       dispatchQueue:self.dispatchQueue];
    self.movieWriter.delegate = self;
    
    return YES;
}

- (NSString *)sessionPreset {
    return AVCaptureSessionPreset1280x720;
}

- (void)startRecording {                                                        // 4
    [self.movieWriter startWriting];
    self.recording = YES;
}

- (void)stopRecording {
    [self.movieWriter stopWriting];
    self.recording = NO;
}


#pragma mark - Delegate methods

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection {
    
    [self.movieWriter processSampleBuffer:sampleBuffer];                        // 5

    if (captureOutput == self.videoDataOutput) {
        
        CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
        
        CIImage *sourceImage = [CIImage imageWithCVPixelBuffer:imageBuffer options:nil];
        
        [self.imageTarget setImage:sourceImage];
    }
}

- (void)didWriteMovieAtURL:(NSURL *)outputURL {                                 // 6
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
    
    if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:outputURL]) {
        
        ALAssetsLibraryWriteVideoCompletionBlock completionBlock;
        
        completionBlock = ^(NSURL *assetURL, NSError *error){
            if (error) {
                [self.delegate assetLibraryWriteFailedWithError:error];
            }
        };
        
        [library writeVideoAtPathToSavedPhotosAlbum:outputURL
                                    completionBlock:completionBlock];
    }
}

@end

(1)创建一个新属性,保存关于THMovieWriter对象的强引用。同时需要类遵循THMovieWriterDelegate协议。
(2) iOs 7版本中引入了一些便捷方法让开发者可以简单地创建带有推荐音频和视频设置的字典对象,这些设置都针对配置AVAssetWriter需要的文件类型。调用这些方法并创建传递到THMovieWriter中的字典。
(3)创建一个新的THMovieWriter实例,传递配置字典和控制器的dispatchQueue的引用,该引用是在它的超类(THBaseCameraController)中定义的。将该控制器作为影片写入器的委托。
(4)实现startRecording和stopRecording方法, 并在THMovieWriter.上调用相应的方法。对每个方法都按照需要更新recording状态。
(5)调用影片写入器的processSampleBuffer:方法,将当前CMSampleBuffer和媒体类型传递给方法。
(6)实现委托协议的didWriteMovieAtURL:方法。 使用前面介绍过的ALAssetsLibrary框架将最新创建的影片写入到用户的Photos library。

现在可以运行应用程序并录制视频了。打开应用程序并点击Record按钮,当录制进行时,可以点击可用的筛选器。再次点击Record按钮可以停止录制。可以切换到iOS相机应用程序体验这个视频带来的喜悦吧。

8.4 小结

本章主要学习了AVAssetReader和AVAssetWriter所提供的强大功能。这些类都是AVFoundation在处理媒体资源对象时的底层方法,在许多高级用例中都起到了至关重要的作用。虽然在CMSampleBuffer对象一级处理媒体比较复杂, 但是它可以提供仅使用框架顶层功能无法实现的一些高级功能。精通AVAssetReader和AVAssetWriter的用法需要一定的时间,不过本章的主题以及示例项目可以为开发者创建自己的应用程序奠定一个 良好的基础。

你可能感兴趣的:(AVFoundation开发秘籍笔记:第8章 读取和写入媒体)