本文转自:AVAudioFoundation(5):音视频导出 | www.samirchen.comhtml
本文主要内容来自 AVFoundation Programming Guide。ios
要读写音视频数据资源 asset,咱们须要用到 AVFoundation 提供的文件导出 API。AVAssetExportSession
提供了比较简单的 API 来知足基本的导出需求,好比修改文件类型、剪辑资源长度。若是要知足更加深度的导出需求,咱们则须要用到 AVAssetReader
和 AVAssetWriter
。session
当咱们须要去操做 asset 的内容时,咱们能够用 AVAssetReader
,好比读取 asset 中的音频轨道来展现波形等等。当咱们想用一些音频采样或静态图片去生成 asset 时,咱们可使用 AVAssetWriter
。app
须要注意的是 AVAssetReader
不适用于作实时处理。AVAssetReader
无法用来处理 HLS 之类的实时数据流。可是 AVAssetWriter
是能够用来处理实时数据源的,好比 AVCaptureOutput
,当须要处理实时数据源时,须要设置 expectsMediaDataInRealTime
属性为 YES
。若是对非实时数据源设置该属性为 YES
,那么可能会形成你导出的文件有问题。异步
每个 AVAssetReader
一次只能与一个 asset 关联,可是这个 asset 能够包含多个轨道。因为这个缘由一般咱们须要为 AVAssetReader
指定一个 AVAssetReaderOutput
的具体子类来具体操做 asset 的读取,好比:async
AVAssetReaderTrackOutput
AVAssetReaderAudioMixOutput
AVAssetReaderVideoCompositionOutput
初始化 AVAssetReader
时须要传入相应读取的 asset。ide
NSError *outError; AVAsset *someAsset = <#AVAsset that you want to read#>; AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:someAsset error:&outError]; BOOL success = (assetReader != nil);
须要注意的是要检查建立的 asset reader 是否为空。初始化失败时,能够查看具体的 error 信息。性能
在完成建立 asset reader 后,建立至少一个 output 对象来接收读取的媒体数据。当建立 output 对象时,要记得设置 alwaysCopiesSampleData
属性为 NO
,这样你会得到性能上的提高。在本章中的示例代码中,这个属性都应该被设置为 NO
。ui
若是咱们想从媒体资源中读取一个或多个轨道,而且可能会转换数据的格式,那么可使用 AVAssetReaderTrackOutput
类,为每一个 AVAssetTrack
轨道使用一个 track output 对象。this
下面的示例展现了使用 asset reader 来把一个 audio track 压缩为线性的 PCM:
AVAsset *localAsset = assetReader.asset; // Get the audio track to read. AVAssetTrack *audioTrack = [[localAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0]; // Decompression settings for Linear PCM. NSDictionary *decompressionAudioSettings = @{AVFormatIDKey: [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM]}; // Create the output with the audio track and decompression settings. AVAssetReaderOutput *trackOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:audioTrack outputSettings:decompressionAudioSettings]; // Add the output to the reader if possible. if ([assetReader canAddOutput:trackOutput]) { [assetReader addOutput:trackOutput]; }
若是想从一个指定的 asset track 按它本来存储的格式读取媒体数据,设置 outputSettings
为 nil
便可。
当想要读取用 AVAudioMix
或 AVVideoComposition
混音或编辑过的媒体数据时,须要对应的使用 AVAssetReaderAudioMixOutput
和 AVAssetReaderVideoCompositionOutput
。通常,当咱们从一个 AVComposition
读取媒体资源时,咱们须要用到这些 output 类。
使用一个单独的 audio mix output 咱们就能够读取 AVAudioMix
混合过的 asset 中的多个音频轨道。为了指明这些音频轨道是如何混合的,咱们须要在 AVAssetReaderAudioMixOutput
对象初始化完成后将对应的 AVAudioMix
对象设置给它的 audioMix
属性。
下面的代码展现了如何基于一个 asset 来建立咱们的 audio mix output 对象去处理全部的音频轨道,而后压缩这些音频轨道为线性 PCM 数据,并为 output 对象设置 audioMix
属性。
AVAudioMix *audioMix = <#An AVAudioMix that specifies how the audio tracks from the AVAsset are mixed#>; // Assumes that assetReader was initialized with an AVComposition object. AVComposition *composition = (AVComposition *) assetReader.asset; // Get the audio tracks to read. NSArray *audioTracks = [composition tracksWithMediaType:AVMediaTypeAudio]; // Get the decompression settings for Linear PCM. NSDictionary *decompressionAudioSettings = @{AVFormatIDKey: [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM]}; // Create the audio mix output with the audio tracks and decompression setttings. AVAssetReaderOutput *audioMixOutput = [AVAssetReaderAudioMixOutput assetReaderAudioMixOutputWithAudioTracks:audioTracks audioSettings:decompressionAudioSettings]; // Associate the audio mix used to mix the audio tracks being read with the output. audioMixOutput.audioMix = audioMix; // Add the output to the reader if possible. if ([assetReader canAddOutput:audioMixOutput]) { [assetReader addOutput:audioMixOutput]; }
若是想要 asset reader 返回无压缩格式,那么就把 audioSettings
参数传为 nil
。对于使用 AVAssetReaderVideoCompositionOutput
时,也是一样的道理。
咱们使用 video composition output 的方式跟上面相似。下面的代码展现了,若是读取多个视频轨道的媒体数据并将他们压缩为 ARGB 格式。
AVVideoComposition *videoComposition = <#An AVVideoComposition that specifies how the video tracks from the AVAsset are composited#>; // Assumes assetReader was initialized with an AVComposition. AVComposition *composition = (AVComposition *) assetReader.asset; // Get the video tracks to read. NSArray *videoTracks = [composition tracksWithMediaType:AVMediaTypeVideo]; // Decompression settings for ARGB. NSDictionary *decompressionVideoSettings = @{(id) kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithUnsignedInt:kCVPixelFormatType_32ARGB], (id) kCVPixelBufferIOSurfacePropertiesKey: [NSDictionary dictionary]}; // Create the video composition output with the video tracks and decompression setttings. AVAssetReaderOutput *videoCompositionOutput = [AVAssetReaderVideoCompositionOutput assetReaderVideoCompositionOutputWithVideoTracks:videoTracks videoSettings:decompressionVideoSettings]; // Associate the video composition used to composite the video tracks being read with the output. videoCompositionOutput.videoComposition = videoComposition; // Add the output to the reader if possible. if ([assetReader canAddOutput:videoCompositionOutput]) { [assetReader addOutput:videoCompositionOutput]; }
To start reading after setting up all of the outputs you need, call the startReading method on your asset reader. Next, retrieve the media data individually from each output using the copyNextSampleBuffer method. To start up an asset reader with a single output and read all of its media samples, do the following:
在 output 对象建立完成后,接着就要开始读取数据了,这时候咱们须要调用 asset reader 的 startReading
接口。接着,使用 copyNextSampleBuffer
接口来从各个 output 来获取媒体数据。
下面的代码展现了 asset reader 如何用一个 output 对象从 asset 中读取全部的媒体数据:
// Start the asset reader up. [self.assetReader startReading]; BOOL done = NO; while (!done) { // Copy the next sample buffer from the reader output. CMSampleBufferRef sampleBuffer = [self.assetReaderOutput copyNextSampleBuffer]; if (sampleBuffer) { // Do something with sampleBuffer here. CFRelease(sampleBuffer); sampleBuffer = NULL; } else { // Find out why the asset reader output couldn't copy another sample buffer. if (self.assetReader.status == AVAssetReaderStatusFailed) { NSError *failureError = self.assetReader.error; // Handle the error here. } else { // The asset reader output has read all of its samples. done = YES; } } }
AVAssetWriter
类能够将媒体数据从多个源写入指定文件格式的单个文件。咱们也不须要将 asset writer 对象与特定 asset 相关联,但必须为要建立的每一个输出文件使用单独的 asset writer。因为 asset writer 能够从多个来源写出媒体数据,所以咱们必须为每一个要被写入到输出文件的轨道建立一个 AVAssetWriterInput
对象。每一个 AVAssetWriterInput
对象都指望以 CMSampleBufferRef
对象的形式接收数据,可是若是要将 CVPixelBufferRef
对象附加到 asset writer,可使用 AVAssetWriterInputPixelBufferAdaptor
类。
为了建立 asset writer,咱们须要指定输出文件的 URL 和所需的文件类型。如下代码展现了如何初始化 asset writer 来建立 QuickTime 格式的视频:
NSError *outError; NSURL *outputURL = <#NSURL object representing the URL where you want to save the video#>; AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:outputURL fileType:AVFileTypeQuickTimeMovie error:&outError]; BOOL success = (assetWriter != nil);
想要用 asset writer 来写媒体数据,必须至少设置一个 asset writer input。例如,若是数据源已经使用 CMSampleBufferRef
类来表示,那么使用 AVAssetWriterInput
类便可。
下面的代码展现了如何设置将音频媒体数据压缩为 128 kbps AAC 并将其链接到 asset writer 的 input:
// Configure the channel layout as stereo. AudioChannelLayout stereoChannelLayout = { .mChannelLayoutTag = kAudioChannelLayoutTag_Stereo, .mChannelBitmap = 0, .mNumberChannelDescriptions = 0 }; // Convert the channel layout object to an NSData object. NSData *channelLayoutAsData = [NSData dataWithBytes:&stereoChannelLayout length:offsetof(AudioChannelLayout, mChannelDescriptions)]; // Get the compression settings for 128 kbps AAC. NSDictionary *compressionAudioSettings = @{ AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatMPEG4AAC], AVEncoderBitRateKey : [NSNumber numberWithInteger:128000], AVSampleRateKey : [NSNumber numberWithInteger:44100], AVChannelLayoutKey : channelLayoutAsData, AVNumberOfChannelsKey : [NSNumber numberWithUnsignedInteger:2] }; // Create the asset writer input with the compression settings and specify the media type as audio. AVAssetWriterInput *assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:compressionAudioSettings]; // Add the input to the writer if possible. if ([assetWriter canAddInput:assetWriterInput]) { [assetWriter addInput:assetWriterInput]; }
须要注意的是,若是要以本来存储的格式来写入媒体数据,能够在 outputSettings
参数中传 nil
。只有 asset writer 使用 AVFileTypeQuickTimeMovie
的 fileType
进行初始化时,才能传 nil
。
您的 asset writer input 能够经过设置 metadata
和 transform
属性来包含一些元数据或为特定的轨道指定不一样的变换。对于数据源是视频轨道的 asset writer input,您能够经过执行如下操做来将视频的原始变换保存在输出文件中:
AVAsset *videoAsset = <#AVAsset with at least one video track#>; AVAssetTrack *videoAssetTrack = [[videoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; assetWriterInput.transform = videoAssetTrack.preferredTransform;
须要注意的是 metadata
和 transform
这两个属性须要在开始写以前设定才会生效。
将媒体数据写入输出文件时,有时咱们可能须要分配像素数据缓冲区。这时可使用 AVAssetWriterInputPixelBufferAdaptor
类。为了最大的效率,不要使用单独的池分配的像素缓冲区,而是使用像素缓冲适配器提供的像素缓冲池。如下代码建立一个在 RGB 域中工做的像素缓冲区对象,它将使用 CGImage
对象来建立其像素缓冲区:
NSDictionary *pixelBufferAttributes = @{ kCVPixelBufferCGImageCompatibilityKey: [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey: [NSNumber numberWithBool:YES], kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_32ARGB] }; AVAssetWriterInputPixelBufferAdaptor *inputPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:self.assetWriterInput sourcePixelBufferAttributes:pixelBufferAttributes];
须要注意的是,全部 AVAssetWriterInputPixelBufferAdaptor
对象必须链接到单个 asset writer input。Asset writer input 必须接受 AVMediaTypeVideo
类型的媒体数据。
配置 asset writer 所需的全部输入后,便可开始写媒体数据。与 asset reader 同样,经过调用 startWriting
方法启动写入过程。而后,须要经过调用 startSessionAtSourceTime:
方法启动写入会话。Asset writer 完成的全部写入都必须在这些会话之一内进行,每一个会话的时间范围不超出源中包含的媒体数据的时间范围。
下面的代码展现了当咱们的数据源是一个 asset reader,它提供从 AVAsset
对象读取的媒体数据,而且不但愿包含资产前半部分的媒体数据:
CMTime halfAssetDuration = CMTimeMultiplyByFloat64(self.asset.duration, 0.5); [self.assetWriter startSessionAtSourceTime:halfAssetDuration]; //Implementation continues.
一般,要结束写入会话,您必须调用 endSessionAtSourceTime:
方法。可是,若是写入会话直到文件的末尾,则能够经过调用 finishWriting
方法来结束。
下面代码展现了使用单个 input 启动 asset writer 并写入其全部媒体数据:
// Prepare the asset writer for writing. [self.assetWriter startWriting]; // Start a sample-writing session. [self.assetWriter startSessionAtSourceTime:kCMTimeZero]; // Specify the block to execute when the asset writer is ready for media data and the queue to call it on. [self.assetWriterInput requestMediaDataWhenReadyOnQueue:myInputSerialQueue usingBlock:^{ while ([self.assetWriterInput isReadyForMoreMediaData]) { // Get the next sample buffer. CMSampleBufferRef nextSampleBuffer = [self copyNextSampleBufferToWrite]; if (nextSampleBuffer) { // If it exists, append the next sample buffer to the output file. [self.assetWriterInput appendSampleBuffer:nextSampleBuffer]; CFRelease(nextSampleBuffer); nextSampleBuffer = nil; } else { // Assume that lack of a next sample buffer means the sample buffer source is out of samples and mark the input as finished. [self.assetWriterInput markAsFinished]; break; } } }];
上面代码中的 copyNextSampleBufferToWrite
方法只是一个占位方法。在这个方法里须要实现一些逻辑来返回将要写入的媒体数据,用 CMSampleBufferRef
对象表示,这里的 sample buffer 就能够用 asset reader input 做为数据来源。
您能够一块儿使用 asset reader 和 asset writer 对象将 asset 从一种格式转换为另外一种。使用这些对象,咱们对转换的控制比 AVAssetExportSession
要多得多。好比,咱们能够选择哪些轨道要写入到输出文件中;指定输出格式;在转换过程当中修改 asset。此过程的第一步只是根据须要设置 asset reader outputs 和 asset writer inputs。在 asset reader 彻底配置后,能够分别调用 startReading
和 startWriting
方法来启动。
如下代码展现了如何使用单个 asset writer input 来写入由单个 asset reader output 提供的媒体数据:
NSString *serializationQueueDescription = [NSString stringWithFormat:@"%@ serialization queue", self]; // Create a serialization queue for reading and writing. dispatch_queue_t serializationQueue = dispatch_queue_create([serializationQueueDescription UTF8String], NULL); // Specify the block to execute when the asset writer is ready for media data and the queue to call it on. [self.assetWriterInput requestMediaDataWhenReadyOnQueue:serializationQueue usingBlock:^{ while ([self.assetWriterInput isReadyForMoreMediaData]) { // Get the asset reader output's next sample buffer. CMSampleBufferRef sampleBuffer = [self.assetReaderOutput copyNextSampleBuffer]; if (sampleBuffer != NULL) { // If it exists, append this sample buffer to the output file. BOOL success = [self.assetWriterInput appendSampleBuffer:sampleBuffer]; CFRelease(sampleBuffer); sampleBuffer = NULL; // Check for errors that may have occurred when appending the new sample buffer. if (!success && self.assetWriter.status == AVAssetWriterStatusFailed) { NSError *failureError = self.assetWriter.error; //Handle the error. } } else { // If the next sample buffer doesn't exist, find out why the asset reader output couldn't vend another one. if (self.assetReader.status == AVAssetReaderStatusFailed) { NSError *failureError = self.assetReader.error; //Handle the error here. } else { // The asset reader output must have vended all of its samples. Mark the input as finished. [self.assetWriterInput markAsFinished]; break; } } } }];
下面的代码展现了如何使用 asset reader 和 asset writer 将资产的第一个视频轨道和音频轨从新编码到新的文件中。其中主要包括下面这些步骤:
下面是初始化的代码:
// 初始化过程: NSString *serializationQueueDescription = [NSString stringWithFormat:@"%@ serialization queue", self]; // Create the main serialization queue. self.mainSerializationQueue = dispatch_queue_create([serializationQueueDescription UTF8String], NULL); NSString *rwAudioSerializationQueueDescription = [NSString stringWithFormat:@"%@ rw audio serialization queue", self]; // Create the serialization queue to use for reading and writing the audio data. self.rwAudioSerializationQueue = dispatch_queue_create([rwAudioSerializationQueueDescription UTF8String], NULL); NSString *rwVideoSerializationQueueDescription = [NSString stringWithFormat:@"%@ rw video serialization queue", self]; // Create the serialization queue to use for reading and writing the video data. self.rwVideoSerializationQueue = dispatch_queue_create([rwVideoSerializationQueueDescription UTF8String], NULL); // 加载资源: self.asset = <#AVAsset that you want to reencode#>; self.cancelled = NO; self.outputURL = <#NSURL representing desired output URL for file generated by asset writer#>; // Asynchronously load the tracks of the asset you want to read. [self.asset loadValuesAsynchronouslyForKeys:@[@"tracks"] completionHandler:^{ // Once the tracks have finished loading, dispatch the work to the main serialization queue. dispatch_async(self.mainSerializationQueue, ^{ // Due to asynchronous nature, check to see if user has already cancelled. if (self.cancelled) { return; } BOOL success = YES; NSError *localError = nil; // Check for success of loading the assets tracks. success = ([self.asset statusOfValueForKey:@"tracks" error:&localError] == AVKeyValueStatusLoaded); if (success) { // If the tracks loaded successfully, make sure that no file exists at the output path for the asset writer. NSFileManager *fm = [NSFileManager defaultManager]; NSString *localOutputPath = [self.outputURL path]; if ([fm fileExistsAtPath:localOutputPath]) { success = [fm removeItemAtPath:localOutputPath error:&localError]; } } if (success) { success = [self setupAssetReaderAndAssetWriter:&localError]; } if (success) { success = [self startAssetReaderAndWriter:&localError]; } if (!success) { [self readingAndWritingDidFinishSuccessfully:success withError:localError]; } }); }];
下面是初始化 asset reader 和 writer 的代码:
- (BOOL)setupAssetReaderAndAssetWriter:(NSError **)outError { // Create and initialize the asset reader. self.assetReader = [[AVAssetReader alloc] initWithAsset:self.asset error:outError]; BOOL success = (self.assetReader != nil); if (success) { // If the asset reader was successfully initialized, do the same for the asset writer. self.assetWriter = [[AVAssetWriter alloc] initWithURL:self.outputURL fileType:AVFileTypeQuickTimeMovie error:outError]; success = (self.assetWriter != nil); } if (success) { // If the reader and writer were successfully initialized, grab the audio and video asset tracks that will be used. AVAssetTrack *assetAudioTrack = nil, *assetVideoTrack = nil; NSArray *audioTracks = [self.asset tracksWithMediaType:AVMediaTypeAudio]; if ([audioTracks count] > 0) { assetAudioTrack = [audioTracks objectAtIndex:0]; } NSArray *videoTracks = [self.asset tracksWithMediaType:AVMediaTypeVideo]; if ([videoTracks count] > 0) { assetVideoTrack = [videoTracks objectAtIndex:0]; } if (assetAudioTrack) { // If there is an audio track to read, set the decompression settings to Linear PCM and create the asset reader output. NSDictionary *decompressionAudioSettings = @{ AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM] }; self.assetReaderAudioOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:assetAudioTrack outputSettings:decompressionAudioSettings]; [self.assetReader addOutput:self.assetReaderAudioOutput]; // Then, set the compression settings to 128kbps AAC and create the asset writer input. AudioChannelLayout stereoChannelLayout = { .mChannelLayoutTag = kAudioChannelLayoutTag_Stereo, .mChannelBitmap = 0, .mNumberChannelDescriptions = 0 }; NSData *channelLayoutAsData = [NSData dataWithBytes:&stereoChannelLayout length:offsetof(AudioChannelLayout, mChannelDescriptions)]; NSDictionary *compressionAudioSettings = @{ AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatMPEG4AAC], AVEncoderBitRateKey : [NSNumber numberWithInteger:128000], AVSampleRateKey : [NSNumber numberWithInteger:44100], AVChannelLayoutKey : channelLayoutAsData, AVNumberOfChannelsKey : [NSNumber numberWithUnsignedInteger:2] }; self.assetWriterAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:[assetAudioTrack mediaType] outputSettings:compressionAudioSettings]; [self.assetWriter addInput:self.assetWriterAudioInput]; } if (assetVideoTrack) { // If there is a video track to read, set the decompression settings for YUV and create the asset reader output. NSDictionary *decompressionVideoSettings = @{ (id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithUnsignedInt:kCVPixelFormatType_422YpCbCr8], (id)kCVPixelBufferIOSurfacePropertiesKey : [NSDictionary dictionary] }; self.assetReaderVideoOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:assetVideoTrack outputSettings:decompressionVideoSettings]; [self.assetReader addOutput:self.assetReaderVideoOutput]; CMFormatDescriptionRef formatDescription = NULL; // Grab the video format descriptions from the video track and grab the first one if it exists. NSArray *videoFormatDescriptions = [assetVideoTrack formatDescriptions]; if ([videoFormatDescriptions count] > 0) { formatDescription = (__bridge CMFormatDescriptionRef)[formatDescriptions objectAtIndex:0]; } CGSize trackDimensions = { .width = 0.0, .height = 0.0, }; // If the video track had a format description, grab the track dimensions from there. Otherwise, grab them direcly from the track itself. if (formatDescription) { trackDimensions = CMVideoFormatDescriptionGetPresentationDimensions(formatDescription, false, false); } else { trackDimensions = [assetVideoTrack naturalSize]; } NSDictionary *compressionSettings = nil; // If the video track had a format description, attempt to grab the clean aperture settings and pixel aspect ratio used by the video. if (formatDescription) { NSDictionary *cleanAperture = nil; NSDictionary *pixelAspectRatio = nil; CFDictionaryRef cleanApertureFromCMFormatDescription = CMFormatDescriptionGetExtension(formatDescription, kCMFormatDescriptionExtension_CleanAperture); if (cleanApertureFromCMFormatDescription) { cleanAperture = @{ AVVideoCleanApertureWidthKey : (id)CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureWidth), AVVideoCleanApertureHeightKey : (id)CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureHeight), AVVideoCleanApertureHorizontalOffsetKey : (id)CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureHorizontalOffset), AVVideoCleanApertureVerticalOffsetKey : (id)CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureVerticalOffset) }; } CFDictionaryRef pixelAspectRatioFromCMFormatDescription = CMFormatDescriptionGetExtension(formatDescription, kCMFormatDescriptionExtension_PixelAspectRatio); if (pixelAspectRatioFromCMFormatDescription) { pixelAspectRatio = @{ AVVideoPixelAspectRatioHorizontalSpacingKey : (id)CFDictionaryGetValue(pixelAspectRatioFromCMFormatDescription, kCMFormatDescriptionKey_PixelAspectRatioHorizontalSpacing), AVVideoPixelAspectRatioVerticalSpacingKey : (id)CFDictionaryGetValue(pixelAspectRatioFromCMFormatDescription, kCMFormatDescriptionKey_PixelAspectRatioVerticalSpacing) }; } // Add whichever settings we could grab from the format description to the compression settings dictionary. if (cleanAperture || pixelAspectRatio) { NSMutableDictionary *mutableCompressionSettings = [NSMutableDictionary dictionary]; if (cleanAperture) { [mutableCompressionSettings setObject:cleanAperture forKey:AVVideoCleanApertureKey]; } if (pixelAspectRatio) { [mutableCompressionSettings setObject:pixelAspectRatio forKey:AVVideoPixelAspectRatioKey]; } compressionSettings = mutableCompressionSettings; } } // Create the video settings dictionary for H.264. NSMutableDictionary *videoSettings = (NSMutableDictionary *) @{ AVVideoCodecKey : AVVideoCodecH264, AVVideoWidthKey : [NSNumber numberWithDouble:trackDimensions.width], AVVideoHeightKey : [NSNumber numberWithDouble:trackDimensions.height] }; // Put the compression settings into the video settings dictionary if we were able to grab them. if (compressionSettings) { [videoSettings setObject:compressionSettings forKey:AVVideoCompressionPropertiesKey]; } // Create the asset writer input and add it to the asset writer. self.assetWriterVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:[videoTrack mediaType] outputSettings:videoSettings]; [self.assetWriter addInput:self.assetWriterVideoInput]; } } return success; }
下面是从新编码 asset 的代码:
- (BOOL)startAssetReaderAndWriter:(NSError **)outError { BOOL success = YES; // Attempt to start the asset reader. success = [self.assetReader startReading]; if (!success) { *outError = [self.assetReader error]; } if (success) { // If the reader started successfully, attempt to start the asset writer. success = [self.assetWriter startWriting]; if (!success) { *outError = [self.assetWriter error]; } } if (success) { // If the asset reader and writer both started successfully, create the dispatch group where the reencoding will take place and start a sample-writing session. self.dispatchGroup = dispatch_group_create(); [self.assetWriter startSessionAtSourceTime:kCMTimeZero]; self.audioFinished = NO; self.videoFinished = NO; if (self.assetWriterAudioInput) { // If there is audio to reencode, enter the dispatch group before beginning the work. dispatch_group_enter(self.dispatchGroup); // Specify the block to execute when the asset writer is ready for audio media data, and specify the queue to call it on. [self.assetWriterAudioInput requestMediaDataWhenReadyOnQueue:self.rwAudioSerializationQueue usingBlock:^{ // Because the block is called asynchronously, check to see whether its task is complete. if (self.audioFinished) { return; } BOOL completedOrFailed = NO; // If the task isn't complete yet, make sure that the input is actually ready for more media data. while ([self.assetWriterAudioInput isReadyForMoreMediaData] && !completedOrFailed) { // Get the next audio sample buffer, and append it to the output file. CMSampleBufferRef sampleBuffer = [self.assetReaderAudioOutput copyNextSampleBuffer]; if (sampleBuffer != NULL) { BOOL success = [self.assetWriterAudioInput appendSampleBuffer:sampleBuffer]; CFRelease(sampleBuffer); sampleBuffer = NULL; completedOrFailed = !success; } else { completedOrFailed = YES; } } if (completedOrFailed) { // Mark the input as finished, but only if we haven't already done so, and then leave the dispatch group (since the audio work has finished). BOOL oldFinished = self.audioFinished; self.audioFinished = YES; if (oldFinished == NO) { [self.assetWriterAudioInput markAsFinished]; } dispatch_group_leave(self.dispatchGroup); } }]; } if (self.assetWriterVideoInput) { // If we had video to reencode, enter the dispatch group before beginning the work. dispatch_group_enter(self.dispatchGroup); // Specify the block to execute when the asset writer is ready for video media data, and specify the queue to call it on. [self.assetWriterVideoInput requestMediaDataWhenReadyOnQueue:self.rwVideoSerializationQueue usingBlock:^{ // Because the block is called asynchronously, check to see whether its task is complete. if (self.videoFinished) { return; } BOOL completedOrFailed = NO; // If the task isn't complete yet, make sure that the input is actually ready for more media data. while ([self.assetWriterVideoInput isReadyForMoreMediaData] && !completedOrFailed) { // Get the next video sample buffer, and append it to the output file. CMSampleBufferRef sampleBuffer = [self.assetReaderVideoOutput copyNextSampleBuffer]; if (sampleBuffer != NULL) { BOOL success = [self.assetWriterVideoInput appendSampleBuffer:sampleBuffer]; CFRelease(sampleBuffer); sampleBuffer = NULL; completedOrFailed = !success; } else { completedOrFailed = YES; } } if (completedOrFailed) { // Mark the input as finished, but only if we haven't already done so, and then leave the dispatch group (since the video work has finished). BOOL oldFinished = self.videoFinished; self.videoFinished = YES; if (oldFinished == NO) { [self.assetWriterVideoInput markAsFinished]; } dispatch_group_leave(self.dispatchGroup); } }]; } // Set up the notification that the dispatch group will send when the audio and video work have both finished. dispatch_group_notify(self.dispatchGroup, self.mainSerializationQueue, ^{ BOOL finalSuccess = YES; NSError *finalError = nil; // Check to see if the work has finished due to cancellation. if (self.cancelled) { // If so, cancel the reader and writer. [self.assetReader cancelReading]; [self.assetWriter cancelWriting]; } else { // If cancellation didn't occur, first make sure that the asset reader didn't fail. if ([self.assetReader status] == AVAssetReaderStatusFailed) { finalSuccess = NO; finalError = [self.assetReader error]; } // If the asset reader didn't fail, attempt to stop the asset writer and check for any errors. if (finalSuccess) { finalSuccess = [self.assetWriter finishWriting]; if (!finalSuccess) { finalError = [self.assetWriter error]; } } } // Call the method to handle completion, and pass in the appropriate parameters to indicate whether reencoding was successful. [self readingAndWritingDidFinishSuccessfully:finalSuccess withError:finalError]; }); } // Return success here to indicate whether the asset reader and writer were started successfully. return success; }
处理完成的代码:
- (void)readingAndWritingDidFinishSuccessfully:(BOOL)success withError:(NSError *)error { if (!success) { // If the reencoding process failed, we need to cancel the asset reader and writer. [self.assetReader cancelReading]; [self.assetWriter cancelWriting]; dispatch_async(dispatch_get_main_queue(), ^{ // Handle any UI tasks here related to failure. }); } else { // Reencoding was successful, reset booleans. self.cancelled = NO; self.videoFinished = NO; self.audioFinished = NO; dispatch_async(dispatch_get_main_queue(), ^{ // Handle any UI tasks here related to success. }); } }
处理取消的代码:
- (void)cancel { // Handle cancellation asynchronously, but serialize it with the main queue. dispatch_async(self.mainSerializationQueue, ^{ // If we had audio data to reencode, we need to cancel the audio work. if (self.assetWriterAudioInput) { // Handle cancellation asynchronously again, but this time serialize it with the audio queue. dispatch_async(self.rwAudioSerializationQueue, ^{ // Update the Boolean property indicating the task is complete and mark the input as finished if it hasn't already been marked as such. BOOL oldFinished = self.audioFinished; self.audioFinished = YES; if (oldFinished == NO) { [self.assetWriterAudioInput markAsFinished]; } // Leave the dispatch group since the audio work is finished now. dispatch_group_leave(self.dispatchGroup); }); } if (self.assetWriterVideoInput) { // Handle cancellation asynchronously again, but this time serialize it with the video queue. dispatch_async(self.rwVideoSerializationQueue, ^{ // Update the Boolean property indicating the task is complete and mark the input as finished if it hasn't already been marked as such. BOOL oldFinished = self.videoFinished; self.videoFinished = YES; if (oldFinished == NO) { [self.assetWriterVideoInput markAsFinished]; } // Leave the dispatch group, since the video work is finished now. dispatch_group_leave(self.dispatchGroup); }); } // Set the cancelled Boolean property to YES to cancel any work on the main queue as well. self.cancelled = YES; }); }
AVOutputSettingsAssistant
类有助于为 asset reader 或 writer 建立输出设置字典。这使得设置更简单,特别是对于具备多个特定预设的高帧率 H264 电影。
下面的示例是如何使用 output settings assistant:
AVOutputSettingsAssistant *outputSettingsAssistant = [AVOutputSettingsAssistant outputSettingsAssistantWithPreset:<#some preset#>]; CMFormatDescriptionRef audioFormat = [self getAudioFormat]; if (audioFormat != NULL) { [outputSettingsAssistant setSourceAudioFormat:(CMAudioFormatDescriptionRef)audioFormat]; } CMFormatDescriptionRef videoFormat = [self getVideoFormat]; if (videoFormat != NULL) { [outputSettingsAssistant setSourceVideoFormat:(CMVideoFormatDescriptionRef)videoFormat]; } CMTime assetMinVideoFrameDuration = [self getMinFrameDuration]; CMTime averageFrameDuration = [self getAvgFrameDuration] [outputSettingsAssistant setSourceVideoAverageFrameDuration:averageFrameDuration]; [outputSettingsAssistant setSourceVideoMinFrameDuration:assetMinVideoFrameDuration]; AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:<#some URL#> fileType:[outputSettingsAssistant outputFileType] error:NULL]; AVAssetWriterInput *audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:[outputSettingsAssistant audioSettings] sourceFormatHint:audioFormat]; AVAssetWriterInput *videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:[outputSettingsAssistant videoSettings] sourceFormatHint:videoFormat];