VideoToolbox硬编码YUV为h264(九)

前言

IOS 8.0系统之后,苹果提供了VideoToolbox框架,它可以将摄像头采集的原始视频数据编码为指定的格式,如常见的h264/h265。摄像头采集的原始视频数据是很大的,以YUV颜色空间为例,1280x720p 30fps分辨率的视频,1秒的大小 = 1280x720x1.5x30 = 41.472mbps,所以原始视频数据不利于存储和在网络上进行传输,一般在采集到原始视频数据后都会进行一次有损压缩,然后进行存储或者传输。本文将记录如何采集视频然后编码为h264码流

h264码流格式

1、H264裸码流是由一个接一个的NALU(Nal Unit)组成的,NALU = 开始码 + NALU类型 + 视频数据,h264裸码流文件ffplay播放命令:

ffplay -f h264 test.h264

2、开始码:必须是"00 00 00 01" 或"00 00 01"
3、NALU类型:

类型 说明
0 未规定
1 非IDR图像中不采用数据划分的片段(P帧/B帧)
2 非IDR图像中A类数据划分片段
3 非IDR图像中B类数据划分片段
4 非IDR图像中C类数据划分片段
5 IDR图像的片段(I帧/Idr帧)
6 补充增强信息(SEI)
7 序列参数集(SPS)
8 图像参数集(PPS)
9 分割符
10 序列结束符
1 1 流结束符
1 2 填充数据
1 3 序列参数集扩展
14 带前缀的NAL单元
15 子序列参数集
16 -18 保留
19 不采用数据划分的辅助编码图像片段
20 编码片段扩展
21-23 保留
24-31 未规定

一般只用到1、5、7、8这4个类型,类型为5表示这是一个I帧,I帧前面必须有SPS和PPS数据,也就是类型为7和8,类型为1表示这是一个P帧或B帧。

h264原始码流一般按照如下顺序:NALU(SPS)+NALU(PPS)+NALU(Idr帧)+NALU(P帧)+NALU(P/B帧)+..+NALU(SPS)+NALU(PPS)+NALU(I帧)+.....

tips:
h264编码只支持yuv颜色空间;YUV颜色空间与RGB颜色空间表示视频的区别就是,同等分辨率前者占用空间少一半。

视频采集相关代码

苹果官方文档-AVFoundation

视频采集使用AVFoundation框架完成,如下图所示


captureDetail_2x.png

有如下几个很重要的对象
1、AVCaptureSession:
管理视频输入输出的会话(输入:摄像头;输出:输送数据给app端)
2、AVCaptureDevice:
代表了一个具体的物理设备,比如摄像头(前置/后置),扬声器等等;备注:模拟器无法运行摄像头相关代码
3、AVCaptureDeviceInput:
代表具体的视频输入,它要由具体的物理设备创建
4、AVCaptureVideoDataOutput:
它是AVCaptureOutput(它是一个抽象类)的子类,用于输出原始视频数据
5、AVCaptureConnection:
代表了AVCaptureInputPort和AVCaptureOutput、AVCaptureVideoPreviewLayer之间的连接通道,通过它可以将视频数据输送给AVCaptureVideoPreviewLayer进行显示,设置输出视频的输出视频的方向,镜像等等。
6、AVCaptureVideoPreviewLayer:
是一个可以显示摄像头内容的CAlayer的子类

具体采集相关代码如下:
1、初始化AVCaptureSession

self.mCaptureSession = [[AVCaptureSession alloc] init];
self.mCaptureSession.sessionPreset = AVCaptureSessionPreset640x480;   // 配置输出图像的分辨率
_width = 640;
_height = 480;

sessionPreset属性用来配置最终输出的原始视频的分辨率
2、创建视频输入对象,并添加到AVCaptureSession中

AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront];
// 根据物理设备创建输入对象
self.mCaptureInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDevice error:nil];
if ([self.mCaptureSession canAddInput:self.mCaptureInput]) {
    [self.mCaptureSession addInput:self.mCaptureInput];
}

AVCaptureDevicePositionFront代表前置摄像头
3、创建视频输出对象,设置输出代理,并添加到AVCaptureSession中

self.mVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
// 当回调因为耗时操作还在进行时,系统对新的一帧图像的处理方式,如果设置为YES,则立马丢弃该帧。
// NO,则缓存起来(如果累积的帧过多,缓存的内存将持续增长);该值默认为YES
self.mVideoDataOutput.alwaysDiscardsLateVideoFrames = NO;
/** 设置采集的视频数据帧的格式。这里代表生成的图像数据为YUV数据,颜色范围是full-range的
 *  并且是bi-planner存储方式(也就是Y数据占用一个内存块;UV数据占用另外一个内存块)
 */
[self.mVideoDataOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
[self.mVideoDataOutput setSampleBufferDelegate:self queue:captureQueue];
if ([self.mCaptureSession canAddOutput:self.mVideoDataOutput]) {
    [self.mCaptureSession addOutput:self.mVideoDataOutput];
}

由于使用h264方式编码,所以这里必须设置为yuv颜色空间
4、配置采集的视频数据通过AVCaptureVideoPreviewLayer渲染出来(非必须)

AVCaptureConnection *connection = [self.mVideoDataOutput connectionWithMediaType:AVMediaTypeVideo];
[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];

/** AVCaptureVideoPreviewLayer是一个可以显示摄像头内容的CAlayer的子类
 *  以下代码直接将摄像头的内容渲染到AVCaptureVideoPreviewLayer上面
 */
self.mVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.mCaptureSession];
[self.mVideoPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];
[self.mVideoPreviewLayer setFrame:self.view.bounds];
[self.view.layer addSublayer:self.mVideoPreviewLayer];

此步骤对于视频采集来说也是很重要的,因为可以实时看到自己想要采集的具体内容
5、开始采集

- (void)startRunCapSession
{
    if (!self.mCaptureSession.isRunning) {
        [self.mCaptureSession startRunning];
    }
}

6、通过代理获取到原始的视频数据

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    NSLog(@"采集到的数据 ==>%@",[NSThread currentThread]);
    /** CVImageBufferRef 表示原始视频数据的对象;
     *  包含未压缩的像素数据,包括图像宽度、高度等;
     *  等同于CVPixelBufferRef
     */
    // 获取CMSampleBufferRef中具体的视频数据
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
    /** 执行编码
     *  参数1:已经创建并且准备好的VTCompressionSessionRef对象
     *  参数2:具体的视频原始数据;CVImageBufferRef类型
     *  参数3:视频数据开始编码的时间;CMTime类型,一般是CMTimeMake(帧序号, 压缩单位(比如1000));
     *  参数4:该帧视频的时长,一般不需要计算(因为没法算),传kCMTimeInvalid即可
     *  参数5:要编码的视频相关属性;CFDictionaryRef类型
     *  参数6:传递给编码输出回调的参数;void* 类型
     *  参数7:编码结果标记;通过回调函数获取
     */
    // 帧序号时间,用于表示帧开始编码的时间(备注:这个时间是相对时间,并不是真正时间)
    CMTime presentationTime = CMTimeMake(_frameId++, 1000);
    VTEncodeInfoFlags encodeflags;
    OSStatus status = VTCompressionSessionEncodeFrame(_encodeSession, imageBuffer, presentationTime, kCMTimeInvalid, NULL, NULL, &encodeflags);
    if (status != noErr) {
        NSLog(@"VTCompressionSessionEncodeFrame fail %d",status);
        
        // 释放资源
        VTCompressionSessionInvalidate(_encodeSession);
        CFRelease(_encodeSession);
        _encodeSession = NULL;
    }
}

采集到的原始视频数据将通过该回调函数传回,原始视频数据存放在CMSampleBufferRef类型对象中。

CMSampleBufferRef:
1、包含音视频描述信息,比如包含音频的格式描述 AudioStreamBasicStreamDescription、包含视频的格式描述 CMVideoFormatDescriptionRef
2、包含音视频数据,可以是原始数据也可以是压缩数据;通过CMSampleBufferGetxxx()系列函数提取
CVImageBufferRef:
表示原始视频数据的对象;包含未压缩的像素数据,包括图像宽度、高度等;
等同于CVPixelBufferRef

编码相关代码

在进行编码之前得先初始化编码器,设置编码参数等等准备工作,具体使用流程如下:
1、初始化编码器

OSStatus status = VTCompressionSessionCreate(NULL, _width, _height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)self, &_encodeSession);
if (status != noErr) {
    NSLog(@"VTCompressionSessionCreate fail %d",status);
    return;
}

/** 创建编码器对象 VTCompressionSessionRef
VTCompressionSessionCreate(...)
* 参数1:创建对象内存使用的内存分配器,NULL代表使用默认分配器kCFAllocatorDefault
* 参数2/3:要编码的视频帧的宽和高;单位像素
* 参数4:使用的编码方式 比如H264(kCMVideoCodecType_H264)
* 参数5:设置编码方式相关的参数,比如H264编码所需的参数;CFDictionaryRef类型,NULL,则默认值;也可以
* 通过VTSessionSetProperty()函数设置
* 参数6:设置原始视频数据缓存的方式,CFDictionaryRef类型,NULL则代表使用默认值
* 参数7:设置编码数据的内存分配器及其它保存方式,CFAllocatorRef类型,NULL则使用默认值
* 参数8:设置编码数据输出回调函数
* 参数9:设置传入给该回调函数的参数;void类型
* 参数10:要创建的VTCompressionSessionRef对象
/
/
遇到问题:返回-12902错误
* 分析问题:在VTErrors.h中查看错误说明,意思参数错误,经检查是_width和_height没有指定具体的值
* 解决问题:给_width和_height赋上具体的值
*/
在创建编码器时一定要指定要编码的原始视频的宽和高,否则会返回错误。

2、设置编码器参数
通过VTSessionSetProperty()接口设置编码器相关参数,比如编码效率级别,GOP,平均码率,帧率,码率上限值等等

/** VTSessionSetProperty()函数既可以设置编码相关属性,又可以设置解码相关属性
 *  对于H264编码来说,以下属性是必须的
 *  1、编码效率级别:kVTCompressionPropertyKey_ProfileLevel
 *      kVTProfileLevel_H264_Baseline_AutoLevel
 *  2、GOP(关键帧间隔):
 *      kVTCompressionPropertyKey_MaxKeyFrameInterval
 *  3、编码后的帧率:
 *      kVTCompressionPropertyKey_ExpectedFrameRate;
 *      改变该值可以加快视频速度或者减慢视频速度
 *  4、编码后的平均码率:
 *      kVTCompressionPropertyKey_AverageBitRate
 *      平均码率决定了压缩的程度
 *  5、编码后的码率上限:
 *      kVTCompressionPropertyKey_DataRateLimits
 */
// 设置实时编码输出(避免延迟)
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
/** 设置H264编码的压缩级别
 *  BP(Baseline Profile):基本画质。支持I/P 帧,只支持无交错(Progressive)和CAVLC;主要应用:可视电话,会议
 *  电视,和无线通讯等实时视频通讯领域
 *  EP(Extended profile):进阶画质。支持I/P/B/SP/SI 帧,只支持无交错(Progressive)和CAVLC;
 *  MP(Main profile):主流画质。提供I/P/B 帧,支持无交错(Progressive)和交错(Interlaced),也支持CAVLC 和CABAC 的支持;主要应用:数字广播电视和数字视频存储
 *  HP(High profile):高级画质。在main Profile 的基础上增加了8×8内部预测、自定义量化、 无损视频编码和更多的YUV 格式;
 *  应用于广电和存储领域
 *  iPhone上方案如下:
 *  实时直播:
 *      低清Baseline Level 1.3
 *      标清Baseline Level 3
 *      半高清Baseline Level 3.1
 *      全高清Baseline Level 4.1
 *  存储媒体:
 *  低清 Main Level 1.3
 *  标清 Main Level 3
 *  半高清 Main Level 3.1
 *  全高清 Main Level 4.1
 *  高清存储:
 *  半高清 High Level 3.1
 *  全高清 High Level 4.1
 *
 *  参考文章:https://blog.csdn.net/sphone89/article/details/17492433
 */
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
// 设置是否开启B帧编码;默认开启,注意只有EP,MP,HP级别才支持B帧,如果是BP级别,该设置无效。
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanTrue);
/** 设置关键帧GOP间隔
 *  1、码率不变的前提下,GOP值越大P、B帧的数量会越多,平均每个I、P、B帧所占用的字节数就越多,也就更容易获取较好的图像质量;B帧的数量越多,同
 *  理也更容易获得较好的图像质量;
 *  2、需要说明的是,通过提高GOP值来提高图像质量是有限度的,在遇到场景切换的情况时,H.264编码器会自动强制插入一个I帧,此时实际的GOP值被缩短了。
 *  另一方面,在一个GOP中,P、B帧是由I帧预测得到的,当I帧的图像质量比较差时,会影响到一个GOP中后续P、B帧的图像质量,直到下一个GOP开始才有
 *  可能得以恢复,所以GOP值也不宜设置过大。
 *  3、同时,由于P、B帧的复杂度大于I帧,所以过多的P、B帧会影响编码效率,使编码效率降低。另外,过长的GOP还会影响Seek操作的响应速度,由于P、B帧
 *  是由前面的I或P帧预测得到的,所以Seek操作需要直接定位,解码某一个P或B帧时,需要先解码得到本GOP内的I帧及之前的N个预测帧才可以,GOP值越长
 *  需要解码的预测帧就越多,seek响应的时间也越长。
 */
int iFrameInternal = 10;
CFNumberRef iFrameRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &iFrameInternal);
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, iFrameRef);
// 设置期望帧率
int fps = 25;
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);

/** 设置均值码率,单位是bps,它不是一个硬性指标,实际的码率可能会上下浮动;VideoToolBox框架只支持ABR模式,而对于H264来说,它有四种
 *  码率控制模式,如下:
 *  CBR:恒定比特率方式进行编码,Motion发生时,由于码率恒定,只能通过增大QP来减少码字大小,图像质量变差,当场景静止时,图像质量又变好
 *      因此图像质量不稳定。这种算法优先考虑码率(带宽)。
 *  VBR:动态比特率,其码率可以随着图像的复杂程度的不同而变化,因此其编码效率比较高,Motion发生时,马赛克很少。码率控制算法根据图像
 *      内容确定使用的比特率,图像内容比较简单则分配较少的码率(似乎码字更合适),图像内容复杂则分配较多的码字,这样既保证了质量,又
 *      兼顾带宽限制。这种算法优先考虑图像质量。
 * CVBR:它是VBR的一种改进方法这种算法对应的Maximum bitRate恒定或者Average BitRate恒定。这种方法的兼顾了以上两种方法的优点,
 *      在图像内容静止时,节省带宽,有Motion发生时,利用前期节省的带宽来尽可能的提高图像质量,达到同时兼顾带宽和图像质量的目的
 *  ABR:在一定的时间范围内达到设定的码率,但是局部码率峰值可以超过设定的码率,平均码率恒定。可以作为VBR和CBR的一种折中选择。
 *
 *  H264各个分辨率推荐的码率表:http://www.lighterra.com/papers/videoencodingh264/
 */
SInt32 avgbitRate = 0.96*1000000;   // 注意单位是bit/s 这里是640x480的 为0.96Mbps
CFNumberRef avgRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &avgbitRate);
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_AverageBitRate, avgRateLimitRef);
/** 遇到问题:编码的视频马赛克严重
 *  原因分析:没有正确的设置码率上限值
 *  解决思路:正确设置码率上限
 *
 *  备注:码率上限一个数组,按照@[比特数,时长.....]方式传值排列,至少一对 比特数,时长;如果有多个,这些值必须平滑,内部会有一个算法算出最终值
 *  均值码率过低,也会造成马赛克
 */
// 设置码率上限
int bitRateLimits = avgbitRate; // 一秒钟的最大码率
NSArray *limit = @[@(bitRateLimits * 1.5), @(1)];
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);

3、准备编码相关上下文

status = VTCompressionSessionPrepareToEncodeFrames(_encodeSession);
if (status == noErr) {
    NSLog(@"CompressionSession 初始化成功 可以开始解码了");
}

这里有一个地方要注意下,如果没有设置码率上限值或者码率上限值设置方式不对,平均码率过小都会引起编码出现马赛克,请看上面具体的注释
4、开始编码
开始编码应该在采集的回调函数中,也就是采集相关代码的最后一部中的代码

// 帧序号时间,用于表示帧开始编码的时间(备注:这个时间是相对时间,并不是真正时间)
CMTime presentationTime = CMTimeMake(_frameId++, 1000);
VTEncodeInfoFlags encodeflags;
OSStatus status = VTCompressionSessionEncodeFrame(_encodeSession, imageBuffer, presentationTime, kCMTimeInvalid, NULL, NULL, &encodeflags);
if (status != noErr) {
    NSLog(@"VTCompressionSessionEncodeFrame fail %d",status);
    
    // 释放资源
    VTCompressionSessionInvalidate(_encodeSession);
    CFRelease(_encodeSession);
    _encodeSession = NULL;
}

组装为h264码流

调用VTCompressionSessionEncodeFrame()函数后,系统内部会进行编码,编码结果通过第一步创建的回调函数返回
编码的NALU数据格式为:NALU长度(四字节)+编码类型+编码数据,h264码流的NALU数据格式为:起始码+编码类型+编码数据,所以要先转换一下在保存,具体代码如下

void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
{
    NSLog(@"didCompressH264 called with status %d infoFlags %d", (int)status, (int)infoFlags);
    if (status != noErr) {
        NSLog(@"compress fail %d",status);
        return;
    }
    
    // 返回该sampleBuffer是否可以进行操作了
    if (!CMSampleBufferDataIsReady(sampleBuffer)) {
        NSLog(@"CMSampleBufferDataIsReady is not ready");
        return;
    }
    
    VideoEnDecodeViewController *mySelf = (__bridge VideoEnDecodeViewController*)outputCallbackRefCon;
    // CMSampleBufferGetSampleAttachmentsArray获取视频帧的描述信息,比如是否关键帧等等;kCMSampleAttachmentKey_NotSync标记是否关键帧
    BOOL keyframe = CFDictionaryContainsKey(CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES), 0), kCMSampleAttachmentKey_NotSync);
    if (keyframe) {
        /** CMFormatDescriptionRef中包含了PPS/SPS/SEI,宽高、颜色空间、编码格式等描述信息的结构体,它等同于
         *  CMVideoFormatDescriptionRef
         *  SPS在索引0处;PPS在索引1处
         */
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        size_t SPSSize, SPSCount;
        const uint8_t *sps;
        OSStatus retStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sps, &SPSSize, &SPSCount, 0);
        if (retStatus == noErr) {
            size_t PPSSize, PPSCount;
            const uint8_t *pps;
            retStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pps, &PPSSize, &PPSCount, 0);
            if (retStatus == noErr) {
                NSData *spsData = [NSData dataWithBytes:sps length:SPSSize];
                NSData *ppsData = [NSData dataWithBytes:pps length:PPSSize];
                
                // 保存sps和pps
                [mySelf saveSPS:spsData pps:ppsData];
            }
        }
    }
    
    // CMBlockBufferRef表示一个内存块,用来存放编码后的音频/视频数据
    CMBlockBufferRef dataBlockRef = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t lenght,totalLenght;
    char *dataptr;
    // 获取指向内存块数据的指针
    OSStatus status1 = CMBlockBufferGetDataPointer(dataBlockRef, 0, &lenght, &totalLenght, &dataptr);
    if (status1 == noErr) {
        size_t bufferOffset = 0;
        static const int AACStartCodeLenght = 4;
        /** 一次编码可能会包含多个nalu
         *  所以要循环获取所有的nalu数据,并解析出来
         *  每个NALU的格式为
         *  四字节(NALU总长度)+视频数据(NALU总长度-4)
         *  和正规的h264的nalu封装格式0001开头的有点不一样
         */
        while (bufferOffset < totalLenght - AACStartCodeLenght) {
            uint32_t naluUnitLenght = 0;
            // 读取该NALU的数据总长度,该NALU就是一帧完整的编码的视频
            memcpy(&naluUnitLenght, dataptr+bufferOffset, AACStartCodeLenght);
            
            // 返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
            // 从大端转系统端(必须,否则会造成长度错误问题)
            naluUnitLenght = CFSwapInt32BigToHost(naluUnitLenght);
            // 将真正的编码后的视频帧提取出来
            NSData *data = [[NSData alloc] initWithBytes:(dataptr + bufferOffset + AACStartCodeLenght) length:naluUnitLenght];
            
            // 然后添加0001开头码组成正规的h264封装格式
            [mySelf saveEncodedData:data isKeyFrame:keyframe];
            
            // 循环读取
            bufferOffset += AACStartCodeLenght + naluUnitLenght;
        }
    }
}

备注:
1、编码的数据都存储在CMSampleBufferRef对象变量sampleBuffer中,要注意一次编码可能会包含多个nalu
2、h264码流文件存储顺序要注意下,一定要按照sps pps I帧 p帧 p帧/b帧....sps pps I帧 p帧 p帧/b帧....的顺序,否则会导致无法播放

导出保存的h264文件,使用ffplay命令播放

由于只能使用手机进行视频采集,所以需要将保存在真机中的文件导出来,具体方法为:


1564319045580.jpg

然后使用ffplay 播放,命令如下
ffplay -f h264 /Users/feipai1/Desktop/qwe.media\ 2019-07-28\ 14:14.36.613.xcappdata/AppData/Documents/abc.h264

遇到问题

1、创建编码器时返回-12902错误;主要是因为宽高的参数没有设置,正确设置即可
2、编码后的视频出现马赛克;因为码率上限值设置不正确导致,正确设置方式为,kVTCompressionPropertyKey_DataRateLimits必须对应一个数组
int bitRateLimits = avgbitRate; // 一秒钟的最大码率
NSArray *limit = @[@(bitRateLimits * 1.5), @(1)];
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);

项目地址

参考VideoEnDecodeViewController.h/.m文件中代码
Demo

参考文章

http://www.enkichen.com/2017/11/26/image-h264-encode/
http://www.enkichen.com/2018/03/24/videotoolbox/
https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/04_MediaCapture.html#//apple_ref/doc/uid/TP40010188-CH5-SW2

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

推荐阅读更多精彩内容