1、引言 调用系统VideoToolbox的API实现一个硬编很容易,仔细看看文档、了解API的使用实现一个基本功能相信难不倒大家。但实际工作中有许多细节,一不注意就会掉坑里,甚至有些系统性问题难以解决。本文一方面会介绍必备的基础知识,带大家对编码有一个基本的认识,另一方面也会分享直播SDK在VT硬编实现上遇到的问题和解决方案,希望能帮助到大家。2、必备基础知识2。1帧概念I帧(帧内编码图像帧)即帧内(Intra)图像,采用帧内编码,不参考其它图像,但可作为其它类型图像的参考帧。P帧(预测编码图像帧)即预测(Predicted)图像,采用帧间编码,参考前一幅I或P图像,用作运动补偿。B帧(双向预测编码图像帧)即双向预测(Bipredicted)图像,提供最高的压缩比,它既需要之前的图像帧(I帧或P帧),也需要后来的图像帧(P帧),采用运动预测的方式进行帧间双向预测编码。2。2时间戳PTS:显示时间戳,主要用于视频的同步和输出,在渲染的时候使用,在没有Bframe的情况下DTS和PTS的输出顺序是一样的。DTS:解码时间戳,主要用于视频的解码,在解码阶段使用。CTSPTSDTS。 示例: gop I B B P B B P 显示顺序 1hr2hr3hr4hr5hr6hr7hr解码顺序 1hr3hr4hr2hr6hr7hr5hrPTS 1hr2hr3hr4hr5hr6hr7hrDTS 1hr3hr4hr2hr6hr7hr52。3GOPReference GOP:一段时间内图像变化不大的图像集我们就可以称之为一个序列,gop就是一组视频帧,其中第一个I帧我们称为是IDR帧。 Reference:参考周期,指两个P帧之间的距离,iOS硬件编码器中无法指定。2。4IDR 一个GOP的第一个帧称IDR帧(立即刷新帧),IDR帧的作用是立刻刷新,使错误不致传播。从IDR帧开始,重新算一个新的序列开始编码。而I帧不具有随机访问的能力,这个功能是由IDR承担。IDR帧会导致DPB(DecodedPictureBuffer参考帧列表这是关键所在)清空,而I不会。 IDR帧一定是I图像,但I帧不一定是IDR图像。一个序列中可以有很多的I帧图像,I帧图像之后的图像可以引用I帧图像之间的图像做运动参考。2。5码率控制ABR:平均目标码率,简单场景分配较低bit,复杂场景分配足够bit,使得有限的bit数能够在不同场景下合理分配,这类似VBR。同时一定时间内,平均码率又接近设置的目标码率,这样可以控制输出文件的大小,这又类似CBR。可以认为是CBR和VBR的折中方案,这是大多数人的选择。特别在对质量和视频带宽都有要求的情况下,可以优先选择该模式,一般速度是VBR的两倍到三倍,相同体积的视频文件质量却比CBR好很多。适用场景:ABR在直播和低延时系统用的比较多,因为只编码了一次,所以速度快,同时兼顾了视频质量和带宽,对于转码速度有要求的情况下也可以选择该模式。特点:视频质量整体可控,同时兼顾了视频码率和速度,是一个折中方案,实际用的比较多;使用过程一般要让调用方设置,最低码率、最高码率和平均码率,这些值要尽可能设置合理点。VBR:(VariableBitRate)可变码率,简单场景分配比较大的QP,压缩率小,质量高。复杂场景分配较小QP。得到基本稳定的视觉质量,因为人眼本来就对复杂场景不敏感,缺点在于输出码率大小不可控。适用场景:VBR适用于那些对带宽和编码速度不太限制,但是对质量有很高要求的场景。特别是在运动的复杂场景下也可以保持比较高的清晰度且输出质量比较稳定,适合对延时不敏感的点播,录播或者存储系统。特点:码率不稳定,质量基本稳定且非常高;编码速度一般比较慢,点播、下载和存储系统可以优先使用,不适合低延时、直播系统;这种模型完全不考虑输出的视频带宽,为了质量,需要多少码率就占用多少,也不太考虑编码速度。CBR:(ConstantBitRate)恒定码率,一定时间范围内比特率基本保持的恒定,属于码率优先模型。适用场景:一般也不建议使用这种方式,虽然输出的码率总是处于一个稳定值,但是质量不稳定,不能充分有效利用网络带宽,因为这种模型不考虑视频内容的复杂性,把所有视频帧的内容统一对待。但是有些编码软件只支持固定质量或者固定码率方式,有时不得不用。用的时候在允许的带宽范围内尽可能把带宽设置大点,以防止复杂运动场景下视频质量很低,如果设置的不合理,在运动场景下直接就糊了。特点:码率稳定,但是质量不稳定,带宽有效利用率不高,特别当该值设置不合理,在复杂运动场景下,画面非常模糊,非常影响观看体验。2。6编码数据裸流 H264的码流结构它主要有两种格式:AnnexB和AVCC。AnnexB格式以0x000001或0x00000001开头,AVCC格式以所在的NALU的长度开头,以AnnexB为例。 但对于一个H。264裸流来说,就是一系列NALU的集合,每个NALU既可以表示图像数据,也可以表示处理图像所需要的参数数据。 NALU结构分为视频编码层(VCL)和网络适配层(NAL): 视频编码层(VCL即VideoCodingLayer):负责高效的视频内容表示,这是核心算法引擎,其中对宏块、片的处理都包含在这个层级上,它输出的数据是SODB。 网络适配层(NAL即NetworkAbstractionLayer):以网络所要求的恰当方式对数据进行打包和发送,比较简单,先报VCL吐出来的数据SODB进行字节对齐,形成RBSP,最后把RBSP数据前面加上NAL头则组成一个NALU单元。 NALUNALUHeaderRBSP 但严格来讲NALUNALUHeaderEBSP,而EBSP防竞争的RBSP,H。264规范规定,编码器吐出来的数据需要在每个NALU添加起始码:0x000001或者0x00000001,用来指示一个NALU的起始,0x000000时,也可以表示当前NALU的结束,如果NALU内部存在0x000001or0x000000时,就要通过插入一个新的字节0x03防竞争。 NALUHeaderforbiddenbit(1bit)nalreferencebit(2bits)(优先级)nalunittype(5bits)(类型) NALU类型: NALU的类型即RBSP可以承载的数据类型。 NaluType NALU内容 备注 0hr未指定 1hr非IDR图像编码的slice 比如普通I、P、B帧 2hr编码slice数据划分A 2类型时,只传递片中最重要的信息,如片头,片中宏块的预测模式等;一般不会用到; 3hr编码slice数据划分B 3类型是只传输残差;一般不会用到; 4hr编码slice数据划分C 4时则只可以传输残差中的AC系数;一般不会用到; 5hrIDR图像中的编码slice IDR帧,IDR一定是I帧但是I帧不一定是IDR帧。 6hrSEI补充增强信息单元 可以存一些私有数据等; 7hrSPS序列参数集 SPS对如标识符、帧数以及参考帧数目、解码图像尺寸和帧场模式等解码参数进行标识记录 8hrPPS图像参数集 PPS对如熵编码类型、有效参考图像的数目和初始化等解码参数进行标志记录。 9hr单元定界符 视频图像的边界 10hr序列结束 表明下一图像为IDR图像 11hr码流结束 表示该码流中已经没有图像 12hr填充数据 哑元数据,用于填充字节 1323 保留 2431 未使用 VCL输出的原始数据比特流SODB即StringOfDataBits,其长度不一定是8bit的整数倍,为了凑成整数个字节,往往需要对SODB最后一个字节进行填充形成RBSP,最后一个不满8bit的字节第一bit位置1,然后后面缺省的bit置0即可。 接着我们再从层次结构理解码率的构成 帧:一副图像编码后的视频数据也叫做一帧,其中有I帧、B帧、P帧。 片:一帧图像又可以划分为很多片,由一个片或者多个片组成。 宏块:视频编码的最小处理单元,承载了视频的具体YUV信息,一片由一个或者多个宏块组成。 C音视频学习资料免费获取方法:关注音视频开发T哥,点击链接即可免费获取2023年最新C音视频开发进阶独家免费学习大礼包!3、VideoToolbox 介绍一下VideoToolBox及关键接口的使用,如果对接口使用很清楚的同学可以直接跳过看提炼部分或后续章节。3。1使用说明 第一步:VTCompressionSessionCreate创建视频编码器并设置编码器初始属性。NSDictionarypixelBufferOptions{(NSString)kCVPixelBufferPixelFormatTypeKey:(cvPixelFormatTypeValue),(NSString)kCVPixelBufferWidthKey:(framewidth),(NSString)kCVPixelBufferHeightKey:(frameheight),(NSString)kCVPixelBufferOpenGLESCompatibilityKey:YES,(NSString)kCVPixelBufferIOSurfacePropertiesKey:{}};CMVideoCodecTypecodecType(avctxcodecidAVCodecIDH264?kCMVideoCodecTypeH264:kCMVideoCodecTypeByteVC1);errVTCompressionSessionCreate(kCFAllocatorDefault,内存分配器,设置为默认分配framewidth,pixel的宽frameheight,pixel的高codecType,编码器类型(h264h265)encoderSpecifications,指定必须使用特定的编码器。一般传NULL即可。videotoolbox会自己选择(bridgeCFDictionaryRef)pixelBufferOptions,原始视频数据需要的属性,系统会根据这个创建一个pixelbufferpool如传NULL将不会创建,可能会增加不必要的copyNULL,压缩后的内存分配器,固定传NULLcompressionOutputCallback,编码数据的输出回调this,传递的参数session编码器session对象);if(errnoErr){compressionSessionsession;constint32tvgop;4secondkfiCFNumberRefrefCFNumberCreate(NULL,kCFNumberSInt32Type,v);设置I帧间隔,目前是4errVTSessionSetProperty(session,kVTCompressionPropertyKeyMaxKeyFrameInterval,ref);CFRelease(ref);}if(errnoErr){CFBooleanRefallowFrameReoderingavctxhasbframes?kCFBooleanTrue:kCFBooleanFalse;为了对B帧进行编码,视频编码器必须对帧进行重新排序,默认为True。将此设置为false可以防止帧重新排序。简单讲:用来设置是否编B帧,Highprofile支持B帧,目前开启状态errVTSessionSetProperty(session,kVTCompressionPropertyKeyAllowFrameReordering,allowFrameReodering);}if(errnoErrfps0){constintfpsfps;CFNumberRefrefCFNumberCreate(NULL,kCFNumberSInt32Type,fps);期望帧率,不用于控制帧率,只是作为提示提供给编码器,目前是15errVTSessionSetProperty(session,kVTCompressionPropertyKeyExpectedFrameRate,ref);CFRelease(ref);}if(errnoErr){constintvbitrate;CFNumberRefrefCFNumberCreate(NULL,kCFNumberSInt32Type,v);设置平均码率恒定(ABR)在一定的时间范围内达到设定的码率,但是局部码率峰值可以超过设定的码率errVTSessionSetProperty(session,kVTCompressionPropertyKeyAverageBitRate,ref);CFRelease(ref);kVTCompressionPropertyKeyDataRateLimits配置和输出B帧有冲突if(!avctxhasbframes){if(available(iOS8。2,)){intvbitratekLimitToAverageBitRateFactor8;CFNumberRefbytesCFNumberCreate(kCFAllocatorDefault,kCFNumberSInt32Type,v);字节数v1;1sCFNumberRefdurationCFNumberCreate(kCFAllocatorDefault,kCFNumberSInt32Type,v);CFMutableArrayReflimitCFArrayCreateMutable(kCFAllocatorDefault,2,kCFTypeArrayCallBacks);CFArrayAppendValue(limit,bytes);CFArrayAppendValue(limit,duration);用来设置硬性码率限制,实际做的就是设置码率的硬性限制是每秒码率不超过平均码率的2(kLimitToAverageBitRateFactor)倍VTSessionSetProperty(session,kVTCompressionPropertyKeyDataRateLimits,limit);CFRelease(bytes);CFRelease(duration);CFRelease(limit);}}}if(errnoErr){质量水平1、BaselineProfile:基本画质。支持IP帧,只支持无交错(Progressive)和CAVLC;2、Extendedprofile:进阶画质。支持IPBSPSI帧,只支持无交错(Progressive)和CAVLC;(用的少)3、Mainprofile:主流画质。提供IPB帧,支持无交错(Progressive)和交错(Interlaced),也支持CAVLC和CABAC的支持;4、Highprofile:高级画质。在mainProfile的基础上增加了8x8内部预测、自定义量化、无损视频编码和更多的YUV格式;errVTSessionSetProperty(session,kVTCompressionPropertyKeyProfileLevel,profileLevelstring);}if(errnoErr!usebaselineavctxcodecidAVCodecIDH264){H。264压缩的熵编码模式。kVTH264EntropyModeCAVLC(ContextbasedAdaptiveVariableLengthCoding)orkVTH264EntropyModeCABAC(ContextbasedAdaptiveBinaryArithmeticCoding)CABAC通常以较高的计算开销为代价提供更好的压缩errVTSessionSetProperty(session,kVTCompressionPropertyKeyH264EntropyMode,kVTH264EntropyModeCABAC);}if(errnoErr){用来设置编码器的工作模式是实时还是离线实时:延迟更低,但压缩效率会差一些,要求实时性高的场景需要开启离线则编得慢些,延迟更大,但压缩效率会更高。本地录制视频文件可以使用离线模式目前是关闭状态errVTSessionSetProperty(session,kVTCompressionPropertyKeyRealTime,enablerealtime?kCFBooleanTrue:kCFBooleanFalse);}if(errnoErravctxhasbframes){if(available(iOS12。0,)){在一个GOP里面的某一帧在解码时要依赖于前一个GOP中的某一些帧,这种GOP结构叫做OpenGOP。一般码流里面含有B帧的时候才会出现OpenGOP,OpenGOP以一个或多个B帧开始,参考之前GOP的P帧和当前GOP的I帧我们通常用的是CloseGOPCloseGOP中的帧不可以参考其前后的其它GOP一般以I帧开头errVTSessionSetProperty(session,kVTCompressionPropertyKeyAllowOpenGOP,enableopengop?kCFBooleanTrue:kCFBooleanFalse);}}准备编码if(errnoErr){errVTCompressionSessionPrepareToEncodeFrames(session);} 第二步:当视频数据来了以后,调用VTCompressionSessionEncodeFrame开始编码。CMTimepresentationTimeStampCMTimeMake(timestampms,1000);CFDictionaryRefframePropertiesnullptr;强制产生I帧if(forceKeyframe){CFTypeRefkeys〔〕{kVTEncodeFrameOptionKeyForceKeyFrame};CFTypeRefvalues〔〕{kCFBooleanTrue};framePropertiesCFDictionaryCreate(kCFAllocatorDefault,keys,values,1,kCFTypeDictionaryKeyCallBacks,kCFTypeDictionaryValueCallBacks);forceKeyframefalse;}CMTimedurCMTimeMake(1,fps);OSStatusstatusVTCompressionSessionEncodeFrame(session,会话pixelBuffer,视频帧数据presentationTimeStamp,当前帧的ptsdur,帧间隔时间frameProperties,帧的额外属性nullptr,固定设置nullptrflags接受额外编码信息); 第三步:处理编码后的输出回调数据。staticvoidcompressionOutputCallback(voidoutputCallbackRefCon,voidsourceFrameRefCon,OSStatusstatus,VTEncodeInfoFlagsinfoFlags,CMSampleBufferRefsampleBuffer){} 从编码器出来的数据从下面的Callback中拿到,系统为我们封装成了CMSampleBufferRef,我们要处理的数据都在其中,系统也很友好的提供了一些get方法方便我们获取想要的数据。 比如:时间信息CMTimedtsCMSampleBufferGetDecodeTimeStamp(sampleBuffer);CMTimeptsCMSampleBufferGetPresentationTimeStamp(sampleBuffer);参数信息获取SPS信息sizetsparameterSetSize,sparameterSetCount;constuint8tsparameterSet;CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,0,sparameterSet,sparameterSetSize,sparameterSetCount,0);获取PPS信息sizetpparameterSetSize,pparameterSetCount;constuint8tpparameterSet;CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,1,pparameterSet,pparameterSetSize,pparameterSetCount,0);编码数据CMBlockBufferRefblockCMSampleBufferGetDataBuffer(sampleBuffer);charbufferData;sizetsize;CMBlockBufferGetDataPointer(block,0,NULL,size,bufferData);3。2提炼总结iOS11系统开始对H。265的硬编硬解的支持。但是并不是所有的iOS设备升级到iOS11都可以使用H。265的硬编解功能,H。264硬解最少需要A9芯片的iPhone6siPhone6sPlusiPhoneSE,H。265硬编则最少需要A10芯片的iPhone7iPhone7Plus。设置期望帧率,不用于控制帧率,只是作为提示提供给编码器,1s输入多少帧就输出多少帧。iOS硬编H264不支持opengop,支持opengop的条件是:H265B帧iOS12系统,然而iOS编出的I帧都是IDR帧,因此opengop在iOS实际的设置中也没有起到作用,H265也同样如此。kVTCompressionPropertyKeyDataRateLimits配置和输出B帧有冲突,使用时需要注意,否则将编不出B帧。kVTCompressionPropertyKeyDataRateLimits需要设置,否则kVTCompressionPropertyKeyAverageBitRate设置的码率不受控。系统提供了CMSampleBufferRef,CMSampleBufferRefCMBlockBufferCMVideoFormateDescCMTime:CMBlockBuffer存放着NALU数据;CMVideoFormateDesc存放着spspps信息;CMTime存放着ptsordts信息;硬编出来的数据格式为AVCC格式,而自研rtmp库仅支持AnnexB,所以我们需要做的是AVCC转AnnexB。4、避坑指南4。1属性设置失败 在设置编码器属性时,要充分考虑到属性与属性间的互斥性,以及属性与h264h265的互斥性,如果不清楚这些,你写的编码器代码可能会导致最终的不确定性bug出现。 其实这个在上面有也有提到,这里再次强调一遍,因为这类问题没什么难道可言但排除起来又可能耗时耗力。iOS硬编h264不支持opengop,支持opengop的条件是:h265B帧iOS12系统;kVTCompressionPropertyKeyDataRateLimits配置和输出B帧有冲突,使用时需要注意,否则将编不出B帧,且kVTCompressionPropertyKeyDataRateLimits在系统8。2及以上可用;4。2Gop不符合预期 我们知道控制gopVT提供两个属性kVTCompressionPropertyKeyMaxKeyFrameInterval帧率控制kVTCompressionPropertyKeyMaxKeyFrameIntervalDuration时间间隔控制 起初用kVTCompressionPropertyKeyMaxKeyFrameInterval控制,当受到性能影响帧率不足预期帧率时gop自然也会受到一定影响,这也是影响gop的一个因素,后来我们引入kVTCompressionPropertyKeyMaxKeyFrameIntervalDuration并想通过这两个共同控制gop行为,但最终的行为依然是不符合预期的,先看一下文档怎么说。 简单讲,文档告诉我们从一个关键帧到下一个关键帧的最长持续时间(秒)。默认为零,没有限制。当帧速率可变时,此属性特别有用。此key可以与kVTCompressionPropertyKeyMaxKeyFrameInterval一起设置,并且将强制执行这两个限制每X帧或每Y秒需要一个关键帧,以先到者为准。 然而,当我们fps是15,kVTCompressionPropertyKeyMaxKeyFrameInterval设置30kVTCompressionPropertyKeyMaxKeyFrameIntervalDuration设置3。5s,按照文档理解gop间隔为2s帧率不过时也不会超过3。5s,但我们发现gop是以3。5s生效的,并没有像文档中介绍的那样comesfirst。4。3编码卡死 现象 编码器卡死问题一直以来是各个团队一个共性问题,目前看这类问题主要发生在多个编码器session共存的情况,一旦处理出现冲突就会卡住,怀疑是系统内部在等待共用的资源,而且这个问题可能存在于多种情况,目前我们还无法完全解决,这里可以提供一个主要路径的规避方案。 通过分析卡死的堆栈和用户行为情况,我们发现存在多个编码器session时卡死的概率会很高,当然并不是说多个session不被允许,通过模拟实验最终我们发现需要满足以下几个条件,卡死会必现:EncodeSessionA和EncodeSessionB在不同的线程上被创建EncodeSessionA的生命周期比EncodeSessionB长EncodeSessionA创建后没有视频帧进行编码当前没有问题,等再次启动SessionB并编码时必现卡死 原因 定性为系统性问题 方案 确定要编码时再createsession,避开系统行为4。4码率编不上去 现象 在我们没有解决这个问题前,经常有线上用户反馈画面模糊的问题,跟过一些case,共性是h265编码码率低,动态场景下码率600以下,静态场景甚至不到100都有可能,只能让用户重启手机解决问题。 后来我们进行了线下的测试找到了必现的手机,起初怀疑是跟系统版本和机型相关,尝试用相同机型和系统版本都没有复现,说明和系统版本、机型无关,接着我们用WebRtcdemo进行测试发现输入30帧最终丢到了5帧,对比和我们使用方式的不同点在于RealTime的开启,而在直播demo上如果开启RealTime也会遇到丢帧问题,通过不断排查最终找到了解决方案。 原因 hevc硬编时间戳的处理精度有bug,时间戳绝对值太大,会导致编码码率上不去、开启RealTime编码器甚至会丢帧,而时间戳过大的原因跟开机时间有关,这也可以解释重启手机能恢复的原因。 方案 给编码器的pts去掉了最高位来避免该系统问题4。5录屏h265编码码率低 现象 录屏系统输出的帧率不稳定,动态时60帧、静态时20帧,静动态切换时帧率会很不稳定,业务会进行丢帧、补充逻辑,基于这个背景业务再放h25630帧时会出现码率低到900k左右导致画面模糊。 原因帧率不稳定时会影响码率;帧率稳定,但帧携带的pts时间戳是不稳定的(由于丢帧补帧逻辑,导致pts会有问题,比如:重复、甚至回退),依然会导致码率低; 方案 丢帧、补帧逻辑解耦,pts不依赖录屏出帧和丢帧、补帧逻辑,直接编码器获取当前的时间戳传入编码器。5、总结 通过对这篇文章的阅读,相信大家对编码的基本概念、iOSVT硬编的使用、实际工作中可能遇到的难题都有一定的了解了,如果大家有更多的问题或者经验欢迎留言交流。 参考文献 《音视频压缩:H264码流层次结构和NALU详解》(https:cloud。tencent。comdeveloperarticle1746993)引言 调用系统VideoToolbox的API实现一个硬编很容易,仔细看看文档、了解API的使用实现一个基本功能相信难不倒大家。但实际工作中有许多细节,一不注意就会掉坑里,甚至有些系统性问题难以解决。本文一方面会介绍必备的基础知识,带大家对编码有一个基本的认识,另一方面也会分享直播SDK在VT硬编实现上遇到的问题和解决方案,希望能帮助到大家。 原文链接:iOSVideoToolbox硬编指南