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");
}
}];
}
}];
複製代碼
音頻波形視圖即提供圖像化顯示的音頻波形,方便用戶查看和編輯音頻軌道。其主要步驟包括性能
首先是對讀取器和讀取通道的初始化優化
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);// 回收樣本
}
}
複製代碼
屏幕空間有限,而讀取的音頻樣本很是龐大,所以須要進行採樣。基本思路是將樣本按照必定距離進行分割,在分割出的一個獨立"箱"中找到最大樣本。編碼
- (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;
}
複製代碼
AVCaptureVideoDataOutput 沒法像 AVCaptureMovieFileOutput 同樣便捷地記錄輸出,它須要用到 AVAssetWriter 方法,可是另外一方面能夠對每一幀數據進行實時處理,所以更爲靈活和強大。使用 AVCaptureVideoDataOutput 記錄媒體資源須要注意如下幾個地方url
初始化 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;
}
複製代碼
- (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;
複製代碼
記錄輸出前首先要對 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 提供了一些便捷方法來獲取
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);
}
}];
複製代碼