AVAudioFoundation(5):音視頻導出

本文轉自:AVAudioFoundation(5):音視頻導出 | www.samirchen.comhtml

本文主要內容來自 AVFoundation Programming Guideios

要讀寫音視頻數據資源 asset,咱們須要用到 AVFoundation 提供的文件導出 API。AVAssetExportSession 提供了比較簡單的 API 來知足基本的導出需求,好比修改文件類型、剪輯資源長度。若是要知足更加深度的導出需求,咱們則須要用到 AVAssetReaderAVAssetWritersession

當咱們須要去操做 asset 的內容時,咱們能夠用 AVAssetReader,好比讀取 asset 中的音頻軌道來展現波形等等。當咱們想用一些音頻採樣或靜態圖片去生成 asset 時,咱們可使用 AVAssetWriterapp

須要注意的是 AVAssetReader 不適用於作實時處理。AVAssetReader 無法用來處理 HLS 之類的實時數據流。可是 AVAssetWriter 是能夠用來處理實時數據源的,好比 AVCaptureOutput,當須要處理實時數據源時,須要設置 expectsMediaDataInRealTime 屬性爲 YES。若是對非實時數據源設置該屬性爲 YES,那麼可能會形成你導出的文件有問題。異步

讀取 Asset

每個 AVAssetReader 一次只能與一個 asset 關聯,可是這個 asset 能夠包含多個軌道。因爲這個緣由一般咱們須要爲 AVAssetReader 指定一個 AVAssetReaderOutput 的具體子類來具體操做 asset 的讀取,好比:async

  • AVAssetReaderTrackOutput
  • AVAssetReaderAudioMixOutput
  • AVAssetReaderVideoCompositionOutput

建立 Asset Reader

初始化 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 Outputs

在完成建立 asset reader 後,建立至少一個 output 對象來接收讀取的媒體數據。當建立 output 對象時,要記得設置 alwaysCopiesSampleData 屬性爲 NO,這樣你會得到性能上的提高。在本章中的示例代碼中,這個屬性都應該被設置爲 NOui

若是咱們想從媒體資源中讀取一個或多個軌道,而且可能會轉換數據的格式,那麼可使用 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 按它本來存儲的格式讀取媒體數據,設置 outputSettingsnil 便可。

當想要讀取用 AVAudioMixAVVideoComposition 混音或編輯過的媒體數據時,須要對應的使用 AVAssetReaderAudioMixOutputAVAssetReaderVideoCompositionOutput。通常,當咱們從一個 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];
}

讀取 Asset 的媒體數據

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

寫入 Asset

AVAssetWriter 類能夠將媒體數據從多個源寫入指定文件格式的單個文件。咱們也不須要將 asset writer 對象與特定 asset 相關聯,但必須爲要建立的每一個輸出文件使用單獨的 asset writer。因爲 asset writer 能夠從多個來源寫出媒體數據,所以咱們必須爲每一個要被寫入到輸出文件的軌道建立一個 AVAssetWriterInput 對象。每一個 AVAssetWriterInput 對象都指望以 CMSampleBufferRef 對象的形式接收數據,可是若是要將 CVPixelBufferRef 對象附加到 asset writer,可使用 AVAssetWriterInputPixelBufferAdaptor 類。

建立 Asset Writer

爲了建立 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 Inputs

想要用 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 使用 AVFileTypeQuickTimeMoviefileType 進行初始化時,才能傳 nil

您的 asset writer input 能夠經過設置 metadatatransform 屬性來包含一些元數據或爲特定的軌道指定不一樣的變換。對於數據源是視頻軌道的 asset writer input,您能夠經過執行如下操做來將視頻的原始變換保存在輸出文件中:

AVAsset *videoAsset = <#AVAsset with at least one video track#>;
AVAssetTrack *videoAssetTrack = [[videoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
assetWriterInput.transform = videoAssetTrack.preferredTransform;

須要注意的是 metadatatransform 這兩個屬性須要在開始寫以前設定纔會生效。

將媒體數據寫入輸出文件時,有時咱們可能須要分配像素數據緩衝區。這時可使用 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

您能夠一塊兒使用 asset reader 和 asset writer 對象將 asset 從一種格式轉換爲另外一種。使用這些對象,咱們對轉換的控制比 AVAssetExportSession 要多得多。好比,咱們能夠選擇哪些軌道要寫入到輸出文件中;指定輸出格式;在轉換過程當中修改 asset。此過程的第一步只是根據須要設置 asset reader outputs 和 asset writer inputs。在 asset reader 徹底配置後,能夠分別調用 startReadingstartWriting 方法來啓動。

如下代碼展現瞭如何使用單個 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 將資產的第一個視頻軌道和音頻軌從新編碼到新的文件中。其中主要包括下面這些步驟:

  • 使用序列化隊列來處理讀和寫視聽數據。
  • 初始化 asset reader 並配置兩個 output,一個用於音頻,另外一個用於視頻。
  • 初始化 asset writer 並配置兩個 input,一個用於音頻,另外一個用於視頻。
  • 使用 asset reader 經過兩種不一樣的輸出/輸入組合異步地向 asset writer 提供媒體數據。
  • 使用 dispatch group 來完成重編碼的異步調度。
  • 容許用戶在編碼開始後取消該操做。

下面是初始化的代碼:

// 初始化過程:
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 設置助手

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];
相關文章
相關標籤/搜索