AVAudioFoundation(5):音視頻導(dǎo)出

本文轉(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)出需求瑞妇,我們則需要用到 AVAssetReaderAVAssetWriter扔罪。

當我們需要去操作 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è)置 outputSettingsnil 即可。

當想要讀取用 AVAudioMixAVVideoComposition 混音或編輯過的媒體數(shù)據(jù)時镊掖,需要對應(yīng)的使用 AVAssetReaderAudioMixOutputAVAssetReaderVideoCompositionOutput乃戈。一般,當我們從一個 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 使用 AVFileTypeQuickTimeMoviefileType 進行初始化時,才能傳 nil斩祭。

您的 asset writer input 可以通過設(shè)置 metadatatransform 屬性來包含一些元數(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;

需要注意的是 metadatatransform 這兩個屬性需要在開始寫之前設(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)用 startReadingstartWriting 方法來啟動谤绳。

以下代碼展示了如何使用單個 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];
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末懒浮,一起剝皮案震驚了整個濱河市飘弧,隨后出現(xiàn)的幾起案子识藤,更是在濱河造成了極大的恐慌砚著,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痴昧,死亡現(xiàn)場離奇詭異稽穆,居然都是意外死亡,警方通過查閱死者的電腦和手機赶撰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門舌镶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來柱彻,“玉大人,你說我怎么就攤上這事餐胀∮纯” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵否灾,是天一觀的道長卖擅。 經(jīng)常有香客問我,道長墨技,這世上最難降的妖魔是什么惩阶? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮扣汪,結(jié)果婚禮上断楷,老公的妹妹穿的比我還像新娘。我一直安慰自己崭别,他們只是感情好冬筒,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著紊遵,像睡著了一般账千。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上暗膜,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天匀奏,我揣著相機與錄音,去河邊找鬼学搜。 笑死娃善,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的瑞佩。 我是一名探鬼主播聚磺,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼炬丸!你這毒婦竟也來了瘫寝?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤稠炬,失蹤者是張志新(化名)和其女友劉穎焕阿,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體首启,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡暮屡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了毅桃。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片褒纲。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡准夷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出莺掠,到底是詐尸還是另有隱情衫嵌,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布彻秆,位于F島的核電站渐扮,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏掖棉。R本人自食惡果不足惜墓律,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望幔亥。 院中可真熱鬧耻讽,春花似錦、人聲如沸帕棉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽香伴。三九已至慰枕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間即纲,已是汗流浹背具帮。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留低斋,地道東北人蜂厅。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像膊畴,于是被迫代替她去往敵國和親掘猿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容