精华内容
下载资源
问答
  • AVFoundation音视频封装到MP4/MOV(二)

    千次阅读 2020-10-27 10:34:51
    AVFoundation的封装确实比较强大,分两种情况,一种是要写入数据是已经压缩好音视频流(音频aac流,视频h264流)可以直接通过AVFoundation封装到MOV,MP4等等文件中; 另一种是原始的未压缩音视频数据(YUV视频,...

    前言

    从本文开始逐渐学习iOS自带的多媒体处理框架,例如AVFoundation,VideoToolbox,CoreMedia,CoreVideo实现多媒体的处理,并且将实现方式以及效果和ffmpeg的方式做对比

    AVFoundation的封装确实比较强大,分两种情况,一种是要写入的数据是已经压缩好的音视频流(音频aac流,视频h264流)可以直接通过AVFoundation封装到MOV,MP4等等文件中;
    另一种是原始的未压缩音视频数据(YUV视频,PCM视频)也可以通过AVFoundation的接口封装到MOV和MP4中,流程基本一样,只不过AVFoundation再写入未压缩数据时内部自动进行编码之后再写入文件中(而且采用的是硬编码,速度非常快)

    对应的ffmpeg实现方式参考:音视频封装到MP4/MP3ffmpeg(十四)

    封装相关流程

    image.png

    封装相关对象及函数介绍

    • 1、AVAssetWriter对象
      音视频写入对象管理器,通过该对象来控制写入的开始和结束以及管理音视频输入对象
      a、封装器对象,用于将音视频数据(压缩或未压缩数据)写入文件
      b、可以单独写入音频或视频,如果写入未压缩的音视频数据,AVAssetWriter内部会自动调用编码器进行编码
      用如下函数创建该对象实例,备注:outputURL所对应的文件一定要先删除在调用此方法,否则会出错
      assetWriterWithURL:(NSURL *)outputURL fileType:(AVFileType)outputFileType error:(NSError * _Nullable * _Nullable)outError;

    • 2、AVAssetWriterInput
      用于将数据写入容器,可以写入压缩数据也可以写入未压缩数据,用如下方法初始化:
      -(instancetype)initWithMediaType:(AVMediaType)mediaType outputSettings:(nullable NSDictionary<NSString *, id> *)outputSettings sourceFormatHint:(nullable CMFormatDescriptionRef)sourceFormatHint
      如果outputSettings为nil则代表对数据不做压缩处理直接写入容器,不为nil则代表对对数据按照指定格式压缩后写入容器最后一个参数sourceFormatHint为CMFormatDescriptionRef类型,表示封装相关的参数信息,当outputSettings为nil时,该参数必须设定否则无法封装(MOV格式除外)

    • 3、addInput:
      将写入对象添加到封装器中

    • 4、startWriting:
      标记AVAssetWriter为可写状态

    • 5、-(void)requestMediaDataWhenReadyOnQueue:(dispatch_queue_t)queue usingBlock:(void (^)(void))block;
      音视频写入监控队列,该队列的工作原理:
      1、此方法调用后不会阻塞,当输入对象处于可写状态并且可以向输入对象写入数据时,block会回调,该回调block会一直周期性的回调直到输入队列处于不可写状态,所以就可以再此回调调用appendSampleBuffer写入数据了,这样能保证正常写入
      2、输入队列不会持有该回调block
      3、如果不通过requestMediaDataWhenReadyOnQueue回调方式而是直接调用appendSampleBuffer写入数据,则有可能会出错,因为音视频两个输入对象是共用的一个写入管理器,可能处于不可写状态

    • 6、readyForMoreMediaData:
      readyForMoreMediaData为YES代表现在处于可写状态了,为NO代表不可写状态

    实现代码

    这里分两种情况的实现代码,一种是写入时不编码,一种是写入时进行重新编码,请看如下代码:

     

    #import <Foundation/Foundation.h>
    
    
    @interface AVMuxer : NSObject
    
    /** 实现功能:接封装MP4文件然后再重新封装到MP4文件中,不重新进行编码
     */
    - (void)remuxer:(NSURL*)srcURL dstURL:(NSURL*)dstURL;
    
    /** 实现功能:将一个MP4文件转换成MOV文件
     *  视频编码方式由H264变成H265
     *  // 备注:ios 不支持mp3的编码
     *  // 备注:低端机型不支持H265编码
     */
    - (void)transcodec:(NSURL*)srcURL dstURL:(NSURL*)dstURL;
    @end
    

    实现文件

     

    import "AVMuxer.h"
    #import <AVFoundation/AVFoundation.h>
    
    @implementation AVMuxer
    {
        dispatch_semaphore_t semaphore;
        dispatch_queue_t    awriteQueue;
        dispatch_queue_t    vwriteQueue;
    }
    - (void)remuxer:(NSURL*)srcURL dstURL:(NSURL*)dstURL
    {
        semaphore = dispatch_semaphore_create(0);
        awriteQueue = dispatch_queue_create("awriteQueue", DISPATCH_QUEUE_SERIAL);
        vwriteQueue = dispatch_queue_create("vwriteQueue", DISPATCH_QUEUE_SERIAL);
        
        // 创建AVURLAsset容器对象
        AVURLAsset *urlAsset = [[AVURLAsset alloc] initWithURL:srcURL options:nil];
        // 异步初始化容器对象中的属性
        [urlAsset loadValuesAsynchronouslyForKeys:@[@"tracks"] completionHandler:^{
            AVAssetReaderTrackOutput *videoOutput = nil,*audioOutput = nil;
            AVAssetReader *reader = [self createAssetReader:urlAsset videoOutput:&videoOutput audioOutput:&audioOutput];
            [self doDemuxer:reader videoOutput:videoOutput audioOutput:audioOutput thenMuxerTo:dstURL];
        }];
        
        // 等待任务完成
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"结束了");
    }
    
    - (AVAssetReader*)createAssetReader:(AVAsset*)asset videoOutput:(AVAssetReaderTrackOutput**)videoOutput audioOutput:(AVAssetReaderTrackOutput**)audioOutput
    {
        // 创建音视频输出管理对象,通过该对象开启和结束对外界输出音视频
        NSError *error = nil;
        AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:asset error:&error];
        if (error) {
            NSLog(@"create AVAssetReader failer");
            return nil;
        }
        
        BOOL foundVideoTrack = NO,foundAudioTrack = NO;
        NSArray *tracks = [asset tracks];
        for (int i=0; i<tracks.count; i++) {
            AVAssetTrack *track = tracks[i];
            if ([track.mediaType isEqualToString:AVMediaTypeVideo] && !foundVideoTrack) {
                // 视频轨道对象
                NSLog(@"视频轨道");
                
                // 通过视频轨道对象创建视频输出对象,第二个参数为nil代表从容器读取视频数据输出时保留原格式,不解码
                AVAssetReaderTrackOutput *videoTrackOut = [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:nil];
                // 将视频输出对象添加到音视频输出管理器对象中
                if (![reader canAddOutput:videoTrackOut]) {
                    NSLog(@"can add videooutput");
                    return nil;
                }
                [reader addOutput:videoTrackOut];
                *videoOutput = videoTrackOut;
                foundVideoTrack = YES;
            } else if ([track.mediaType isEqualToString:AVMediaTypeAudio] && !foundAudioTrack) {
                // 从AVAsset对象获取音频轨道对象
                NSLog(@"音频轨道");
                // 通过AVAssetTrack对象创建音频输出对象;第二个参数为nil代表输出的音频不经过解码保留原格式
                AVAssetReaderTrackOutput *audioTrackOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:track outputSettings:nil];
                // 将音频输出对象添加到音视频输出管理器对象中
                [reader addOutput:audioTrackOutput];
                *audioOutput = audioTrackOutput;
                foundAudioTrack = YES;
            }
        }
        
        return reader;
    }
    
    - (void)doDemuxer:(AVAssetReader*)reader videoOutput:(AVAssetReaderTrackOutput*)videoOutput audioOutput:(AVAssetReaderTrackOutput*)audioOutput thenMuxerTo:(NSURL*)dstURL
    {
        // 开启从容器中读取数据然后输出音视频数据
        if (![reader startReading]) {
            NSLog(@"reader failer");
            return;
        }
        
        // 创建封装器
        AVAssetWriterInput *videoInput = nil,*audioInput = nil;
        AVAssetWriter *writer = [self createAssetWriter:dstURL videoTrack:videoOutput.track audioTrack:audioOutput.track];
        
        // 从封装器中编译出输入对象
        __block BOOL audioFinish = YES;
        __block BOOL videoFinish = YES;
        __block BOOL audioWriteFinish = YES;
        __block BOOL videoWriteFinish = YES;
        for (AVAssetWriterInput *input in writer.inputs) {
            if ([input.mediaType isEqualToString:AVMediaTypeAudio]) {
                audioInput = input;
                audioFinish = NO;
                audioWriteFinish = NO;
            }
            
            if ([input.mediaType isEqualToString:AVMediaTypeVideo]) {
                videoInput = input;
                videoFinish = NO;
                videoWriteFinish = NO;
            }
        }
        
        // 封装器开始写入输入标记
        if (![writer startWriting]) {
            NSLog(@"writer failer");
            return;
        }
        
        __block BOOL firstSample = YES;
        /** 音视频写入监控队列,该队列的工作原理
         *  1、此方法调用后不会阻塞,当输入对象处于可写状态并且可以向输入对象写入数据时,block会回调,该回调block会一直周期性的回调
         *  直到输入队列处于不可写状态,所以就可以再此回调调用appendSampleBuffer写入数据了,这样能保证正常写入
         *  2、videoInput不会持有该回调block
         *  3、如果不通过requestMediaDataWhenReadyOnQueue回调方式而是直接调用appendSampleBuffer写入数据,则有可能会出错,因为音视频
         *  两个输入对象是共用的一个写入管理器,可能处于不可写状态
         *
         */
        [videoInput requestMediaDataWhenReadyOnQueue:vwriteQueue usingBlock:^{
            
            // 虽然block会周期性的回调,不过一般在这里通过循环来持续性的写入数据,readyForMoreMediaData为YES代表现在处于可写状态了
            while (videoInput.readyForMoreMediaData) {
    
                // 如果容器中音频和视频全部被读取完毕,那么status才会从AVAssetReaderStatusReading变成AVAssetReaderStatusCompleted
                // 所以可以通过这个status来监听容器中数据是否全部读取完毕
                // 备注:音视频中只有一个被读取完毕status的值仍为AVAssetReaderStatusReading
                if (reader.status == AVAssetReaderStatusReading) {
                    CMSampleBufferRef samplebuffer = [videoOutput copyNextSampleBuffer];
                    
                    if (firstSample && samplebuffer) {
                        
                        firstSample = NO;
                        [writer startSessionAtSourceTime:CMSampleBufferGetOutputPresentationTimeStamp(samplebuffer)];
                    }
                    
                    if (samplebuffer) {
                        [self printSamplebuffer:samplebuffer video:YES];
                        BOOL result = [videoInput appendSampleBuffer:samplebuffer];
                        NSLog(@"video writer %d",result);
                    } else {
                        // 容器中音视频数据时长可能有些差距,那么会出现视频或者音频先读取完毕的情况,如果要监控音频或者视频中的某一个是否读取完毕
                        // 那么通过判断samplebuffer是否为NULL
                        videoFinish = YES;
                        // 当调用markAsFinished之后,readyForMoreMediaData会变为NO,这个回调方法也不会再调用了
                        // 该方法可以写在任何地方,不影响
                        [videoInput markAsFinished];
                        NSLog(@"没数据啦");
                    }
                }
    //            else {
    //                NSLog(@"状态 %ld",reader.status);
    //            }
            }
    
            if (videoFinish && audioFinish) {
                NSLog(@"真正结束了1");
                // 当音视频输入对象都调用markAsFnish函数后,就需要调用此方法结束整个封装,此方法可以写在这个回调里面也可以
                // 写在任何地方
                [writer finishWritingWithCompletionHandler:^{
                    
                }];
                dispatch_semaphore_signal(self->semaphore);
            }
        }];
        
        [audioInput requestMediaDataWhenReadyOnQueue:awriteQueue usingBlock:^{
            
            while (audioInput.readyForMoreMediaData) {  // 说明可以向封装器写入数据了
                
                if (reader.status == AVAssetReaderStatusReading) {
                    CMSampleBufferRef samplebuffer = [audioOutput copyNextSampleBuffer];
                    
                    if (firstSample && samplebuffer) {
                        firstSample = NO;
                        [writer startSessionAtSourceTime:CMSampleBufferGetOutputPresentationTimeStamp(samplebuffer)];
                    }
                    
                    if (samplebuffer) {
                        [self printSamplebuffer:samplebuffer video:NO];
                        BOOL result = [audioInput appendSampleBuffer:samplebuffer];
                        NSLog(@"video writer %d",result);
                    } else {
                        NSLog(@"说明音频读取完毕");
                        audioFinish = YES;
    //                    [audioInput markAsFinished];
                    }
                }
    //            else {
    //                NSLog(@"我的状态 %ld",reader.status);
    //            }
            }
            
            if (videoFinish && audioFinish) {
                NSLog(@"真正结束了2");
                [writer finishWritingWithCompletionHandler:^{
                    
                }];
                dispatch_semaphore_signal(self->semaphore);
            }
        }];
    }
    
    - (void)printSamplebuffer:(CMSampleBufferRef)samplebuffer video:(BOOL)video
    {
        static int vnum = 0,anum = 0;
        CGFloat pts = CMTimeGetSeconds(CMSampleBufferGetOutputPresentationTimeStamp(samplebuffer));
        CGFloat dts = CMTimeGetSeconds(CMSampleBufferGetOutputDecodeTimeStamp(samplebuffer));
        CGFloat dur = CMTimeGetSeconds(CMSampleBufferGetOutputDuration(samplebuffer));
        size_t size = CMSampleBufferGetTotalSampleSize(samplebuffer);
        
        if (video) {
            vnum++;
            /** CMFormatDescriptionRef对象(格式描述对象)
             *  1、CMVideoFormatDescriptionRef、CMAudioFormatDescriptionRef、CMTextFormatDescriptionRef是它的具体子类,分别
             *  对应着视频、音频、字幕的封装参数对象
             *  2、所有关于音视频等等的编解码参数,宽高等等都存储在此对象中
             *
             *  对于视频来说,它包括编码参数,宽高以及extension扩展参数,CMFormatDescriptionGetExtensions可以查看扩展参数内容
             *
             *  对于一个容器中读取出来的所有音/视频数据对象CMSampleBufferRef,音频对应着一个CMFormatDescriptionRef,视频
             *  对应着一个CMFormatDescriptionRef(即所有视频数据对象得到的格式描述对象地址都一样),音频也是一样
             */
    //            NSLog(@"extension %@",CMFormatDescriptionGetExtensions(curformat));
    //        NSLog(@"CMFormatDescriptionRef %@",CMSampleBufferGetFormatDescription(samplebuffer));
            NSLog(@"video pts(%f) dts(%f) duration(%f) size(%ld) num(%d)",pts,dts,dur,size,vnum);
        } else {
            anum++;
            NSLog(@"audio pts(%f) dts(%f) duration(%f) size(%ld) num(%d)",pts,dts,dur,size,anum);
        }
    }
    
    - (AVAssetWriter *)createAssetWriter:(NSURL*)dstURL videoTrack:(AVAssetTrack*)vtrack audioTrack:(AVAssetTrack*)atrack
    {
        NSError *error = nil;
        // AVAssetWriter 音视频写入对象管理器,通过该对象来控制写入的开始和结束以及管理音视频输入对象
        /** AVAssetWriter对象
         *  1、封装器对象,用于将音视频数据(压缩或未压缩数据)写入文件
         *  2、可以单独写入音频或视频,如果写入未压缩的音视频数据,AVAssetWriter内部会自动调用编码器进行编码
         */
        /** 遇到问题:调用startWriting提示Error Domain=AVFoundationErrorDomain Code=-11823 "Cannot Save"
         *  分析原因:如果封装器对应的文件已经存在,调用此方法时会提示这样的错误
         *  解决方案:调用此方法之前先删除已经存在的文件
         */
        unlink([dstURL.path UTF8String]);
        AVAssetWriter *writer = [AVAssetWriter assetWriterWithURL:dstURL fileType:AVFileTypeMPEG4 error:&error];
        if (error) {
            NSLog(@"create writer failer");
            return nil;
        }
        
        // 往封装器中添加音视频输入对象,每添加一个输入对象代表要往容器中添加一路流,一般添加一路视频流
        if (vtrack) {
            /** AVAssetWriterInput 对象
             *  用于将数据写入容器,可以写入压缩数据也可以写入未压缩数据,如果outputSettings为nil则代表对数据不做压缩处理直接写入容器,
             *  不为nil则代表对对数据按照指定格式压缩后写入容器
             *  最后一个参数sourceFormatHint为CMFormatDescriptionRef类型,表示封装相关的参数信息,当outputSettings为nil时,该参数必须设定
             *  否则无法封装(MOV格式除外)
             */
            CMFormatDescriptionRef srcformat = (__bridge CMFormatDescriptionRef)(vtrack.formatDescriptions[0]);
            AVAssetWriterInput *videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:nil sourceFormatHint:srcformat];
            // 将写入对象添加到封装器中
            [writer addInput:videoInput];
        }
        
        if (atrack) {
            CMFormatDescriptionRef srcformat = (__bridge CMFormatDescriptionRef)(atrack.formatDescriptions[0]);
            AVAssetWriterInput *audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:nil sourceFormatHint:srcformat];
            [writer addInput:audioInput];
        }
     
        return writer;
    }
    
    
    - (void)transcodec:(NSURL *)srcURL dstURL:(NSURL *)dstURL
    {
        semaphore = dispatch_semaphore_create(0);
        awriteQueue = dispatch_queue_create("awriteQueue.com", DISPATCH_QUEUE_SERIAL);
        vwriteQueue = dispatch_queue_create("vwriteQueue.com", DISPATCH_QUEUE_SERIAL);
        CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
        
        AVURLAsset *inputAsset = [AVURLAsset assetWithURL:srcURL];
        [inputAsset loadValuesAsynchronouslyForKeys:@[@"tracks"] completionHandler:^{
            AVAssetReader *reader = [self createReader:inputAsset];
            [self demuxer:reader dstUrl:dstURL];
        }];
        
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"结束了,耗时 %f秒",CFAbsoluteTimeGetCurrent() - startTime);
        
    }
    
    - (AVAssetReader*)createReader:(AVAsset*)asset
    {
        NSError *error = nil;
        AVAssetReader *reader = [AVAssetReader assetReaderWithAsset:asset error:&error];
        if (error) {
            NSLog(@"create reader failer");
            return nil;
        }
        
        // 创建视频输出对象
        AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
        // 由于这里要从h264编码变成h265编码,所以就需要视频的格式为YUV的
        /** 遇到问题:编码视频提示Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed"
         *  分析原因:iOS不支持kCVPixelFormatType_422YpCbCr8BiPlanarFullRange,这里写错了应该是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
         *  解决方案:改成kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
         */
        NSDictionary *videoOutputs = @{
            (id)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
        };
        AVAssetReaderTrackOutput *readerVideoTrackOut = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:videoOutputs];
        readerVideoTrackOut.alwaysCopiesSampleData = NO;
        [reader addOutput:readerVideoTrackOut];
        
        // 创建音频输出对象
        AVAssetTrack *audioTrack = [[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
        NSDictionary *audioSettings = @{
            AVFormatIDKey:@(kAudioFormatLinearPCM),
        };
        AVAssetReaderTrackOutput *readerAudioOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:audioTrack outputSettings:audioSettings];
        readerAudioOutput.alwaysCopiesSampleData = NO;
        [reader addOutput:readerAudioOutput];
        
        return reader;
    }
    
    - (void)demuxer:(AVAssetReader*)reader dstUrl:(NSURL*)dstUrl
    {
        AVAsset *asset = reader.asset;
        
        // 获取视频相关参数信息
        CMFormatDescriptionRef srcVideoformat = (__bridge CMFormatDescriptionRef)[[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0].formatDescriptions[0];
        CMVideoDimensions demensions = CMVideoFormatDescriptionGetDimensions(srcVideoformat);
        
        // 创建封装器,通过封装器的回调函数驱动来获取数据
        NSError *error = nil;
        // zsz:todo 又一次忘记写了这句代码
        unlink([dstUrl.path UTF8String]);
        AVAssetWriter *writer = [AVAssetWriter assetWriterWithURL:dstUrl fileType:AVFileTypeQuickTimeMovie error:&error];
        if (error) {
            NSLog(@"create writer failer");
            dispatch_semaphore_signal(semaphore);
            return;
        }
        
        // 添加音视频输入对象
        // 对于音频来说 编码方式,采样率,采样格式,声道数,声道类型等等是必须的参数,比特率可以不用设置
        // 对于食品来说 编码方式,视频宽和高则是必须参数
        // 低端机型不支持H265编码
        NSDictionary *videoSettings = @{
            AVVideoCodecKey:AVVideoCodecHEVC,
            AVVideoWidthKey:@(demensions.width),
            AVVideoHeightKey:@(demensions.height)
        };
        AVAssetWriterInput *videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
        videoInput.expectsMediaDataInRealTime = NO;
        [writer addInput:videoInput];
        
        // 获取音频相关参数
        CMFormatDescriptionRef audioFormat = (__bridge CMFormatDescriptionRef)[[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0].formatDescriptions[0];
        // 备注,这个extensions并不是音频相关参数,不知道是什么。
    //    CFDictionaryRef properties = CMFormatDescriptionGetExtensions(audioFormat);
        // 获取音频相关参数
        const AudioStreamBasicDescription *audioDes = CMAudioFormatDescriptionGetStreamBasicDescription(audioFormat);
        size_t layout_size = 0;
        // 对于音频来说 编码方式,采样率,采样格式,声道数,声道类型等等是必须的参数,比特率可以不用设置
        // 对于食品来说 编码方式,视频宽和高则是必须参数
        const AudioChannelLayout *layout = CMAudioFormatDescriptionGetChannelLayout(audioFormat, &layout_size);
        // ios 不支持mp3的编码
        NSDictionary *audioSettings = @{
            AVFormatIDKey:@(kAudioFormatMPEG4AAC),
            AVSampleRateKey:@(audioDes->mSampleRate),
            AVChannelLayoutKey:[NSData dataWithBytes:layout length:layout_size],
            AVNumberOfChannelsKey:@(audioDes->mChannelsPerFrame),
        };
        AVAssetWriterInput *audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
        /** 遇到问题:设置音频输入对象的expectsMediaDataInRealTime为YES,提示Domain=AVFoundationErrorDomain Code=-11800 "
         *  The operation could not be completed"
         *  分析原因:暂时没太明白,音视频采集的时候这个属性也都设置为YES了也没有问题,这里出问题
         *  解决方案:将此属性设置为NO
         */
        audioInput.expectsMediaDataInRealTime = NO;
        [writer addInput:audioInput];
        
        //开启解封装和封装
        if (![reader startReading]) {
            NSLog(@"reader startReading failer %@",reader.error);
            dispatch_semaphore_signal(semaphore);
            return;
        }
        if (![writer startWriting]) {
            NSLog(@"writer startWriting failer %@",writer.error);
            dispatch_semaphore_signal(semaphore);
            return;
        }
        
        AVAssetReaderOutput *videoOutput = nil,*audioOutput = nil;
        for (AVAssetReaderOutput *output in reader.outputs) {
            if ([output.mediaType isEqualToString:AVMediaTypeVideo]) {
                videoOutput = output;
            } else {
                audioOutput = output;
            }
        }
        
        __block BOOL firstWrite = YES;
        __block BOOL videoFinish = NO;
        __block BOOL audioFinish = NO;
        // 配置写入音视频数据的工作队列
        /** 遇到问题:模拟器发现内存消耗几个G,实际设备发现内存消耗正常
         *  分析原因:模拟器用cpu指令模拟GPU,内存模拟GPU的内存,因为这里编解码都是用的gpu,所以模拟器看起来内存消耗非常高
         *  解决方案:正常现象
         */
        [videoInput requestMediaDataWhenReadyOnQueue:vwriteQueue usingBlock:^{
            while (videoInput.readyForMoreMediaData) {  // 说明可以开始写入视频数据了
    
                if (reader.status == AVAssetReaderStatusReading) {
                    CMSampleBufferRef samplebuffer = [videoOutput copyNextSampleBuffer];
    
                    if (samplebuffer) {
                        // 从视频输出对象中读取数据
                        CMTime pts = CMSampleBufferGetOutputPresentationTimeStamp(samplebuffer);
    
                        if (firstWrite) {
                            firstWrite = NO;
    
                            [writer startSessionAtSourceTime:pts];
                        }
    
                        // 向视频输入对象写入数据
                        BOOL result = [videoInput appendSampleBuffer:samplebuffer];
    
                        NSLog(@"video writer %d",result);
                        if (!result) {
                            NSLog(@"video writer error %@",writer.error);
                        }
                        CMSampleBufferInvalidate(samplebuffer);
                        CFRelease(samplebuffer);
                    } else {
                       NSLog(@"说明视频数据读取完毕");
                       videoFinish = YES;
    
                       // 源文件中视频数据读取完毕,那么就不需要继续写入视频数据了,将视频输入对象标记为结束
                       [videoInput markAsFinished];
                   }
                }
            }
    
            if (videoFinish && audioFinish) {
                NSLog(@"真正结束了1");
                [writer finishWritingWithCompletionHandler:^{
                    [reader cancelReading];
                    dispatch_semaphore_signal(self->semaphore);
                }];
            }
        }];
        
        // 配置写入音频数据的工作队列
        [audioInput requestMediaDataWhenReadyOnQueue:awriteQueue usingBlock:^{
            while (audioInput.readyForMoreMediaData) {  // 说明可以开始写入数据了
    
                if (reader.status == AVAssetReaderStatusReading) {
                    CMSampleBufferRef samplebuffer = [audioOutput copyNextSampleBuffer];
                    if (samplebuffer) {
    
                        // 从输出对象中读取数据
                        CMTime pts = CMSampleBufferGetOutputPresentationTimeStamp(samplebuffer);
    
                        if (firstWrite) {
                            firstWrite = NO;
    
                            [writer startSessionAtSourceTime:pts];
                        }
    
                        // 向视频输入对象写入数据
                        BOOL result = [audioInput appendSampleBuffer:samplebuffer];
                        NSLog(@"audio writer %d",result);
                        if (!result) {
                            NSLog(@"audio writer error %@",writer.error);
                        }
                        CMSampleBufferInvalidate(samplebuffer);
                        CFRelease(samplebuffer);
                    } else {
                        NSLog(@"说明音频数据读取完毕1111");
                        audioFinish = YES;
    
                        // 源文件中视频数据读取完毕,那么就不需要继续写入视频数据了,将视频输入对象标记为结束
                        [audioInput markAsFinished];
                    }
                }
            }
    
            if (videoFinish && audioFinish) {
                NSLog(@"真正结束了2");
                [writer finishWritingWithCompletionHandler:^{
                    [reader cancelReading];
                    dispatch_semaphore_signal(self->semaphore);
                }];
    
            }
        }];
    }
    @end
    

    遇到问题:

    • 1、调用startWriting提示Error Domain=AVFoundationErrorDomain Code=-11823 "Cannot Save"
      分析原因:如果封装器对应的文件已经存在,调用此方法时会提示这样的错误
      解决方案:调用此方法之前先删除已经存在的文件

    • 2、编码视频提示Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed"
      分析原因:iOS不支持kCVPixelFormatType_422YpCbCr8BiPlanarFullRange,这里写错了应该是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
      解决方案:改成kCVPixelFormatType_420YpCbCr8BiPlanarFullRange

    • 3、设置音频输入对象的expectsMediaDataInRealTime为YES,提示Domain=AVFoundationErrorDomain Code=-11800 " The operation could not be completed"
      分析原因:暂时没太明白,音视频采集的时候这个属性也都设置为YES了也没有问题,这里出问题
      解决方案:将此属性设置为NO

    • 4、模拟器发现内存消耗几个G,实际设备发现内存消耗正常
      分析原因:模拟器用cpu指令模拟GPU,内存模拟GPU的内存,因为这里编解码都是用的gpu,所以模拟器看起来内存消耗非常高
      解决方案:正常现象

    项目地址

    https://github.com/nldzsz/ffmpeg-demo

    位于AVFoundation目录下文件AVMuxer.h/AVMuxer.m中

    展开全文
  • 1.常见封装格式: ①MP4:MP4是文件封装格式,后缀是MP4,但是封装可以是用H265/H264等等。 ②AVI:压缩标准可以任意选择,哪怕是经过压缩模式。现在用少。 ③FLV,ts:流媒体格式,一半用直播。 ④ASF:...

    1.常见封装格式:

        ①MP4:MP4是文件封装格式,后缀是MP4,但是封装可以是用H265/H264等等。

        ②AVI:压缩标准可以任意选择,哪怕是未经过压缩的模式。现在用的少。

        ③FLV,ts:流媒体格式,一半用直播。

        ④ASF:用于做点播,一般MP4格式也可以来做点播技术了。

    2.常见编码格式(视频):

        ①视频H264(AVC part10)。

        ②wmv。

        ③Xvid(Part2)。

        ④mjpeg。每一帧都是关键帧。

    3.常见编码格式(音频):

        ①acc。现在多。有损压缩

        ②MP3。以前多,现在少。有损压缩

        ③ape。效果好。基于无损压缩。

        ④flac。效果好。

    4.视频和音频在编码中的体现


        ①音视频的帧率不一样。

         er视频帧的缓冲复制对于CPU而言开销不大,但是对于YUV->RGB这块的缓冲复制对于CPU开销很大。所以整体视频解    码        要    关注于解码效率和yuv与rgb转换的效率。音频基本忽略不计。

    软解兼容性高,能搞到100帧以上,但是耗电量大。

    硬解是固化流程,帧率固定,变量不大,兼容性不是很高。




        

    展开全文
  • 18.3.4 用AggregateException处理Task上的未处理异常 525 18.4 取消任务 530 18.4.1 Task.Run()是Task.Factory.StartNew()简化形式 532 18.4.2 长时间运行任务 532 18.4.3 对任务进行资源...
  • // 设置录制完成后视频的封装格式THREE_GPP为3gp.MPEG_4为mp4 mediarecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // 设置录制视频编码h263 h264 mediarecorder.setVideoEncoder...
  • FFMpeg编解码测试Demo

    2015-08-06 11:12:03
    对FFMpeg二次封装,提取出YUV编码和H264解码对象,并提供测试Demo,因YUV文件太大上传
  • 索尼A7S3断电或者其它操作问题导致视频未封装完成,和以往视频文件还有一个不同之处,以往MP4文件格式不变,这次变成了RSV格式文件(RSV,以往只会发生在摄像机上,相机也开始使用了这个格式),这次修复的文件较...

    继佳能和松下相机HEVC|H.265断电视频文件成功修复后,终于迎来索尼HEVC|H.265编码损坏视频修复。回想以往索尼微单的MP4视频文件,使用的都是H.264视频编码技术,从A7S3微单开始支持H.265视频编码.

    索尼A7S3断电或者其它操作问题导致视频未封装完成,和以往的视频文件还有一个不同之处,以往MP4文件格式不变,这次变成了RSV格式文件(RSV,以往只会发生在摄像机上,相机也开始使用了这个格式),这次修复的文件较大,26GB, 使用H.265, 4K, 录制时长50分钟左右。以下为修复的视频编码参数,供参考:

    Video  
    ID                                        1
    Format                                     HEVC
    Format/Info                                High Efficiency Video Coding
    Format profile                             Main 10@L5.1@High
    Codec ID                                   hvc1
    Codec ID/Info                              High Efficiency Video Coding
    Bit rate                                   75.1 Mb/s
    Width                                      3 840 pixels
    Height                                     2 160 pixels
    Frame rate                                 50.000 FPS
    Color space                                YUV
    Bit depth                                  10 bits
       
    Audio  
    ID                                        2
    Format                                     PCM
    Codec ID                                   twos
    Bit rate mode                              Constant
    Bit rate                                   1 536 kb/s
    Channel(s)                                 2 channels
    Sampling rate                              48.0 kHz
    Bit depth                                  16 bits

     

    展开全文
  • FFmpeg学习笔记之av_parser_parse2()

    千次阅读 2019-05-31 11:38:07
    输入必须是只包含视频编码数据“裸流”(例如H.264、HEVC码流文件),而不能是包含封装格式媒体数据(例如AVI、MKV、MP4)。 av_parser_init():初始化AVCodecParserContext。其参数是cod...

    av_parser_parse2()拿到AVPaket数据,将一个个AVPaket数据解析组成完整的一帧未解码的压缩数据;

    跟av_read_frame类似。

    输入必须是只包含视频编码数据“裸流”(例如H.264、HEVC码流文件),而不能是包含封装格式的媒体数据(例如AVI、MKV、MP4)。

    av_parser_init():初始化AVCodecParserContext。其参数是codec_id,所以同时只能解析一种


    AVCodecParser用于解析输入的数据流并把它们分成一帧一帧的压缩编码数据。核心函数是av_parser_parse2():

    av_parser_parse2():解析数据获得一个Packet, 从输入的数据流中分离出一帧一帧的压缩编码数据。

    /** 
     * Parse a packet. 
     * 
     * @param s             parser context. 
     * @param avctx         codec context. 
     * @param poutbuf       set to pointer to parsed buffer or NULL if not yet finished. 
     * @param poutbuf_size  set to size of parsed buffer or zero if not yet finished. 
     * @param buf           input buffer. 
     * @param buf_size      input length, to signal EOF, this should be 0 (so that the last frame can be output). 
     * @param pts           input presentation timestamp. 
     * @param dts           input decoding timestamp. 
     * @param pos           input byte position in stream. 
     * @return the number of bytes of the input bitstream used. 
     * 
     * Example: 
     * @code 
     *   while(in_len){ 
     *       len = av_parser_parse2(myparser, AVCodecContext, &data, &size, 
     *                                        in_data, in_len, 
     *                                        pts, dts, pos); 
     *       in_data += len; 
     *       in_len  -= len; 
     * 
     *       if(size) 
     *          decode_frame(data, size); 
     *   } 
     * @endcode 
     */  
    int av_parser_parse2(AVCodecParserContext *s,  
                         AVCodecContext *avctx,  
                         uint8_t **poutbuf, int *poutbuf_size,  
                         const uint8_t *buf, int buf_size,  
                         int64_t pts, int64_t dts,  
                         int64_t pos);  

         其中poutbuf指向解析后输出的压缩编码数据帧,buf指向输入的压缩编码数据。如果函数执行完后输出数据为空(poutbuf_size为0),则代表解析还没有完成,还需要再次调用av_parser_parse2()解析一部分数据才可以得到解析后的数据帧。当函数执行完后输出数据不为空的时候,代表解析完成,可以将poutbuf中的这帧数据取出来做后续处理。

    数据结构初始化流程:

    avformat_open_input()会调用avformat_new_stream()创建AVStream;
    avformat_new_stream()中又会调用avcodec_alloc_context3()创建AVCodecContext
    av_read_frame():获取媒体的一帧压缩编码数据。其中调用了av_parser_parse2()。(注:av_read_frame()属于外部接口,由上层程序主动调用

    “纯净”的解码器中,通过avcodec_decode_video2()(注:此接口在ffmpeg3.0之后已经被遗弃掉了)成功解码第一帧之后,才能获取到宽高等信息

    解析出来的数据,可通过下面的方法判断帧类型:
    AVCodecParserContext->pict_type  :AV_PICTURE_TYPE_I,AV_PICTURE_TYPE_P


    参考:
    原文:https://blog.csdn.net/elesos/article/details/53509420 
     

    展开全文
  • libsndfile:C++封装的C库,用于通过标准库接口读写包含采样声音的文件。 libsoundio:用于跨平台实时音频输入输出的C库。 Maximilian :C++音频和音乐数字信号处理库。 OpenAL :开源音频库---跨平台的音频API。...
  • javaSE代码实例

    2016-06-21 22:30:18
    7.3.5 Java中封装的实现 110 7.4 final的变量 112 7.4.1 final的成员变量 113 7.4.2 final的局部变量 115 7.5 static关键字的使用 116 7.5.1 静态成员 116 7.5.2 静态成员的访问 117 7.5.3 静态最终...
  • 10.2.7 指定名称命名空间 330 10.2.8 命名空间别名 331 10.2.9 嵌套命名空间 331 10.3 预处理器 332 10.3.1 在程序中包含头文件 333 10.3.2 程序中置换 334 10.3.3 宏置换 336 10.3.4 放在多行代码...
  • 2.3.7 无状态文件服务器例子 11 2.3.8 有状态文件服务器例子 11 2.3.9 标识客户 12 2.3.10 无状态是一个协议问题 13 2.3.11 充当客户服务器 13 2.4 小结 14 深入研究 14 习题 15 第3章 客户-服务器...
  • 5.8.5 ListActivity(封装ListViewActivity) 154 5.8.6 ExpandableListView(可扩展列表控件) 155 5.8.7 Spinner(下拉列表控件) 157 5.9 滚动控件 160 5.9.1 ScrollView(垂直滚动控件) 160 5.9.2 ...
  • 每个类都是独立一个.h头文件和.cpp实现文件组成,零耦合,不依赖其他文件,方便单个控件独立出来以源码形式集成到项目中,方便直观。 控件数量远超其他第三方控件库比如qwt集成控件数量,使用方式也比其简单友好...
  • 5.8.5 ListActivity(封装ListViewActivity) 154 5.8.6 ExpandableListView(可扩展列表控件) 155 5.8.7 Spinner(下拉列表控件) 157 5.9 滚动控件 160 5.9.1 ScrollView(垂直滚动控件) 160 5.9.2 ...
  • 实例254 遍历、删除指定目录下所有文件 330 第5章 会话应用 333 5.1 COOKIE 334 实例255 控制登录用户过期时间 334 实例256 自动登录 335 实例257 单击登录 336 实例258 统计用户在线时间 339 实例259 限制用户...
  • 实例254 遍历、删除指定目录下所有文件 330 第5章 会话应用 333 5.1 COOKIE 334 实例255 控制登录用户过期时间 334 实例256 自动登录 335 实例257 单击登录 336 实例258 统计用户在线时间 339 实例259 限制用户...
  • java封装的提供ffmpeg命令执行、停止、查询功能的简单管理器 。 FFCH4j不仅仅只支持ffmpeg命令,还支持执行多平台的命令行指令,不管是执行linux命令还是windows的命令行都是手到擒来(注意:本项目并屏蔽某些敏感...
  • java封装的提供ffmpeg命令执行、停止、查询功能的简单管理器 。 FFCH4j不仅仅只支持ffmpeg命令,还支持执行多平台的命令行指令,不管是执行linux命令还是windows的命令行都是手到擒来(注意:本项目并屏蔽某些敏感...
  • java封装的提供ffmpeg命令执行、停止、查询功能的简单管理器 。 FFCH4j不仅仅只支持ffmpeg命令,还支持执行多平台的命令行指令,不管是执行linux命令还是windows的命令行都是手到擒来(注意:本项目并屏蔽某些敏感...
  • 介绍了应用ASP.NET进行程序开发各个方面知识和技巧,主要包括网站开发常备技术、前端技术应用开发、操作Office软件(Word/Excel)、ADO.NET数据库操作技术、LINQ技术、XML文件、水晶报表、Web Service服务、网站...
  • LuceneInAction(第2版)_中文版

    千次下载 热门讨论 2012-07-12 09:52:59
    4.7.3 搜索被分析域 136 4.8 语言分析 139 4.8.1 Unicode与字符编码 139 4.8.2 非英语语种分析 140 4.8.3 字符规范化处理 140 4.8.4 亚洲语种分析 141 4.8.5 有关非英语语种分析其他问题 143 4.9 ...
  • 介绍了应用ASP.NET进行程序开发各个方面知识和技巧,主要包括网站开发常备技术、前端技术应用开发、操作Office软件(Word/Excel)、ADO.NET数据库操作技术、LINQ技术、XML文件、水晶报表、Web Service服务、网站...
  • 除了Windows/Android/iOS Native SDK,大牛直播SDK播放端还支持Unity3d(Windows/Android/iOS)二次封装,Unity3D下为数不多真正功能强大高稳定、超低延迟RTMP/RTSP直播播放器,此外Windows平台RTMP推送端,也支持...
  • 17.2.3 命名命名空间 604 17.2.4 命名空间成员使用 606 17.2.5 类、命名空间和作用域 609 17.2.6 重载与命名空间 612 17.2.7 命名空间与模板 614 17.3 多重继承与虚继承 614 17.3.1 多重继承 615 17.3.2 转换...
  • 疯狂JAVA讲义

    2014-10-17 13:35:01
    学生提问:当我们使用编译C程序时,不仅需要指定存放目标文件的位置,也需要指定目标文件的文件名,这里使用javac编译Java程序时怎么不需要指定目标文件的文件名呢? 13 1.5.3 运行Java程序 14 1.5.4 根据...
  • 21天学通Java-由浅入深

    2011-06-06 09:59:14
    123 7.2.2 默认访问级别 124 7.3 什么是封装 125 7.4 最终修饰符 127 7.4.1 final修饰对象类型成员变量 127 7.4.2 final修饰基本类型成员变量 129 7.4.3 final修饰局部变量 131 7.4.4 final修饰方法 132 ...
  • iPhone开发秘籍(第2版)--源代码

    热门讨论 2012-12-11 13:51:22
     揭示官方文档详述细节  代码示例清晰易懂 内容简介  《iphone开发秘籍(第2版)》提供了关于iphone sdk以及iphone开发全面信息,对iphone sdk中各种组件做了深入浅出介绍,包括iphone 3.0 sdk所有...
  • iPhone开发秘籍(第2版)--详细书签版

    热门讨论 2012-12-11 13:42:25
     揭示官方文档详述细节  代码示例清晰易懂 内容简介  《iphone开发秘籍(第2版)》提供了关于iphone sdk以及iphone开发全面信息,对iphone sdk中各种组件做了深入浅出介绍,包括iphone 3.0 sdk所有...
  • vc源代码合集0951.rar

    热门讨论 2012-06-13 10:25:18
    2012-06-12 12:51 3,074,435 H264帧内预测算法研究与优化.pdf 2012-06-12 12:49 1,629,393 H264流媒体RTP打包源代码和相关资料.rar 2012-06-12 12:26 397 HEXtoTXT.TXT 2012-06-12 12:23 11,439,766 iccvar C...
  • 久负盛名 Python 入门经典 针对 Python 3 全新升级 十个出色项目,让你尽快可以使用 Python 解决实际问题 目录 第 1章 快速上手:基础知识 ........................ 1 1.1 交互式解释器 .......................

空空如也

空空如也

1 2
收藏数 30
精华内容 12
关键字:

未封装的264文件