本文轉(zhuǎn)自:AVAudioFoundation(5):音視頻導(dǎo)出 | www.samirchen.com
本文主要內(nèi)容來自 AVFoundation Programming Guide呼伸。
要讀寫音視頻數(shù)據(jù)資源 asset俐镐,我們需要用到 AVFoundation 提供的文件導(dǎo)出 API。AVAssetExportSession
提供了比較簡單的 API 來滿足基本的導(dǎo)出需求蔑祟,比如修改文件類型淀衣、剪輯資源長度篱昔。如果要滿足更加深度的導(dǎo)出需求瑞妇,我們則需要用到 AVAssetReader
和 AVAssetWriter
扔罪。
當我們需要去操作 asset 的內(nèi)容時秉沼,我們可以用 AVAssetReader
,比如讀取 asset 中的音頻軌道來展示波形等等矿酵。當我們想用一些音頻采樣或靜態(tài)圖片去生成 asset 時唬复,我們可以使用 AVAssetWriter
。
需要注意的是 AVAssetReader
不適用于做實時處理全肮。AVAssetReader
沒法用來處理 HLS 之類的實時數(shù)據(jù)流敞咧。但是 AVAssetWriter
是可以用來處理實時數(shù)據(jù)源的,比如 AVCaptureOutput
辜腺,當需要處理實時數(shù)據(jù)源時休建,需要設(shè)置 expectsMediaDataInRealTime
屬性為 YES
。如果對非實時數(shù)據(jù)源設(shè)置該屬性為 YES
评疗,那么可能會造成你導(dǎo)出的文件有問題测砂。
讀取 Asset
每一個 AVAssetReader
一次只能與一個 asset 關(guān)聯(lián),但是這個 asset 可以包含多個軌道百匆。由于這個原因通常我們需要為 AVAssetReader
指定一個 AVAssetReaderOutput
的具體子類來具體操作 asset 的讀取砌些,比如:
AVAssetReaderTrackOutput
AVAssetReaderAudioMixOutput
AVAssetReaderVideoCompositionOutput
創(chuàng)建 Asset Reader
初始化 AVAssetReader
時需要傳入相應(yīng)讀取的 asset。
NSError *outError;
AVAsset *someAsset = <#AVAsset that you want to read#>;
AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:someAsset error:&outError];
BOOL success = (assetReader != nil);
需要注意的是要檢查創(chuàng)建的 asset reader 是否為空加匈。初始化失敗時寄症,可以查看具體的 error 信息。
創(chuàng)建 Asset Reader Outputs
在完成創(chuàng)建 asset reader 后矩动,創(chuàng)建至少一個 output 對象來接收讀取的媒體數(shù)據(jù)有巧。當創(chuàng)建 output 對象時,要記得設(shè)置 alwaysCopiesSampleData
屬性為 NO
悲没,這樣你會獲得性能上的提升篮迎。在本章中的示例代碼中男图,這個屬性都應(yīng)該被設(shè)置為 NO
。
如果我們想從媒體資源中讀取一個或多個軌道甜橱,并且可能會轉(zhuǎn)換數(shù)據(jù)的格式逊笆,那么可以使用 AVAssetReaderTrackOutput
類,為每個 AVAssetTrack
軌道使用一個 track output 對象岂傲。
下面的示例展示了使用 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 按它原本存儲的格式讀取媒體數(shù)據(jù)难裆,設(shè)置 outputSettings
為 nil
即可。
當想要讀取用 AVAudioMix
或 AVVideoComposition
混音或編輯過的媒體數(shù)據(jù)時镊掖,需要對應(yīng)的使用 AVAssetReaderAudioMixOutput
和 AVAssetReaderVideoCompositionOutput
乃戈。一般,當我們從一個 AVComposition
讀取媒體資源時亩进,我們需要用到這些 output 類症虑。
使用一個單獨的 audio mix output 我們就可以讀取 AVAudioMix
混合過的 asset 中的多個音頻軌道。為了指明這些音頻軌道是如何混合的归薛,我們需要在 AVAssetReaderAudioMixOutput
對象初始化完成后將對應(yīng)的 AVAudioMix
對象設(shè)置給它的 audioMix
屬性谍憔。
下面的代碼展示了如何基于一個 asset 來創(chuàng)建我們的 audio mix output 對象去處理所有的音頻軌道,然后壓縮這些音頻軌道為線性 PCM 數(shù)據(jù)主籍,并為 output 對象設(shè)置 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
參數(shù)傳為 nil
千元。對于使用 AVAssetReaderVideoCompositionOutput
時苫昌,也是同樣的道理。
我們使用 video composition output 的方式跟上面類似诅炉。下面的代碼展示了,如果讀取多個視頻軌道的媒體數(shù)據(jù)并將他們壓縮為 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];
}
讀取 Asset 的媒體數(shù)據(jù)
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 對象創(chuàng)建完成后涕烧,接著就要開始讀取數(shù)據(jù)了,這時候我們需要調(diào)用 asset reader 的 startReading
接口汗洒。接著议纯,使用 copyNextSampleBuffer
接口來從各個 output 來獲取媒體數(shù)據(jù)。
下面的代碼展示了 asset reader 如何用一個 output 對象從 asset 中讀取所有的媒體數(shù)據(jù):
// 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;
}
}
}
寫入 Asset
AVAssetWriter
類可以將媒體數(shù)據(jù)從多個源寫入指定文件格式的單個文件溢谤。我們也不需要將 asset writer 對象與特定 asset 相關(guān)聯(lián)瞻凤,但必須為要創(chuàng)建的每個輸出文件使用單獨的 asset writer。由于 asset writer 可以從多個來源寫出媒體數(shù)據(jù)世杀,因此我們必須為每個要被寫入到輸出文件的軌道創(chuàng)建一個 AVAssetWriterInput
對象阀参。每個 AVAssetWriterInput
對象都期望以 CMSampleBufferRef
對象的形式接收數(shù)據(jù),但是如果要將 CVPixelBufferRef
對象附加到 asset writer瞻坝,可以使用 AVAssetWriterInputPixelBufferAdaptor
類蛛壳。
創(chuàng)建 Asset Writer
為了創(chuàng)建 asset writer,我們需要指定輸出文件的 URL 和所需的文件類型。以下代碼展示了如何初始化 asset writer 來創(chuàng)建 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);
創(chuàng)建 Asset Writer Inputs
想要用 asset writer 來寫媒體數(shù)據(jù)衙荐,必須至少設(shè)置一個 asset writer input捞挥。例如,如果數(shù)據(jù)源已經(jīng)使用 CMSampleBufferRef
類來表示忧吟,那么使用 AVAssetWriterInput
類即可砌函。
下面的代碼展示了如何設(shè)置將音頻媒體數(shù)據(jù)壓縮為 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];
}
需要注意的是,如果要以原本存儲的格式來寫入媒體數(shù)據(jù)溜族,可以在 outputSettings
參數(shù)中傳 nil
讹俊。只有 asset writer 使用 AVFileTypeQuickTimeMovie
的 fileType
進行初始化時,才能傳 nil
斩祭。
您的 asset writer input 可以通過設(shè)置 metadata
和 transform
屬性來包含一些元數(shù)據(jù)或為特定的軌道指定不同的變換劣像。對于數(shù)據(jù)源是視頻軌道的 asset writer input,您可以通過執(zhí)行以下操作來將視頻的原始變換保存在輸出文件中:
AVAsset *videoAsset = <#AVAsset with at least one video track#>;
AVAssetTrack *videoAssetTrack = [[videoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
assetWriterInput.transform = videoAssetTrack.preferredTransform;
需要注意的是 metadata
和 transform
這兩個屬性需要在開始寫之前設(shè)定才會生效摧玫。
將媒體數(shù)據(jù)寫入輸出文件時耳奕,有時我們可能需要分配像素數(shù)據(jù)緩沖區(qū)。這時可以使用 AVAssetWriterInputPixelBufferAdaptor
類诬像。為了最大的效率屋群,不要使用單獨的池分配的像素緩沖區(qū),而是使用像素緩沖適配器提供的像素緩沖池坏挠。以下代碼創(chuàng)建一個在 RGB 域中工作的像素緩沖區(qū)對象芍躏,它將使用 CGImage
對象來創(chuàng)建其像素緩沖區(qū):
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
類型的媒體數(shù)據(jù)杠巡。
寫媒體數(shù)據(jù)
配置 asset writer 所需的所有輸入后,即可開始寫媒體數(shù)據(jù)丛忆。與 asset reader 一樣慈格,通過調(diào)用 startWriting
方法啟動寫入過程。然后蛋褥,需要通過調(diào)用 startSessionAtSourceTime:
方法啟動寫入會話临燃。Asset writer 完成的所有寫入都必須在這些會話之一內(nèi)進行,每個會話的時間范圍不超出源中包含的媒體數(shù)據(jù)的時間范圍烙心。
下面的代碼展示了當我們的數(shù)據(jù)源是一個 asset reader膜廊,它提供從 AVAsset
對象讀取的媒體數(shù)據(jù),并且不希望包含資產(chǎn)前半部分的媒體數(shù)據(jù):
CMTime halfAssetDuration = CMTimeMultiplyByFloat64(self.asset.duration, 0.5);
[self.assetWriter startSessionAtSourceTime:halfAssetDuration];
//Implementation continues.
通常淫茵,要結(jié)束寫入會話爪瓜,您必須調(diào)用 endSessionAtSourceTime:
方法。但是匙瘪,如果寫入會話直到文件的末尾钥勋,則可以通過調(diào)用 finishWriting
方法來結(jié)束炬转。
下面代碼展示了使用單個 input 啟動 asset writer 并寫入其所有媒體數(shù)據(jù):
// 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
方法只是一個占位方法。在這個方法里需要實現(xiàn)一些邏輯來返回將要寫入的媒體數(shù)據(jù)算灸,用 CMSampleBufferRef
對象表示扼劈,這里的 sample buffer 就可以用 asset reader input 作為數(shù)據(jù)來源。
重編碼 Asset
您可以一起使用 asset reader 和 asset writer 對象將 asset 從一種格式轉(zhuǎn)換為另一種菲驴。使用這些對象荐吵,我們對轉(zhuǎn)換的控制比 AVAssetExportSession
要多得多。比如赊瞬,我們可以選擇哪些軌道要寫入到輸出文件中先煎;指定輸出格式;在轉(zhuǎn)換過程中修改 asset巧涧。此過程的第一步只是根據(jù)需要設(shè)置 asset reader outputs 和 asset writer inputs薯蝎。在 asset reader 完全配置后,可以分別調(diào)用 startReading
和 startWriting
方法來啟動谤绳。
以下代碼展示了如何使用單個 asset writer input 來寫入由單個 asset reader output 提供的媒體數(shù)據(jù):
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 將資產(chǎn)的第一個視頻軌道和音頻軌重新編碼到新的文件中占锯。其中主要包括下面這些步驟:
- 使用序列化隊列來處理讀和寫視聽數(shù)據(jù)。
- 初始化 asset reader 并配置兩個 output缩筛,一個用于音頻消略,另一個用于視頻。
- 初始化 asset writer 并配置兩個 input瞎抛,一個用于音頻艺演,另一個用于視頻。
- 使用 asset reader 通過兩種不同的輸出/輸入組合異步地向 asset writer 提供媒體數(shù)據(jù)桐臊。
- 使用 dispatch group 來完成重編碼的異步調(diào)度胎撤。
- 允許用戶在編碼開始后取消該操作。
下面是初始化的代碼:
// 初始化過程:
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;
});
}
Asset Output 設(shè)置助手
AVOutputSettingsAssistant
類有助于為 asset reader 或 writer 創(chuàng)建輸出設(shè)置字典断凶。這使得設(shè)置更簡單伤提,特別是對于具有多個特定預(yù)設(shè)的高幀率 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];