AVFoundation開發祕籍筆記-08讀取與寫入媒體
一、綜述
AVFoundation定義了一組功能可以用於建立媒體應用程式時遇到的大部分用例場景。
還有一些功能不受AVFoundation框架的內建支援,需要使用框架的AVAssetReader
和AVAssetWriter
類提供的低階功能,可以直接處理媒體樣本。
1、AVAssetReader
用於從AVAsset中讀取媒體樣本,通常會配置一個或多個AVAssetReaderOutput
例項,並通過copyNextSampleBuffer
方法訪問音訊樣本和視訊幀。
AVAssetReaderOutput是一個抽象類,不過框架定義了具體例項來從指定的AVAssetTrack中讀取解碼的媒體樣本,從多音訊軌道中讀取混合輸出,或者從多視訊軌道總讀取組合輸出。
AVAssetReaderAudioMixOutput
AVAssetReaderTrackOutput
AVAssetReaderVideoCompositionOutput
AVAssetReaderSampleReferenceOutput
一個資源讀取器內部通道都是以多執行緒的方式不斷提取下一個可用樣本的,這樣可以在系統請求資源時最小化時延。儘管提供了低時延的檢索操作,還是不傾向於實時操作,比如播放。
AVAssetReader只針對於帶有一個資源的媒體樣本,如果需要同時從多個基於檔案的資源中讀取樣本,可將它們組合到一個AVAsset子類AVComposition中。
NSURL *fileUrl ;
AVAsset *asset = [AVAsset assetWithURL:fileUrl];
AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
NSError *serror;
self.assetReader = [[AVAssetReader alloc] initWithAsset:asset error:&serror];
NSDictionary *readerOutputSetting = @{(id)kCVPixelBufferPixelFormatTypeKey :@(kCVPixelFormatType_32BGRA)};
AVAssetReaderTrackOutput *trackOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:readerOutputSetting];
//從資源視訊軌道中讀取樣本,將視訊幀解壓縮為BGRA格式。
if ([self.assetReader canAddOutput:trackOutput]) {
[self.assetReader addOutput:trackOutput];
}
[self.assetReader startReading];
2、AVAssetWriter
對媒體資源進行編碼並將其寫入到容器檔案中,日服一個MPEG-4檔案或一個QuickTime檔案。
它由一個或多個AVAssetWriterInput
物件配置,用於附加將包含要寫入容器的媒體樣本的CMSampleBuffer
物件。
AVAssetWriterInput
被配置為可以處理指定的媒體型別,比如音訊或視訊,並且附加在其後的樣本會在最終輸出時生成一個獨立的AVAssetTrack
。當使用一個配置了處理視訊樣本的AVAssetWriterInput
時,會常用到一個專門的介面卡物件AVAssetWriterInputPixelBufferAdaptor
,這個類在附加被包裝為CVPixelBuffer物件的視訊樣本時提供最優效能。
輸入資訊也可以通過使用AVAssetWriterInputGroup
組成互斥的引數,可以建立特定資源,包含在播放時使用AVMediaSelectionGroup
和AVMediaSelectionOption
類選擇的指定語言媒體軌道。
AVAssetWriter可以自動支援交叉媒體樣本。AVAssetWriterInput提供一個readyForMoreMediaData屬性來指示在保持所需的交錯情況下輸入資訊是否還可以附加更多資料,只有在這個屬性值為YES時才可以將一個新的樣本新增到輸入資訊中。
AVAssetWriter可用於實時操作和離線操作兩種情況。對於每個場景中都有不同的方法將樣本buffer新增到寫入物件的輸入中。
- 實時:處理實時資源時,比如從AVCaptureVideoDataOutput寫入捕捉的樣本時,AVAssetWriter應該另expectsMediaDataInRealTime為YES來確保readyForMoreMediaData值被正確計算。從實時資源寫入資料優化了寫入器,與維持理想交錯效果相比,快速寫入樣本具有更高的優先順序。
- 離線:當從離線資源中讀取媒體資源時,比如從AVAssetReader讀取樣本buffer,在附加樣本前仍需寫入器輸入的readyForMoreMediaData屬性的狀態,不過可以使用
requestMediaDataWhenReadyOnQueue:usingBlock:
方法控制資料的提供。傳到這個方法中的程式碼塊會隨寫入器輸入準備附加更多的樣本而不斷被呼叫。新增樣本時需要檢索資料並從資源中找到下一個樣本進行新增。
NSURL *outputUrl ;
NSError *wError;
self.assetWriter = [[AVAssetWriter alloc] initWithURL:outputUrl fileType:AVFileTypeQuickTimeMovie error:&wError];
NSDictionary *writerOutputSettings =
@{
AVVideoCodecKey:AVVideoCodecH264,
AVVideoWidthKey:@1280,
AVVideoHeightKey:@720,
AVVideoCompressionPropertiesKey:@{
AVVideoMaxKeyFrameIntervalKey:@1,
AVVideoAverageBitRateKey:@10500000,
AVVideoProfileLevelKey:AVVideoProfileLevelH264Main31,
}
};
AVAssetWriterInput *writerInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:writerOutputSettings];
if ([self.assetWriter canAddInput:writerInput]) {
[self.assetWriter addInput:writerInput];
}
[self.assetWriter startWriting];
與AVAssetExportSession相比,AVAssetWriter明顯的優勢是它對輸出進行編碼時能夠進行更加細緻的壓縮設定控制。可以指定關鍵幀間隔、視訊位元率、H.264配置檔案、畫素寬高比和純淨光圈等設定。
3、示例,從非實時資源中寫入樣本
dispatch_queue_t dispatchQueue = dispatch_queue_create("com.writerQueue", NULL);
[self.assetWriter startSessionAtSourceTime:kCMTimeZero];
//建立一個新的寫入會話,傳遞資源樣本的開始時間。
/**
在寫入器輸入準備好新增更多樣本時,被不斷呼叫。
每次呼叫期間,輸入準備新增更多資料時,再從軌道的輸出中複製可用的樣本,並附加到輸入中。
所有樣本從軌道輸出中複製後,標記AVAssetWriterInput已經結束並指明新增操作已完成。
**/
[writerInput requestMediaDataWhenReadyOnQueue:dispatchQueue usingBlock:^{
BOOL complete = NO ;
while ([writerInput isReadyForMoreMediaData] && !complete) {
CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer];
if (sampleBuffer) {
BOOL result = [writerInput appendSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
complete = !result;
} else {
[writerInput markAsFinished];
complete = YES;
}
}
if (complete) {
[self.assetWriter finishWritingWithCompletionHandler:^{
AVAssetWriterStatus status = self.assetWriter.status;
if (status == AVAssetWriterStatusCompleted) {
//
} else {
}
}];
}
}];
二、建立音訊波形(waveform)檢視
繪製波形三個步驟:
- 1、讀取,讀取音訊樣本進行渲染。需要讀取或可能解壓縮音訊資料。
- 2、縮減,實際讀取到的樣本數量要遠比在螢幕上渲染的多。縮減過程必須作用域樣本集,這一過程包括樣本總量分為小的樣本塊,並在每個樣本塊上找到最大的樣本、所有樣本的平均值或min/max值。
- 3、渲染,將縮減後的樣本呈現在螢幕上。通常用到Quartz框架,可以使用蘋果支援的繪圖框架。如何繪製這些資料的型別取決於如何縮減樣本的。採用min/max對,怎為它的每一對繪製一條垂線。如果使用每個樣本塊平均值或最大值,使用Quartz Bezier路徑繪製波形。
1、讀取音訊樣本 –提取全部樣本集合
- 1、載入AVAsset資源軌道資料
- 2、載入完成之後,建立
AVAssertReader
,並配置AVAssetReaderTrackOutput
- 3、AVAssertReader讀取資料,並將讀取到的樣本資料新增到NDSdata例項後面。
+ (void)loadAudioSamplesFromAsset:(AVAsset *)asset
completionBlock:(THSampleDataCompletionBlock)completionBlock {
// Listing 8.2
NSString *tracks = @"tracks";
[asset loadValuesAsynchronouslyForKeys:@[tracks] completionHandler:^{
AVKeyValueStatus status = [asset statusOfValueForKey:tracks error:nil];
NSData *sampleData = nil;
if (status == AVKeyValueStatusLoaded) { //資源已經載入完成
sampleData = [self readAudioSamplesFromAsset:asset];
}
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock(sampleData);
});
}];
}
+ (NSData *)readAudioSamplesFromAsset:(AVAsset *)asset {
// Listing 8.3
NSError *error = nil;
AVAssetReader *assetReader = [[AVAssetReader alloc] initWithAsset:asset error:&error];
//建立一個AVAssetReader例項,並賦給他一個資源讀取。
if (!assetReader) {
NSLog(@"error creating asset reader :^%@",error);
return nil;
}
AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeAudio] firstObject];
//獲取資源找到的第一個音訊軌道,根據期望的媒體型別獲取軌道。
NSDictionary *outputSettings =
@{
AVFormatIDKey:@(kAudioFormatLinearPCM),//樣本需要以未壓縮的格式被讀取
AVLinearPCMIsBigEndianKey:@NO,
AVLinearPCMIsFloatKey:@NO,
AVLinearPCMBitDepthKey:@(16)
};
//建立NSDictionary儲存從資源軌道讀取音訊樣本時使用的解壓設定。
AVAssetReaderTrackOutput *trackOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:outputSettings];
if ([assetReader canAddOutput:trackOutput]) {
[assetReader addOutput:trackOutput];
}
//建立新的AVAssetReaderTrackOutput例項,將建立的輸出設定傳遞給它,
//將其作為AVAssetReader的輸出並呼叫startReading來允許資源讀取器開始預收取樣本資料。
[assetReader startReading];
NSMutableData *sampleData = [NSMutableData data];
while (assetReader.status == AVAssetReaderStatusReading) {
CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer];
//呼叫跟蹤輸出的方法開始迭代,每次返回一個包含音訊樣本的下一個可用樣本buffer。
if (sampleBuffer) {
CMBlockBufferRef blockBufferRef = CMSampleBufferGetDataBuffer(sampleBuffer);
//CMSampleBuffer中的音訊樣本包含在一個CMBlockBuffer型別中
//CMSampleBufferGetDataBuffer函式可以方位block buffer
size_t length = CMBlockBufferGetDataLength(blockBufferRef);
SInt16 sampleBytes[length];
//確定長度並建立一個16位帶符號整型陣列來儲存音訊樣本
CMBlockBufferCopyDataBytes(blockBufferRef, 0, length, sampleBytes);
//生成一個數組,陣列中元素為CMBlockBuffer所包含的資料
[sampleData appendBytes:sampleBytes length:length];
//將陣列資料內容附加在NDSData例項後面。
CMSampleBufferInvalidate(sampleBuffer);
//指定樣本buffer已經處理和不可再繼續使用
CFRelease(sampleBuffer);
//釋放CMSampleBuffer副本來釋放內容
}
}
if (assetReader.status == AVAssetReaderStatusCompleted) {
//資料讀取成功,返回包含音訊樣本資料的NData
return sampleData;
} else {
NSLog(@"Failed to read audio samples from asset");
return nil;
}
return nil;
}
2、縮減音訊樣本
根據指定壓縮空間,壓縮樣本。即將,總樣本分塊,取每塊子樣本最大值,重新組成新的音訊樣本集合。
//指定尺寸約束篩選資料集
- (NSArray *)filteredSamplesForSize:(CGSize)size {
NSMutableArray *filterDataSamples = [[NSMutableArray alloc] init];
NSUInteger sampleCount = self.sampleData.length/sizeof(SInt16);
//樣本總長度
NSUInteger binSize = sampleCount/size.width;
//子樣本長度
SInt16 *bytes = (SInt16 *)self.sampleData.bytes;
SInt16 maxSample = 0;
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];
[filterDataSamples addObject:@(value)];
//找到樣本最大絕對值。
if (value > maxSample) {
maxSample = value;
}
}
CGFloat scaleFactor = (size.height/2) / maxSample;
//所有樣本中的最大值,計算篩選樣本使用的比例因子
for (NSUInteger i = 0; i < filterDataSamples.count; i ++) {
filterDataSamples[i] = @([filterDataSamples[i] integerValue] *scaleFactor);
}
return filterDataSamples;
}
- (SInt16)maxValueInArray:(SInt16[])values ofSize:(NSUInteger)size {
SInt16 maxValue = 0;
for (int i = 0; i < size; i ++) {
if (abs(values[i]) > maxValue) {
maxValue = abs(values[i]);
}
}
return maxValue;
}
3、渲染音訊樣本
將篩選出來的音訊樣本資料,繪製成波形圖。這裡使用Quartz的Bezier繪製。
- (void)setAsset:(AVAsset *)asset {
if (_asset != asset) {
_asset = asset;
[THSampleDataProvider loadAudioSamplesFromAsset:asset completionBlock:^(NSData *sampleData) {
self.filter = [[THSampleDataFilter alloc] initWithData:sampleData];
[self.loadingView stopAnimating];
[self setNeedsDisplay];
}];
}
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
//在檢視內呈現這個波形,首先基於定義的寬和高常量來縮放影象上下文
CGContextScaleCTM(context, THWidthScaling, THHeightScaling);
//計算x,y偏移量,轉換上下文,在縮放上下文中適當調整便宜
CGFloat xOffset = self.bounds.size.width-self.bounds.size.width*THWidthScaling;
CGFloat yOffset = self.bounds.size.height-self.bounds.size.height*THHeightScaling;
CGContextTranslateCTM(context, xOffset/2, yOffset/2);
//獲取篩選樣本,並傳遞檢視邊界的尺寸。
//實際可能希望在drawRect方法之外執行這一檢索操作,這樣在篩選樣本時會有更好的優化效果
NSArray *filteredSamples = [self.filter filteredSamplesForSize:self.bounds.size];
CGFloat midY = CGRectGetMidY(rect);
//建立一個新的CGMutablePathRef,用來繪製波形Bezier路徑的上半部
CGMutablePathRef halfPath = CGPathCreateMutable();
CGPathMoveToPoint(halfPath, NULL, 0.0f, midY);
for (NSUInteger i = 0; i < filteredSamples.count; i ++) {
float sample = [filteredSamples[i] floatValue];
//每次迭代,向路徑中新增一個點,索引i作為x座標,樣本值作為y座標
CGPathAddLineToPoint(halfPath, NULL, i, midY-sample);
}
//建立第二個CGMutablepathRef,是Bezier路徑繪製完整波形
CGPathAddLineToPoint(halfPath, NULL, filteredSamples.count, midY);
CGMutablePathRef fullPath = CGPathCreateMutable();
CGPathAddPath(fullPath, NULL, halfPath);
//要繪製波形下半部,需要對上半部路徑應用translate和scale變化,是的上半部路徑翻轉到下面,填滿整個波形
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, 0, CGRectGetHeight(rect));
transform = CGAffineTransformScale(transform, 1.0, -1.0);
CGPathAddPath(fullPath, &transform, halfPath);
//將完整路徑新增到影象上下文,根據指定的waveColor設定填充色。並繪製路徑到影象上下文。
CGContextAddPath(context, fullPath);
CGContextSetFillColorWithColor(context, self.waveColor.CGColor);
CGContextDrawPath(context, kCGPathFill);
//建立Quartz物件,在使用之後釋放相應記憶體。
CGPathRelease(halfPath);
CGPathRelease(fullPath);
}
三、捕捉錄製的高階方法
將AVCaptureVideoDataOutput
捕捉的CVPixelBuffer
物件最為OpenGL ES的貼圖來呈現,這是一個強大的功能,不過使用AVCaptureVideoDataOutput
的一個問題是會失去AVCaptureMovieFileOutput
來記錄輸出的便利性。
AVCaptureVideoDataOutput
和AVCaptureAudioDataOutput
如果需要對資料進行更復雜的處理,要為每一個使用單獨的佇列。
1、實現捕捉會話配置
self.captureSession = [[AVCaptureSession alloc] init];
self.captureSession.sessionPreset = AVCaptureSessionPresetMedium;
AVCaptureDevice *videoDevice =
[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *videoInput =
[AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
if (videoInput) {
if ([self.captureSession canAddInput:videoInput]) {
[self.captureSession addInput:videoInput];
self.activeVideoInput = videoInput;
} else {
}
} else {
}
// Setup default microphone
AVCaptureDevice *audioDevice =
[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *audioInput =
[AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:error];
if (audioInput) {
if ([self.captureSession canAddInput:audioInput]) {
[self.captureSession addInput:audioInput];
} else {
}
} else {
}
self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
//設定輸出格式kCVPixelFormatType_32BGRA,結合OpenGL ES和CoreImage時這一格式非常適合。
NSDictionary *outputSettigns = @{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)};
self.videoDataOutput.videoSettings = outputSettigns;
//要記錄輸出內容,所以通常我們希望捕捉全部的可用幀
//設定alwaysDiscardsLateVideoFrames為NO,會給委託方法一些額外的時間來處理樣本buffer
self.videoDataOutput.alwaysDiscardsLateVideoFrames = NO;
[self.videoDataOutput setSampleBufferDelegate:self queue:self.dispatchQueue];
if ([self.captureSession canAddOutput:self.videoDataOutput]) {
[self.captureSession addOutput:self.videoDataOutput];
} else {
NSLog(@"add video data output error");
}
//捕捉音訊樣本
self.audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];
[self.audioDataOutput setSampleBufferDelegate:self queue:self.dispatchQueue];
if ([self.captureSession canAddOutput:self.audioDataOutput]) {
[self.captureSession addOutput:self.audioDataOutput];
} else {
NSLog(@"add audio data output error");
}
NSString *fileType = AVFileTypeQuickTimeMovie;
NSDictionary *videoSettings = [self.videoDataOutput recommendedVideoSettingsForAssetWriterWithOutputFileType:fileType];
NSDictionary *audioSettings = [self.audioDataOutput recommendedAudioSettingsForAssetWriterWithOutputFileType:fileType];
self.movieWriter = [[THMovieWriter alloc] initWithVideoSettings:videoSettings audioSettings:audioSettings dispatchQueue:self.dispatchQueue];
self.movieWriter.delegate = self;
儲存視訊到相簿
- (void)didWriteMovieAtURL:(NSURL *)outputURL {
// Listing 8.17
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:outputURL]) {
//檢驗是否可以寫入
ALAssetsLibraryWriteVideoCompletionBlock completionBlock;
completionBlock = ^(NSURL *assetURL, NSError *error) {
if (error) {
[self.delegate assetLibraryWriteFailedWithError:error];
} else {
}
};
[library writeVideoAtPathToSavedPhotosAlbum:outputURL completionBlock:completionBlock];
}
}
2、代理回撥方法處理
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
//處理視訊幀,並寫入
[self.movieWriter processSampleBuffer:sampleBuffer];
// Listing 8.11
if (captureOutput == self.videoDataOutput) {
//獲取基礎CVPixelBuffer
CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
//從CVPixelBuffer中建立一個新的CIImage,並將它傳遞給需要在螢幕上呈現的圖片目標
CIImage *sourceImage = [CIImage imageWithCVPixelBuffer:imageBuffer options:nil];
//將圖片在preview上展示,這個時候可以對圖片做相關處理。加濾鏡的內容後面再加。
[self.imageTarget setImage:sourceImage];
}
}
3、建立檔案寫入
建立一個物件,通過AVAssetWriter執行視訊編碼和檔案寫入。
將功能封裝
.h
#import <AVFoundation/AVFoundation.h>
@protocol THMovieWriterDelegate <NSObject>
- (void)didWriteMovieAtURL:(NSURL *)outputURL;
@end
@interface THMovieWriter : NSObject
/**
* 例項化,
* videoSettings,audioSettings兩個字典用來描述基礎AVAssetWriter的配置引數
* dispatchQueue 排程佇列
*/
- (id)initWithVideoSettings:(NSDictionary *)videoSettings
audioSettings:(NSDictionary *)audioSettings
dispatchQueue:(dispatch_queue_t)dispatchQueue;
/**
* 寫入程序開始
*/
- (void)startWriting;
/**
* 寫入程序停止
*/
- (void)stopWriting;
/**
* 工作狀態監聽
*/
@property (nonatomic) BOOL isWriting;
/**
* 定義委託協議,監聽寫入磁碟時間
*/
@property (weak, nonatomic) id<THMovieWriterDelegate> delegate;
/**
* 捕捉到新的樣本,呼叫這個方法
*/
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer;
.m檔案
#import "THMovieWriter.h"
#import <AVFoundation/AVFoundation.h>
#import "THContextManager.h"
#import "THFunctions.h"
#import "THPhotoFilters.h"
#import "THNotifications.h"
static NSString *const THVideoFilename = @"movie.mov";
@interface THMovieWriter ()
@property (strong, nonatomic) AVAssetWriter *assetWriter;
@property (strong, nonatomic) AVAssetWriterInput *assetWriterVideoInput;
@property (strong, nonatomic) AVAssetWriterInput *assetWriterAudioInput;
@property (strong, nonatomic)
AVAssetWriterInputPixelBufferAdaptor *assetWriterInputPixelBufferAdaptor;
@property (strong, nonatomic) dispatch_queue_t dispatchQueue;
@property (weak, nonatomic) CIContext *ciContext;
@property (nonatomic) CGColorSpaceRef colorSpace;
@property (strong, nonatomic) CIFilter *activeFilter;
@property (strong, nonatomic) NSDictionary *videoSettings;
@property (strong, nonatomic) NSDictionary *audioSettings;
@property (nonatomic) BOOL firstSample;
@end
@implementation THMovieWriter
- (id)initWithVideoSettings:(NSDictionary *)videoSettings
audioSettings:(NSDictionary *)audioSettings
dispatchQueue:(dispatch_queue_t)dispatchQueue {
self = [super init];
if (self) {
// Listing 8.13
_videoSettings = videoSettings;
_audioSettings = audioSettings;
_dispatchQueue = dispatchQueue;
//得到Core Image上下文,這個物件受OpenGL ES的支援,並用於篩選傳進來的視訊樣本
//最後得到一個VCPixelBuffer
_ciContext = [THContextManager sharedInstance].ciContext;
_colorSpace = CGColorSpaceCreateDeviceRGB();
_activeFilter = [THPhotoFilters defaultFilter];
_firstSample = YES;
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
//切換濾鏡通知監聽器
[nc addObserver:self selector:@selector(filterChanged:) name:THFilterSelectionChangedNotification object:nil];
}
return self;
}
- (void)dealloc {
// Listing 8.13
CGColorSpaceRelease(_colorSpace);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)filterChanged:(NSNotification *)notification {
// Listing 8.13
self.activeFilter = [notification.object copy];
}
- (void)startWriting {
// Listing 8.14
//開始錄影,避免卡頓,非同步排程到dispatchQueue佇列,設定AVAssetWriter物件
dispatch_async(self.dispatchQueue, ^{
NSError *error = nil;
NSString *fileType = AVFileTypeQuickTimeMovie;
//建立新的AVAssetWriter例項
self.assetWriter = [AVAssetWriter assetWriterWithURL:[self outputURL]
fileType:fileType
error:&error];
if (!self.assetWriter || error) {
NSLog(@"Could not create AVAssetWriter: %@",error);
return ;
}
//建立一個新的AVAssetWriterInput,附加從AVCaptureVideoDataOutput中得到的樣本
self.assetWriterVideoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:self.videoSettings];
//設定YES指明這個輸入針對實時性進行優化
self.assetWriterVideoInput.expectsMediaDataInRealTime = YES;
//判斷使用者介面方向,為輸入設定一個合適的轉換。
//寫入會話期間,方向會按照這一設定保持不變。
UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
self.assetWriterVideoInput.transform = THTransformForDeviceOrientation(orientation);
NSDictionary *attributes =
@{
(id)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA),
(id)kCVPixelBufferWidthKey:self.videoSettings[AVVideoWidthKey],
(id)kCVPixelBufferHeightKey:self.videoSettings[AVVideoHeightKey],
(id)kCVPixelFormatOpenGLESCompatibility:(id)kCFBooleanTrue,
};
//建立AVAssetWriterInputPixelBufferAdaptor
//提供了一個優化的CVPixelBufferPool,使用它可以建立CVPixelBuffer物件來渲染篩選視訊幀。
self.assetWriterInputPixelBufferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc] initWithAssetWriterInput:self.assetWriterVideoInput sourcePixelBufferAttributes:attributes];
if ([self.assetWriter canAddInput:self.assetWriterVideoInput]) {
[self.assetWriter addInput:self.assetWriterVideoInput];
} else {
NSLog(@"Unable to add video input");
return ;
}
//建立AVAssetWriterInput附加AVCaptureAudioDataOutput樣本
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");
return;
}
self.isWriting = YES;
self.firstSample = YES;
});
}
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer {
// Listing 8.15
if (!self.isWriting) {
return ;
}
//這個方法可以處理音訊和視訊兩種樣本,所以需要確定樣本的媒體型別才能附加到正確的寫入器輸入。
//檢視buffer的CMFormatDescription
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
//使用CMFormatDescriptionGetMediaType判斷媒體型別
CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDesc);
if (mediaType == kCMMediaType_Video) {
CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
//如果開始捕捉後,正在處理的是第一個視訊樣本
//呼叫資源寫入器的startWriting啟動一個新的寫入會話
//startSessionAtSourceTime: 將樣本呈現時間作為源時間傳遞到方法中。
if (self.firstSample) {
if ([self.assetWriter startWriting]) {
[self.assetWriter startSessionAtSourceTime:timestamp];
} else {
NSLog(@"failed to start writing");
}
self.firstSample = NO;
}
//從畫素buffer介面卡池中建立一個空的CVPixelBuffer
//使用該畫素buffer渲染篩選好的視訊幀的輸出
CVPixelBufferRef outputRenderBuffer = NULL;
CVPixelBufferPoolRef pixelBufferPool = self.assetWriterInputPixelBufferAdaptor.pixelBufferPool;
OSStatus err = CVPixelBufferPoolCreatePixelBuffer(NULL, pixelBufferPool, &outputRenderBuffer);
if (err) {
NSLog(@"Unable to obtain a pixel buffer from thr pool.");
return ;
}
//獲取當前視訊樣本的CVPixelBuffer
CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
//根據畫素buffer窗機啊一個新的CIImage並將他設定為篩選器的kCIInputImageKey值。
CIImage *sourceImage =[CIImage imageWithCVPixelBuffer:imageBuffer options:nil];
[self.activeFilter setValue:sourceImage forKey:kCIInputImageKey];
//通過篩選器得到輸出圖片,會返回一個封裝了CIFilter操作的CIImage物件
CIImage *filterImage = self.activeFilter.outputImage;
if (!filterImage) {
filterImage = sourceImage;
}
//將篩選好的CIImage的輸出渲染到outputRenderBuffer
[self.ciContext render:filterImage toCVPixelBuffer:outputRenderBuffer bounds:filterImage.extent colorSpace:self.colorSpace];
if (self.assetWriterVideoInput.readyForMoreMediaData) {
//如果輸入的readyForMoreMediaData為YES
//將畫素buffer連同當前樣本的時間附加到AVAssetWriterPixelBUfferAdaptor。
if (![self.assetWriterInputPixelBufferAdaptor appendPixelBuffer:outputRenderBuffer withPresentationTime:timestamp]) {
NSLog(@"Error Appending pixel buffer.");
}
}
//完成對當前視訊樣本的處理,釋放畫素buffer
CVPixelBufferRelease(outputRenderBuffer);
} else if (!self.firstSample && mediaType == kCMMediaType_Audio) {
//如果第一個樣本處理完成並且當前的CMSampleBuffer是一個音訊樣本。
if (self.assetWriterAudioInput.isReadyForMoreMediaData) {
if (![self.assetWriterAudioInput appendSampleBuffer:sampleBuffer]) {
NSLog(@"Error appending audio sample buffer");
}
}
}
}
- (void)stopWriting {
// Listing 8.16
//設定為NO,processSampleBuffer:mediaType:就不會再處理更多的樣本
self.isWriting = NO;
dispatch_async(self.dispatchQueue, ^{
//終止寫入會話並關閉磁碟上的檔案
[self.assetWriter finishWritingWithCompletionHandler:^{
//判斷資源寫入器狀態
if (self.assetWriter.status == AVAssetWriterStatusCompleted) {
//回撥到主執行緒,呼叫委託的方法。
dispatch_async(dispatch_get_main_queue(), ^{
NSURL *fileUrl = [self.assetWriter outputURL];
[self.delegate didWriteMovieAtURL:fileUrl];
//回撥 儲存到相簿
});
} else {
NSLog(@"Failed to write movie: %@",self.assetWriter.error);
}
}];
});
}
// 定義outPutUrl配置AVAssetWriter例項。
- (NSURL *)outputURL {
NSString *filePath =
[NSTemporaryDirectory() stringByAppendingPathComponent:THVideoFilename];
NSURL *url = [NSURL fileURLWithPath:filePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:url.path]) {
[[NSFileManager defaultManager] removeItemAtURL:url error:nil];
}
return url;
}
@end
通過AVAssetWriter和AVAssetReader實現視訊檔案的讀去和寫入,同時可以再錄製過程中對視訊進行更多可擴充套件性的處理。
書中的示例中實現,濾鏡視訊的錄製處理,使用到CoreImage對圖片處理,後面也要學習這方面的內容。
這一節只是熟悉AVAssetWriter和AVAssetReader的基本用法,有所瞭解,它們還有更多更深入的功能,後期需要更多的時間去學習。
相關推薦
AVFoundation開發祕籍筆記-08讀取與寫入媒體
一、綜述 AVFoundation定義了一組功能可以用於建立媒體應用程式時遇到的大部分用例場景。 還有一些功能不受AVFoundation框架的內建支援,需要使用框架的AVAssetReader和AVAssetWriter類提供的低階功能,可以直接處理媒體樣
iOS開發WKWebView Cookie的讀取與寫入,與UIWebView的Cookie共享
conf 網絡請求 err trie 引入 mes article app fetch NSHTTPCookieStorage和NSHttpCookie NSHTTPCookieStorage 實現了一個管理Cookie的單例對象(只有一個實例),每個Cookie都是NSH
AVFoundation開發祕籍筆記-06捕捉媒體
一、捕捉功能 1、捕捉會話 AVCaptureSession AVFoundation捕捉棧的核心類是AVCaptureSession。一個捕捉會話相當於一個虛擬的“插線板”,用於連線輸入和輸出的資源。 捕捉會話管理從屋裡裝置得到的資料流,比如攝像頭和
AVFoundation開發祕籍筆記-03資源和元資料
一、資源AVAsset AVAsset是一個抽象類和不可變類,定義媒體資源混合呈現的方式,將媒體資源的靜態屬性模組化成為一個整體,比如標題、時長和元資料等。 AVAsset不需要考慮媒體資源所具有的兩個重要範疇:1、提供了對基本媒體格式的層抽象,不需要關注具
Revit二次開發—引數的讀取與寫入
注:本文轉自公眾號:BIMCoder樑老師 一、前言 在Revit二次開發中,引數是非常重要的組成部分,那麼我們該如何從構件獲取引數並修改該引數。 二、方法 首先得到一個Element後,有如下
java文件讀取與寫入
文件 public color exc cnblogs 循環 pack delet 根據 package com.myjava; import java.io.*; import java.util.ArrayList; import java.util.Collect
Java中XML文件的讀取與寫入
讀取 聯系 過程 樹形 樹形結構 java 以及 ade 文件的 表現:以 “.xml”為文件擴展名的文件; 存儲:樹形結構; 用途:存儲以及傳遞信息;利用相同的XML文件將不同的系統聯系起來; 在Java程序中如何獲取XML文件的內容? 在Java程序中讀取XML文
c++對txt文件的讀取與寫入
lin 一個 離開 term file 例子 內容 存儲 turn 轉自:http://blog.csdn.net/lh3325251325/article/details/4761575 1 #include <iostream> 2 #incl
Hadoop基礎-HDFS的讀取與寫入過程剖析
簡要介紹 ron data 訪問 如果 上傳數據 4.2 客戶端訪問 文件寫入 Hadoop基礎-HDFS的讀取與寫入過程剖析 作者:尹正傑 版權聲明:原創作品,謝絕轉
byte[]讀取與寫入
FileStream fs1 = new FileStream(@"E:\tenp\doc\111.txt", FileMode.Open, FileAccess.Read, FileShare.Read); FileStream fs2 = new FileStream(@"E:\temp\doc
angular1 開啟檔案 並另存為(檔案的讀取與寫入)
最近有個需求,在頁面上有個按鈕可以選取檔案然後在匯出到其它地方, 說明白點就是檔案的讀取與寫入,下面是例子(例子中用到了fileSave.js github地址:https://github.com/eligrey/FileSaver.js) 首先引入fileSave.js i
檔案內容的讀取與寫入
檔案讀取: <f>.read(【size 】) ----- 如果size未給定或為負,則讀入檔案全部內容,若給出size為正,則讀入前size長度 <f>.readline(【size 】) ----- 如果si
文件內容的讀取與寫入
當前位置 文件操作 全部 字節流 寫入文件 字符串 文件內容 see line 文件讀取: <f>.read(【size 】) ----- 如果size未給定或為負,則讀入文件全部內容,若給出size為正,則讀入前size長度 <f>.read
iOS HTTP網路請求Cookie的讀取與寫入(NSHTTPCookieStorage)
當你訪問一個網站時,NSURLRequest都會幫你主動記錄下來你訪問的站點設定的Cookie,如果 Cookie 存在的話,會把這些資訊放在 NSHTTPCookieStorage 容器中共享,當你下次再訪問這個站點時,NSURLRequest會拿著上次儲存下來了的Cookie繼續去請求。 同樣適
web前端開發學習筆記-03-區域與表格
原課程在這裡:https://www.icourse163.org/learn/BJFU-1003382003?tid=1003609002#/learn/announce 區域與表格 區域 區域標籤div 屬性: id align 對齊方式 sup標籤可以加角標 列表
Scala檔案的讀取與寫入,從控制檯輸入內容
2015年07月07日 16:27:24 kaiseu 閱讀數:15709 標籤: java scala
Unity 檔案讀取與寫入
Resources.LoadAssetAtPath(); 僅限於在編輯器內使用 Build後出來的的所有AssetDatabase.LoadAssetAtPath();的返回值都為null;不建議使用。呼叫路徑為:Assets\Resources\A.FBXRes
Unity3d+Json多物件資料讀取與寫入+JsonUtility實現
這幾天做自己的培訓班的畢業專案,涉及到Json的讀取與寫入,本來想用LitJson的,後來發現5.3以後的版本有自帶的實現,而且很方便,配合System.IO就可以方便的實現,網上這方面資料也不少,但這裡給出更具體的實現,例如Json檔案中不只有一個物件,涉及
Java讀取與寫入圖片檔案
// FileImageInputStream fis = new FileImageInputStream(new File("timg.jpg")); // File
使用python讀取與寫入資料到excel表
讀取資料 # -*- coding: utf-8 -*- # @File : 讀取資料.py # @Date : 2019-01-05 # @Author : 派森帶你學python # 1.xlrd主要是用來讀取excel檔案 import xlrd # 開啟一個工作