AVFoundation 讀取和寫入媒體

1. 綜述

AVFoundation 提供了對底層數據的讀寫功能,須要用到 AVAssetReader 和 AVAssetWriter 兩個核心類。數組

AVAssetReader 用於從 AVAsset 實例讀取媒體樣本,須要配置一個或多個 AVAssetReaderOutput 實例。多線程

AVAssetReaderOutput 是一個抽象類,下分三個具體類,負責讀取指定 AVAssetTrack 的AVAssetReaderTrackOutput,負責讀取多音頻軌道的 AVAssetReaderAudioMixOutput,負責讀取多媒體軌道的 AVAssetReaderVideoCompositionOutput。其內部通道以多線程方式讀取下一個可用樣本,從而下降請求資源的延時。但仍然不推薦用 AVAssetReaderOutput 實現包括播放在內的實時操做。app

AVAssetWriter 用於對媒體資源進行編碼和寫入,須要配置一個或多個 AVAssetWriterInput 實例,一個 AVAssetWriterInput 負責一種媒體類型,最終生成獨立的 AVAssetTrack。同時還須要用到 AVAssetWriterInputPixelBufferAdaptor 爲視頻樣本提供最優性能。async

下面是一個簡單的使用示例ide

  • 進行媒體資源讀取準備
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:videoURL options:nil];
    AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject]; // 獲取 video 類型的媒體軌道
    self.assetReader = [[AVAssetReader alloc] initWithAsset:asset error:nil];
    NSDictionary *readerOutputSettings = @{(id)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA)}; // 將視頻幀解壓縮爲 32 位 BGRA 格式
    AVAssetReaderTrackOutput *trackout = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:readerOutputSettings];
    [self.assetReader addOutput:trackout];
    [self.assetReader startReading];// 作好從 AVAsset 讀取樣本的準備,若是返回 NO 則代表出錯了
複製代碼
  • 進行媒體資源寫入準備
self.assetWriter = [[AVAssetWriter alloc] initWithURL:[self outputURL] fileType:AVFileTypeQuickTimeMovie error:nil]; // 指定待寫入的 URL 和 媒體類型
    NSDictionary *writeOutputSettings = @{AVVideoCodecKey:AVVideoCodecH264, // 視頻格式
                                          AVVideoWidthKey:@1280,
                                          AVVideoHeightKey:@720,
                                          AVVideoCompressionPropertiesKey:@{ // 硬編碼參數
                                                  AVVideoAverageBitRateKey:@10500000,
                                                  AVVideoProfileLevelKey:AVVideoProfileLevelH264Main31
                                                  }
                                          };
    AVAssetWriterInput *writerInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:writeOutputSettings];
    [self.assetWriter addInput:writerInput];
    [self.assetWriter startWriting]; // 作好寫入準備
複製代碼
  • 以拉模式寫入
dispatch_queue_t queue = dispatch_queue_create("writer", NULL); // 在串行隊列上寫入
    [self.assetWriter startSessionAtSourceTime:kCMTimeZero]; // 從視頻起始位置開始寫入會話
    [writerInput requestMediaDataWhenReadyOnQueue:queue usingBlock:^{ // 準備寫入更多樣本時調用 block
        BOOL complete = NO;
        while ([writerInput isReadyForMoreMediaData] && !complete) {
            CMSampleBufferRef sampleBuffer = [trackout copyNextSampleBuffer]; // 從讀取器讀取更多樣本
            if (sampleBuffer) {
                BOOL result = [writerInput appendSampleBuffer:sampleBuffer]; // 將樣本附加到寫入器通道
                CFRelease(sampleBuffer);
                complete = !result;
            } else {
                [writerInput markAsFinished]; // 標記寫入完成
                complete = YES;
            }
        }
        
        if (complete) {
            [self.assetWriter finishWritingWithCompletionHandler:^{ // 完成寫入
                if (self.assetWriter.status == AVAssetWriterStatusCompleted) { // 判斷是否寫入成功
                    NSLog(@"complete");
                } else {
                    NSLog(@"fail");
                }
            }];
        }
    }];
複製代碼

2. 建立音頻波形視圖

音頻波形視圖即提供圖像化顯示的音頻波形,方便用戶查看和編輯音頻軌道。其主要步驟包括性能

  • 讀取:解壓讀取音頻數據
  • 採樣:實際讀取到的樣本數量巨大,須要進行採樣,在每個樣本塊上取 min、max 或者 average 值
  • 渲染:在 UI 界面上渲染獲得的採樣值(與 AVFoundation 無關)

2.1 讀取音頻樣本

首先是對讀取器和讀取通道的初始化優化

NSError *error = nil;
    
    AVAssetReader *assetReader = [[AVAssetReader alloc] initWithAsset:asset error:&error]; // 配置 AVAssetReader
    if (!assetReader) {
        NSLog(@"Error creating asset reader: %@", [error localizedDescription]);
        return nil;
    }
    AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeAudio] firstObject]; // 配置音頻軌道
    NSDictionary *outputSettings = @{
        AVFormatIDKey               : @(kAudioFormatLinearPCM), // 讀取格式爲 PCM,一種未壓縮的音頻樣本格式
        AVLinearPCMIsBigEndianKey   : @NO, // 小端字節順序
		AVLinearPCMIsFloatKey		: @NO, // 有符號整型
		AVLinearPCMBitDepthKey		: @(16) // 位元深度 16 位
    };
    AVAssetReaderTrackOutput *trackOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:outputSettings];
    [assetReader addOutput:trackOutput];
    [assetReader startReading];
複製代碼

而後循環讀取樣本值並轉移ui

NSMutableData *sampleData = [NSMutableData data];
    while (assetReader.status == AVAssetReaderStatusReading) { // 持續讀取樣本
        CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer]; // 讀取一個音頻樣本
        if (sampleBuffer) {
            CMBlockBufferRef blockBufferRef = CMSampleBufferGetDataBuffer(sampleBuffer); // 獲取其對應的不保留引用的 block buffer
            size_t length = CMBlockBufferGetDataLength(blockBufferRef); // 獲取樣本長度
            SInt16 sampleBytes[length];
            CMBlockBufferCopyDataBytes(blockBufferRef, 0, length, sampleBytes); // 轉移樣本內的數據到一個空數組
            [sampleData appendBytes:sampleBytes length:length]; // 添加數組到 NSMutableData 中
            CMSampleBufferInvalidate(sampleBuffer);
            CFRelease(sampleBuffer);// 回收樣本
        }
    }
複製代碼

2.2 採樣音頻樣本

屏幕空間有限,而讀取的音頻樣本很是龐大,所以須要進行採樣。基本思路是將樣本按照必定距離進行分割,在分割出的一個獨立"箱"中找到最大樣本。編碼

- (NSArray *)filteredSamplesForSize:(CGSize)size {
    NSMutableArray *filteredSamples = [[NSMutableArray alloc] init];
    NSUInteger sampleCount = self.sampleData.length / sizeof(SInt16); // 獲取樣本個數,樣本格式是 SInt16,總長度除以單個數據長度
    NSUInteger binSize = sampleCount / size.width; // 獲取一個樣本箱的大小

    SInt16 *bytes = (SInt16 *) self.sampleData.bytes; // 獲取樣本的基地址
    for (NSUInteger i = 0; i < sampleCount; i += binSize) { // 遍歷樣本箱
        SInt16 sampleBin[binSize];
        for (NSUInteger j = 0; j < binSize; j++) { // 遍歷箱內樣本
			sampleBin[j] = CFSwapInt16LittleToHost(bytes[i + j]); // 因爲是按照小端順序讀取,須要經過 CFSwapInt16LittleToHost 方法轉換爲主機的本地字節順序
        }
        SInt16 value = [self maxValueInArray:sampleBin ofSize:binSize]; // 取出樣本箱中最大值
        [filteredSamples addObject:@(value)]; // 添加到採樣數組中
    }
    return filteredSamples;
}
複製代碼

3. 捕捉錄製的高級方法

AVCaptureVideoDataOutput 沒法像 AVCaptureMovieFileOutput 同樣便捷地記錄輸出,它須要用到 AVAssetWriter 方法,可是另外一方面能夠對每一幀數據進行實時處理,所以更爲靈活和強大。使用 AVCaptureVideoDataOutput 記錄媒體資源須要注意如下幾個地方url

  • 初始化過程
  • 實時渲染預覽頁面
  • 記錄媒體輸出

3.1 初始化

初始化 AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput

self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    NSDictionary *outputSettings = @{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)}; // 設置輸出爲 32 位 BGRA 格式
    self.videoDataOutput.videoSettings = outputSettings;
    self.videoDataOutput.alwaysDiscardsLateVideoFrames = NO; // 默認爲 YES,會當即丟棄在 captureOutput:didOutputSampleBuffer:fromConnection:delegate 方法中阻止處理當前捕獲幀的幀,設置爲 NO 能夠給委託方法額外時間處理樣本,可是會帶來性能上的損耗
    [self.videoDataOutput setSampleBufferDelegate:self queue:self.dispatchQueue]; // 將委託回調加入到串行隊列中
    if ([self.captureSession canAddOutput:self.videoDataOutput]) {
        [self.captureSession addOutput:self.videoDataOutput];
    } else {
        return NO;
    }
    
    self.audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];
    [self.audioDataOutput setSampleBufferDelegate:self queue:self.dispatchQueue];
    if ([self.captureSession canAddOutput:self.audioDataOutput]) {
        [self.captureSession addOutput:self.audioDataOutput];
    } else {
        return NO;
    }
複製代碼

3.2 實時渲染預覽頁面

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection {
    if (captureOutput == self.videoDataOutput) { // 判斷是 video 通道
        CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); // 取出像素幀
        CIImage *sourceImage = [CIImage imageWithCVPixelBuffer:imageBuffer options:nil]; // 生成預覽的 UIImage
    }
}
複製代碼

固然實際上不少時候咱們須要對預覽加上濾鏡效果,此時能夠用 CIFilter 實現,首先經過 name 獲取一系列的 CIFilter

+ (NSArray *)filterNames {
    
    return @[@"CIPhotoEffectChrome",
             @"CIPhotoEffectFade",
             @"CIPhotoEffectInstant",
             @"CIPhotoEffectMono",
             @"CIPhotoEffectNoir",
             @"CIPhotoEffectProcess",
             @"CIPhotoEffectTonal",
             @"CIPhotoEffectTransfer"];
}

+ (CIFilter *)filterForDisplayName:(NSString *)displayName {
    for (NSString *name in [self filterNames]) {
        if ([name containsString:displayName]) {
            return [CIFilter filterWithName:name];
        }
    }
    return nil;
}
複製代碼

而後對 UIImage 對象使用 CIFilter

[self.filter setValue:sourceImage forKey:kCIInputImageKey];
	CIImage *filteredImage = self.filter.outputImage;
複製代碼

3.3 記錄媒體輸出

記錄輸出前首先要對 videoInput 和 audioInput 進行初始化

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

        self.assetWriterVideoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:self.videoSettings];
        self.assetWriterVideoInput.expectsMediaDataInRealTime = YES; // 指明輸入應該針對實時性進行優化
        
        UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
		self.assetWriterVideoInput.transform = THTransformForDeviceOrientation(orientation); // 修復 orientation

		NSDictionary *attributes = @{
			(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), // 與 AVCaptureVideoDataOutput 使用的像素格式一致可以保證最大效率
			(id)kCVPixelBufferWidthKey : self.videoSettings[AVVideoWidthKey],
			(id)kCVPixelBufferHeightKey : self.videoSettings[AVVideoHeightKey],
			(id)kCVPixelFormatOpenGLESCompatibility : (id)kCFBooleanTrue
		};
        self.assetWriterInputPixelBufferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc] initWithAssetWriterInput:self.assetWriterVideoInput sourcePixelBufferAttributes:attributes]; //AVAssetWriterInputPixelBufferAdaptor 提供一個優化的 pixelBufferPool
        
		if ([self.assetWriter canAddInput:self.assetWriterVideoInput]) {
			[self.assetWriter addInput:self.assetWriterVideoInput];
		} else {
			NSLog(@"Unable to add video input.");
			return;
		}

        self.assetWriterAudioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:self.audioSettings];
        self.assetWriterAudioInput.expectsMediaDataInRealTime = YES;
        if ([self.assetWriter canAddInput:self.assetWriterAudioInput]) {
            [self.assetWriter addInput:self.assetWriterAudioInput];
        } else {
            NSLog(@"Unable to add audio input.");
        }
複製代碼

這裏要注意,對於初始化 AVAssetWriterInput 和 AVAssetWriterInput 時須要用到的 settings 值,iOS 7 提供了一些便捷方法來獲取

  • recommendedVideoSettingsForAssetWriterWithOutputFileType 獲取指定類型的視頻設置
  • recommendedAudioSettingsForAssetWriterWithOutputFileType 獲取指定類型的音頻設置
if (!self.isWriting) { // 檢測是否正在記錄
        return;
    }
    
    CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer); // 獲取當前幀的描述信息
    CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDesc); // 獲取媒體類型

    if (mediaType == kCMMediaType_Video) {
        CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); // 獲取時間戳
        if (self.firstSample) { // 若是是第一幀,則進行寫入啓動操做
            if ([self.assetWriter startWriting]) {
                [self.assetWriter startSessionAtSourceTime:timestamp]; // 在當前時間戳啓動
            } else {
                NSLog(@"Failed to start writing.");
            }
            self.firstSample = NO;
        }
        
        CVPixelBufferRef outputRenderBuffer = NULL;
        CVPixelBufferPoolRef pixelBufferPool = self.assetWriterInputPixelBufferAdaptor.pixelBufferPool; // 獲取到 adaptor 的 pixelBufferPool
        OSStatus err = CVPixelBufferPoolCreatePixelBuffer(NULL, pixelBufferPool, &outputRenderBuffer); // 建立一個空的 CVPixelBufferRef,使用該 buffer 渲染篩選好的視頻幀
        if (err) {
            NSLog(@"Unable to obtain a pixel buffer from the pool.");
            return;
        }

        CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); // 獲取當前樣本的像素幀
        CIImage *sourceImage = [CIImage imageWithCVPixelBuffer:imageBuffer options:nil]; // 生成 CIImage
        [self.activeFilter setValue:sourceImage forKey:kCIInputImageKey]; // 濾鏡配置
        CIImage *filteredImage = self.activeFilter.outputImage;

        if (!filteredImage) {
            filteredImage = sourceImage;
        }

        [self.ciContext render:filteredImage toCVPixelBuffer:outputRenderBuffer bounds:filteredImage.extent colorSpace:self.colorSpace]; // 將 CIImage 渲染到空的 CVPixelBufferRef 中

        if (self.assetWriterVideoInput.readyForMoreMediaData) {
            if (![self.assetWriterInputPixelBufferAdaptor appendPixelBuffer:outputRenderBuffer withPresentationTime:timestamp]) { // 寫入到 adaptor 中,完成對視頻幀的處理
                NSLog(@"Error appending pixel buffer.");
            }
        }
        
        CVPixelBufferRelease(outputRenderBuffer); // 回收被渲染的幀
    }
    else if (!self.firstSample && mediaType == kCMMediaType_Audio) {
        if (self.assetWriterAudioInput.isReadyForMoreMediaData) {
            if (![self.assetWriterAudioInput appendSampleBuffer:sampleBuffer]) { // 音頻樣本直接寫入
                NSLog(@"Error appending audio sample buffer.");
            }
        }
    }
複製代碼

具體的操做已經在註釋中寫明瞭,此處再也不過多說明。最終結束寫入操做時,仍然須要調用 finishWritingWithCompletionHandler 方法

[self.assetWriter finishWritingWithCompletionHandler:^{
            if (self.assetWriter.status == AVAssetWriterStatusCompleted) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    NSURL *fileURL = [self.assetWriter outputURL]; // 拿到 url 後進行相冊保存操做
                });
            } else {
                NSLog(@"Failed to write movie: %@", self.assetWriter.error);
            }
        }];
複製代碼
相關文章
相關標籤/搜索