AVFoundation - 读取和写入媒体

1. AVAssetReader, 用于从AVAsset实例中读取媒体样本. 通常会配置一个或多个AVAssetReaderOutput实例, 并通过copyNextSampleBuffer方法访问音频样本和视频帧. AVAssetReaderOutput是一个抽象类, 不过框架定义了3个具体实例来从指定的AVAssetTrack中读取解码的媒体样本, 从多音频轨道中读取混合输出, 或者从多视频轨道中读取组合输出. 一个资源读取器的内部通道都是以多线程的方式不断提取下一个可用样本, 这样可以在系统请求资源时最小化延时. 尽管提供了低时延的检索操作, 还是不倾向于实时操作, 比如播放. AVAssetReader只针对带有一个资源媒体样本, 如果需要同时从多个基于文件的资源中读取样本, 可将他们组合到一个AVAsset子类AVComposition中.

AVAsset *asset = //Asynchoroously loaded video asset

AVAssetTrack *track = [[asset trackWithMediaType:AVMediaTypeVideo] firstObject];

self.assetReader = [[AVAssetReader alloc] initWithAsset:asset error:nil];

NSDictionary *readerOutputSettings = @{kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)};

AVAssetReaderTrackOutput *trackOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:readerOutputSettings];    //从资源的视频轨道中读取样本, 将视频帧解压缩为BGRA格式.

[self.assetReader addOutput:trackOutput];

[self.assetReader startReading];

2. AVAssetWrite, 用于对媒体资源进行编码并将其写入容器文件中. 比如一个MPEG-4文件或一个QuickTime文件. 它由一个或多个AVAssetWriteInput对象配置. 用于附加将要写入容器的媒体样本的CMSampleBuffer对象. AVAssetWriteInput可以被配置为处理指定的媒体类型, 比如音频或视频, 并且附加在其后的样本在最终输出时生成一个独立的AVAssetTrack. 当使用了一个配置处理视频样本的AVAssetWriteInput时, 开发者经常用到一个专门的适配器对象AVAssetWriteInputPixelBufferAdaptor. 这个类在附加被包装为CVPixelBuffer对象的视频样本时提供最优性能. (创建一个AVAssetWriter对象, 传递新文件写入目的地, 并创建一个新的AVAssetWriterInput, 带有相应的媒体类型和输出设置, 以便创建一个720p, H.264格式的视频)

NSURL *outputURL = // destination output URL

self.assetWriter = [[AVAssetWriter alloc] initWithURL:outputURL fileType:AVFileTypeQuickTimeMovie error:nil];

NSDictionary *writeOutputSettings = @{AVVideoCodecKey: AVVideoCodecH264, AVVideoWidthKey: @1280, AVVideoHeightKey:@720, AVVideoCompressionPropertiesKey:@{AVVideoMaxKeyFrameIntervalKey: @1, AVVideoAverageBitRateKey:@10500000, VAVideoProfileLevelKey: AVVideoProfileLevelH264Main31}};

AVAssetWriterInput *writeInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:writeOutputSettings];

[self.assetWriter addInput:writerInput];

[self.assetWriter startWriting];

注意:与AVAssetExportSession相比, AVAssetWriter明显的优势是它对输出进行编码时能够进行更加细致的压缩控制. 可以让开发者指定诸如关键帧间隔, 视频比特率, h.264配置文件, 像素宽高比和纯净光圈等设置.

3. 创建一个新的写入会话来从资源中读取样本并写到新的位置. a: 使用startSessionAtSourceTime方法创建一个新的写入会话, 并传递kCMTimeZero参数作为资源样本的开始时间. 传给requestMediaDataWhenReadyOnQueue:usingBlock方法的代码块添加更多样本时会不断被调用.

dispatch_queue_t dispatchQueue = dispatch_queue_create("com.writequeue", NULL);

[self.assetWrite startSessionAtSourceTime:kCMTimeZero];    //a

[writeInput requestMediaDataWhenReadyOnQueue:dispatchQueue usingBlock:^{

    BOOL complete = NO;

    while ([writeInput isReadyForMoreMediaData] && !complete) {

        CMSampleBuffer sampleBuffer = [trackOutput copyNextSampleBuffer];

        if (sampleBuffer) {

            BOOL result = [writerInput appendSampleBuffer:sampleBuffer];

            CFRelease(sampleBuffer);

            complete = !result;

       }else {

            [writeInput markAsFinished];

            complete = YES;

        }

    }

    if (complete) {

        [self.assetWriter finishWritingWithCompletionHandle:^{

            if (status == AVAssetWriterStatusCompleted) {

                //handle success

            }else{

            }

        }];

    }

}];

4. 创建音频波形图, 绘制波形图的步骤: 1.第一步读取, 读取音频样本进行渲染, 需要读取或可能解压音频数据.(PCM是一种未压缩的音频样本格式)  2. 第二步缩减, 读取到的样本数量远比屏幕上渲染的多, 缩减过程必须作用于这个样本集, 这一过程通常包括将样本总量分为小的样本块, 并在每个样本块上找打最大样本, 所有样本的平均值或min/max值. 3. 第三步渲染, 将缩减后的样本呈现在屏幕上通常用到Quartz框架. 如果采用min/max对, 则为它的每一对绘制一条垂线. 如果使用样本的平均值或最大值, 绘制波形图比较合适.

5. 读取音频样本, a:首先对资源键执行标准的异步载入, 这样访问资源的tracks属性就不会遇到阻碍. b: tracks键成功载入, 调用readAudioSamplesFromAsset从资源音频轨道读取样本. c: 创建一个字典保存从资源轨道读取音频样本时使用的解压设置, 样本需要以未压缩的格式被读取. d: startReading允许资源读取器开始预收取样本数据. e: 调用跟踪输出的copyNextSampleBuffer方法开始每个迭代, 每次都返回一个包含音频样本的下一个可用buffer. f: CMSampleBuffer中的音频样本被包含在一个CMBlockBuffer类型中, 使用CMSampleBufferGetDataBuffer函数可以访问这个block buffer. 使用CMBlockBufferGetDataLenght函数确定其长度并创建一个16位带符号整形数组来保存音频样本. G:使用CMSampleBufferInvalidate函数来指定样本buffer已经处理和不可再继续使用.此外需要释放CMSampleBuffer副本来释放内容.

+ (void)loadAudioSamplesFromAsset:(AVAsset *)asset completionBlock:^(NSData *data){

    NSString *tracks = @"tracks";

    [asset loadValusAsynchronouslyForKeys:@[tracks] completionHandler:^{ //a

        AVKeyValueStatus status = [asset statusOfValueForKey:tracks error:nil];

        NSData *sampleData = nil;

        if (status == AVKeyValueStatusLoaded) {

            sampleData = [self readAudioSamplesFromAsset:asset]; //b

        }

        dispatch_async(dispatch_get_main_queue(), ^{

            completionBlock(sampleData);    

        });

    }];

}

+ (NSData *)readAudioSamplesFromAsset:(AVAsset *)asset {

    NSError *error = nil;

    AVAssetReader *assetReader = [[AVAssetReader alloc] initWithAsset:asset error:&error];

    if (!assetReader) {

        NSLog(@"Error creating asset reader: %@", [error localizedDescription]);

        return nil;

    }

    AVAssetTrack *track = [[asset trackWithMediaType:AVMediaTypeAudio] firstObject];

    NSDictionary *outputSettings = @{AVFormatIDKey : @(kAudioFormatLinearPCM), AVLinearPCMIsBigEndianKey: @NO, AVLinearPCMIsFloatKey: @NO, AVLinearPCMBitDepthKey: @(16)};    //c

    AVAssetReaderTrackOutput *trackOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:outputSettings];

    [assetReader addOutput:trackOutput];

    [assetReader startReading];    //d

    NSMutableData *sampleData = [NSMutableData data];

    while (assetReader.status == AVAssetReaderStatusReading) {

        CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer]; //e

        if (sampleBuffer) {

            CMBlockBufferRef blockBufferRef = CMSampleBufferGetDataBuffer(sampleBuffer);    //f

            size_t length = CMBlockBufferGetDataLength(blockBufferRef);

            SInt16 sampleBytes[length];

            CMBlockBufferCopyDataBytes(blockBufferRef, 0, length, sampleBytes); //G 

            CMSampleBufferInvalidate(sampleBuffer);

            CFRelease(sampleBuffer);

        }

        if (assetReader.status == AVAssetReaderStatusCompleted) {

            return sampleData;

        }else {

            return nil;

        }

    }    

}

6. 缩减音频样本, 上面可以从一个指定的视频资源中提取全部样本集合, 即使非常小的音频文件都可能有十万个样本, 远大于屏幕上进行绘制所需的样本, 需要进行筛选. a: 用带有音频样本信息的NSData来初始化这个实例. b: 按照指定的尺寸约束来筛选数据集.(处理共分为两步: 1.首先将样本分成箱, 找到每个箱里的最大样本. 2.当所有箱都处理完成后, 对传递给filterSamplesForSize:方法的尺寸约束有关样本应用比例因子)

- (id)initWithData:(NSData *)sampleData {        //a

    self = [super init];

    if (self) {

        _sampleData = sampleData;

    }

    return self;

}

- (NSArray *)filteredSampleForSize:(CGSize)size {        //b

    NSMutableArray *filteredSamples = [[NSMutableArray alloc] init];

    NSUInteger sampleCount = self.sampleData.length / sizeof(SInt16);

    NSUInteger binSize = sampleCount / size.width;

    SInt16 *bytes = (SInt16 *)self.sampleData.bytes;

    SInt16 max = 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]);

        }

        SInt16 value = [self maxValueInArray:sampleBin ofSize:binSize];

        [filteredSamples addObject:@(value)];

        if (value > maxSample) {

            maxSample = value;

        }

    }

    for (NSUInteger i =0 ; I <filteredSamples.cout; I++){

        filteredSamples[I] = @([filteredSamples[i] intValue] * scaleFactor);

    }

    return filteredSamples;

}

- (SInt16)maxValueInArray:(SInt16[])values ofSize:(NSUInteger)size {

    SInt16 maxValue = 0;

    for (int I = 0; I < size; I++) {

        if (abs(value[I]) > maxValue) {

            maxValue = abs(values[I]);

        }

    }

    return maxValue;

}

7. 渲染音频样本, a:绘制波形的下半部, 需要对上半部路径应用translate和scale变化. 这会使得上半部路径翻转到下面, 填满整个波形.

@implementation WaveView

- (void)setAsset:(AVAsset *)asset {

    if (_asset != asset) {

        _asset = asset;

        [SampleDataProvider loadAudioSamplesFromAsset:self.asset completionBlock:^{    //载入音频样本

            self.filter = [[SampleDataFilter alloc] initWithData:sampleData];

            [self setNeedsDisplay];

        }];

    }

}

- (void)drawRect:(CGRect)rect {

    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextScaleCTM(context, THWithSCaling, THHeightScaling);

    CGFloat xOffset = self.bounds.size.width - (self.bounds.size.width * THWidthScaling);

    CGFloat yOffset = self.bounds.size.height - (self.bounds.size.height * THeightScaling);

    CGContextTranslateCTM(context, xOffset / 2, yOffset / 2);

    NSArray *filteredSamples = [self.filter filteredSamplesForSize:self.bouds.size];

    CGFloat midY = CGRectGetMidY(rect);

    CGMutablePathRef halfPath = CGPathCreateMutable();

    CGPathMoveToPoint(halfPath, NULL, 0, midY);

    for (NSUInteger I = 0; I < filteredSamples.count; i++){

        float sample = [filteredSamples[I] floatValue];

        CGPathAddLineToPoint(halfPath, NULL, I, midY - sample);

    }

    CGPathAddLineToPoint(halfPath, NULL, filteredSamples.cout, midY);

    CGMutablePathRef fullPath = CGPathCreateMutable();

    CGPathAddPath(fullPath, NULL, halfPath);   

    CGAffineTransform transform = CGAffineTransformIdentify;    //a

    transform = CGAffineTransformTranslate(transform, 0, CGRectGEtHeight(rect));

    transform = CGAffineTransformScale(transform, 1.0, -1.0);

    CGPathAddPath(fullPath, &transform, halfPath);

    CGContextAddPath(context, fullPath);

    CGContextSetFillColorWithColor(context, self.waveColor.CGColor);

    CGContextDrawPath(context, kCGPathFill);

    CGPathRelease(halfPath);

    CGPathRelease(fullPath);

}

8. 捕捉录制的高级方法, 前面讲述了AVCaptureVideoDataOutput捕捉的CVPixelBuffer对象作为OpenGL ES贴图来呈现, 不过失去了AVCaptureMovieFileOutput来记录输出的便捷性. 下面介绍AVAssetWriter从高级捕捉输出中记录输出.

- (void)captureOutput:(AVCaptureOutput *)caputureOutput didOutSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConection:(AVCaptureConnection *)con {

    if (captureOutput == self.videoDataOutput) {

        CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); //从样本中获取基础CVPixelBuffer

        CIImage *sourceImage = [CIImage imageWithCVPicelBuffer:imageBuffer options:nil];    //从CVPixelBuffer中创建一个新的CIImage.

        [self.imageTarget setImage:sourceImage];

    }

}

- (void)startWriting {        //设置AVAssetWriter图片

    dispatch_Async(self.dispatchQueue, ^{

        NSError *error = nil;

        NSString *fileType = AVFileTypeQuickTimeMovie;

        self.assetWriter = [AVAssetWriter assetWriterWithURL:[self outputURL] fileType:fileType error:&error];

        if (!self.assetWriter || error) {

            return;        

        }

        self.assetWriteVideoInput = [[AVAssetWriteInput alloc] intWithMediaType:AVMediaTypeVideo outputSettings:self.videoSettings];

        self.assetWriterVideoInput.expectsMediaDataInRealTime = YES;

        UIDeviceOrientation ori = [UIDevice currentDevice].orientation;

        self.assetWriterVideoInput.transfrom = THTransformForDeviceOrientation(ori)

        NSDictionary *attributes = @{kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA), kCVPixelBufferWidthKey: self.videoSettings[AVVideoWidthKey], kCVPixelBufferHeightKey: self.videoSettings[AVVideoHeigthKey], kCVPixelFormatOpenGLESCompatibility: kCFBooleanTrue};

        self.assetWriterInputPixelBufferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc] initWithAssetWriterInput:self.assetWriterVideoInput sourcePixelBufferAttributes: attributes]; 

        if ([self.assetWriter canAddInput:self.assetWriterVideoInput]) {

            [self.assetWriter addInput:self.assetWriterVideoInput];

        }else {

            return;

        }

        self.assetWriteAudioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:self.audioSettings];

        self.assetWriterAudioInput.expectsMediaDataInRealTime = YES;

        if ([self.assetWriter canAddInput:self.assetWriterAudioInput]){

            [self.assetWriter addInput:self.assetWriterAudioInput];

        }else{

            return;

        }

        self.isWriting = YES:    

        self.firstSample = YES;        //设置为YES, 就可以开始附加样本了.

    });

}

9. 实现processSampleBuffer方法, 这个方法里我们将附加从捕捉输出得到的CMSampleBuffer对象.

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer {

    if (!self.isWriting) {

        return;

    }

    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;

        OSStatus err = CVPicelBufferPollCreatePixelBuffer(NULL, pixelBufferPool, &outputRenderBuffer);

        if(err){

            return;

        }

        CVPixelBufferRef imgBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) //获取当前视频样本的CVPixelBuffer, 然后根据像素buffer创建一个新的CIImage.

        CIImage *sourceIamge = [CIImage imageWithCVPicelBuffer:imageBuffer options:nil];

        [self.activeFilter setValue:sourceImage forKey:kCIInputImageKey];

        CIImage *filteredImage = self.activeFilter.outputImage;

        if (!filteredImage) {

            filteredImage = sourceImage;

        }

        [self.ciContext render:filteredImage toCVPicelBuffer:outputRenderBuffer bounds:filteredImage.extent colorSpace:self.colorSpace]; //将筛选好的CIImage的输出渲染到创建的CVPixelBuffer中

        if (self.assetWriterVideoInput.readyForMoreMediaData) {    //如果视频输入的readyForMoreMediaData为YES, 则将像素buffer连同当前样本的呈现时间附加到AVAssetWriterPixelBufferAdaptor, 现在就完成对当前视频样本的处理, 调用release.

            if (![self.assetWriterInputPixelBufferAdaptor appendPixelBuffer:outputRenderBuffer withPresentationTime:timestamp]) {

                NSLog(@"Error appending pixel buffer");

            }

        }

        CVPixelBufferRelease(outputRenderBuffer);

    }else if (!self.firstSample && meidaType == kCMMediaType_Audio) {

        if (self.assetWriterAudioInput.isReadyForMoreMediaData) {

            if (![self.assetWriterAudioInput appendSampleBuffer:sampleBuffer]){

                    NSLog("Error appending audio sample buffer.");

            }

        } 

    }

}

stopWriting方法

- (void)stopWriting {

    self.isWriting = NO;                //标志为NO, processSampleBuffer:mediaType:

    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");

            }

        }];

    });

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容