1. 从软编码到硬编码:为什么iOS视频编码必须拥抱硬件
如果你在iOS上做过视频录制或者直播,大概率遇到过这样的场景:用AVFoundation的AVCaptureSession录个1080p 30fps的视频,CPU占用率低得感人,手机也不怎么发热。但如果你尝试用Core Image或者Metal实时处理每一帧画面,再用VideoToolbox的软件编码器(比如VTCompressionSession)去压成H.264,手机很快就会变成暖手宝,帧率也开始跳水。这个现象背后,就是软件编码与硬件编码最直观的差异。今天我们不聊那些高深的算法,就从一个一线开发者的角度,掰开揉碎了讲讲iOS上的视频硬件编码到底是怎么回事,以及怎么把它用好、用稳。
视频编码,本质上是个计算密集型的体力活。它要把每一帧画面(一堆原始的像素数据)压缩成体积小得多的比特流。软件编码,比如开源的x264,是在CPU上用复杂的算法(运动估计、变换、量化、熵编码)一步步算出来的,精度高、可控性强,但代价就是CPU算力被大量占用,功耗飙升。而硬件编码,则是把编码这个特定任务,交给了手机里一个专门的电路模块——视频编码器(Video Encoder),这个模块被集成在GPU或者专用的多媒体处理单元(如Apple的媒体引擎)里。它的工作方式更像是“流水线作业”或者“查表”,用固定的硬件逻辑和电路去执行编码流程,所以速度极快、功耗极低,但灵活性和编码质量的上限通常不如顶级的软件编码器。
在移动端,尤其是iOS生态里,硬件编码不是“可选项”,而是“必选项”。原因很简单:续航和发热。苹果对App的后台活动、CPU占用、能耗有着极其严格的监控和限制。一个频繁触发CPU高占用的视频编码操作,轻则导致设备发烫、帧率不稳,重则直接被系统“杀掉”进程。因此,无论是系统自带的相机App、AVFoundation框架,还是第三方如微信视频通话、抖音直播,其视频编码的默认和首选路径,一定是硬件编码。理解并掌握它,是开发一个体验合格的iOS视频相关功能的基石。
2. 核心框架VideoToolbox:硬件编码的通行证
在iOS上,所有底层的视频编解码操作,无论是硬件加速还是软件回退,都通过一个名为VideoToolbox的框架来对接。你可以把它想象成一个“翻译官”或者“调度中心”。你的App告诉VideoToolbox:“我想把这一堆CVPixelBuffer(像素缓冲区)压缩成H.264格式,码率2Mbps。” VideoToolbox则负责去调用系统底层最合适的编码器(优先硬件编码器)来完成任务,并把编码后的数据(CMSampleBuffer)返回给你。它抽象了底层硬件的具体实现,提供了一套统一的C语言API。
2.1 VideoToolbox的关键会话:VTCompressionSession
硬件编码的核心是创建一个VTCompressionSessionRef会话。这个会话对象管理着编码的整个生命周期和参数配置。创建它的时候,有几个参数至关重要:
- allocator: 通常传
kCFAllocatorDefault,用系统默认的内存分配器。 - width & height: 编码视频的宽高。这里有个大坑:这个宽高必须和你要输入的
CVPixelBuffer的宽高严格一致,否则编码会失败或产生异常。通常,你需要从摄像头或图像处理管线拿到CVPixelBuffer后,先检查它的尺寸。 - codecType: 指定编码格式。对于硬件编码,最常用的是:
kCMVideoCodecType_H264: 最通用,兼容性最好。kCMVideoCodecType_HEVC(也叫H.265): 同等画质下码率比H.264低约50%,但需要iOS 11+,且播放端也需要支持。在存储空间敏感(如本地录像)或带宽敏感(如直播)的场景下优势明显。
- **encoderSpecification
: 这个参数通常设为NULL,让系统自动选择编码器。但你可以通过它来**强制指定使用软件编码器**(用于测试或特殊需求),例如传入一个包含kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder键且值为kCFBooleanFalse`的字典。不过,在真机上,我们几乎总是希望系统选择硬件编码器。 - outputCallback: 一个回调函数指针。这是编码器的“输出管道”。当一帧图像被编码完成后,VideoToolbox会调用这个回调函数,并把编码后的数据(可能是单个NAL单元,也可能是切片)传递给你。这是你拿到编码数据唯一的地方。
- outputCallbackRefCon: 传递给上述回调函数的自定义指针,通常是你封装的一个类或结构体的上下文对象,用于在C回调函数中访问Objective-C/Swift的对象和方法。
创建会话的代码结构大致如下(以Swift为例,展示逻辑):
import VideoToolbox var compressionSession: VTCompressionSession? let status = VTCompressionSessionCreate( allocator: kCFAllocatorDefault, width: Int32(videoWidth), height: Int32(videoHeight), codecType: kCMVideoCodecType_H264, encoderSpecification: nil, // 自动选择,优先硬件 imageBufferAttributes: nil, // 输入像素缓冲区的属性,通常nil compressedDataAllocator: nil, outputCallback: myOutputCallback, // 你的回调函数 refcon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()), // 传递self compressionSessionOut: &compressionSession ) guard status == noErr, let session = compressionSession else { // 处理创建失败 return }2.2 配置编码参数:平衡质量、码率与延迟
创建会话后,下一步是配置编码参数。这是决定视频最终观感和文件大小的关键。通过VTSessionSetProperty函数来设置。有几个核心属性你必须了解:
kVTCompressionPropertyKey_RealTime(Bool): 设为true。这是为实时编码(如直播、通话)优化的关键开关。开启后,编码器会优先考虑低延迟,可能会牺牲一点压缩效率。kVTCompressionPropertyKey_ProfileLevel(String): 指定H.264的档次和级别。对于主流兼容,常用kVTProfileLevel_H264_Main_AutoLevel或kVTProfileLevel_H264_High_AutoLevel。AutoLevel会让系统根据你设置的其他参数(如分辨率、帧率、码率)自动选择一个合规的级别。kVTCompressionPropertyKey_AverageBitRate(Int):平均码率,单位是bps(比特每秒)。这是最重要的质量控制参数之一。例如,1080p 30fps的视频,设置2_000_000(2Mbps)是一个常见的起点。码率越高,画面质量越好,但文件体积或网络带宽占用也越大。kVTCompressionPropertyKey_DataRateLimits([Int]): 这是一个更精细的码率控制参数,是一个包含两个NSNumber的数组:[bytesPerSecond, seconds]。它表示在seconds秒的时间窗口内,平均码率不超过bytesPerSecond字节/秒。它比AverageBitRate限制得更严格,常用于需要严格控码的直播场景。注意单位是字节,而AverageBitRate是比特。kVTCompressionPropertyKey_ExpectedFrameRate(Float): 期望的帧率。告诉编码器你预计输入的帧率是多少,有助于其进行内部缓冲和码率分配。kVTCompressionPropertyKey_MaxKeyFrameInterval(Int): 最大关键帧(I帧)间隔,单位是帧数。例如设置为30,意味着每30帧至少会强制插入一个关键帧。关键帧是完整编码的一帧,解码时不依赖其他帧。在直播中,这个值不宜过大(比如60或30),方便新观众快速加入(秒开)和网络丢包后的恢复。在本地录像中,可以适当增大以提升压缩率。kVTCompressionPropertyKey_AllowFrameReordering(Bool): 是否允许帧重排序。为了压缩效率,编码器可能会采用B帧(双向预测帧),这会导致编码顺序和显示顺序不同。设为false可以禁止B帧,降低编码复杂度和解码延迟,适合实时通信。设为true(默认)可以获得更好的压缩率。kVTCompressionPropertyKey_H264EntropyMode(String): H.264的熵编码模式。可选kVTH264EntropyMode_CAVLC(兼容性好,效率稍低)或kVTH264EntropyMode_CABAC(压缩效率高,但需要解码端支持)。通常硬件编码器默认且推荐使用CABAC。
配置代码示例:
let properties: [NSString: Any] = [ kVTCompressionPropertyKey_RealTime: true, kVTCompressionPropertyKey_ProfileLevel: kVTProfileLevel_H264_Main_AutoLevel, kVTCompressionPropertyKey_AverageBitRate: 2_000_000, kVTCompressionPropertyKey_ExpectedFrameRate: 30, kVTCompressionPropertyKey_MaxKeyFrameInterval: 30, kVTCompressionPropertyKey_AllowFrameReordering: false, // 实时场景禁用B帧 kVTCompressionPropertyKey_H264EntropyMode: kVTH264EntropyMode_CABAC, ] for (key, value) in properties { VTSessionSetProperty(session, key: key, value: value as CFTypeRef) }注意:这些属性设置必须在调用
VTCompressionSessionPrepareToEncodeFrames之前完成,否则可能不生效。另外,不同iOS版本、不同设备型号的硬件编码器对参数的支持程度可能有细微差异,最稳妥的方式是在设置后检查一下状态码。
3. 编码流程实战:从PixelBuffer到NAL单元
配置好会话和参数后,就进入了实际的编码循环。这个过程可以概括为:获取图像 -> 送入编码器 -> 在回调中接收编码数据。
3.1 输入图像数据
你的视频源可能来自AVCaptureVideoDataOutput、Metal渲染结果、或者从文件读取的图像。无论来源如何,最终都需要一个CVPixelBufferRef。这是Core Video框架中表示图像内存的对象。
将一帧图像送入编码器的函数是VTCompressionSessionEncodeFrame。你需要提供:
session: 编码会话。imageBuffer: 包含图像数据的CVPixelBuffer。presentationTimeStamp(CMTime): 该帧的显示时间戳。这个时间戳必须单调递增,通常你可以用一个自增的帧计数乘以每帧的时长(如1/30秒)来计算。如果时间戳出现回退或跳跃过大,可能导致编码器内部状态混乱。duration(CMTime): 该帧的持续时间。对于固定帧率,可以传kCMTimeInvalid,编码器会使用配置的期望帧率。frameProperties(CFDictionaryRef): 可选的帧级属性。这是一个非常强大的功能,允许你为这一帧单独设置属性,覆盖会话级的设置。最常用的就是强制插入关键帧(I帧)。例如,在直播中,当有新的观众连接时,你可以通过设置下一帧的frameProperties为[kVTEncodeFrameOptionKey_ForceKeyFrame: true]来立即生成一个关键帧,实现“秒开”。sourceFrameRefCon&infoFlagsOut: 高级参数,通常传nil和nil。
let presentationTimeStamp = CMTime(value: frameCount, timescale: 30) // 假设30fps frameCount += 1 var frameProperties: [NSString: Any]? = nil if needForceKeyFrame { frameProperties = [kVTEncodeFrameOptionKey_ForceKeyFrame: true] needForceKeyFrame = false } let status = VTCompressionSessionEncodeFrame( compressionSession!, imageBuffer: pixelBuffer, presentationTimeStamp: presentationTimeStamp, duration: kCMTimeInvalid, frameProperties: frameProperties as CFDictionary?, sourceFrameRefCon: nil, infoFlagsOut: nil ) if status != noErr { print("编码帧失败: \(status)") }3.2 处理编码输出:解码回调函数
当编码器完成一帧(或一个切片)的编码后,会调用你注册的回调函数。这个函数运行在一个非主线程、高优先级的编码器内部线程上,所以你的处理要快,不要在这里做耗时的操作(如文件写入、网络发送),最好是快速将数据放入一个线程安全的队列,由另一个工作线程消费。
回调函数的签名是固定的:
typedef void (*VTCompressionOutputCallback)( void * CM_NULLABLE outputCallbackRefCon, void * CM_NULLABLE sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CM_NULLABLE CMSampleBufferRef sampleBuffer );在回调中,你需要:
- 检查
status和infoFlags。status为noErr表示成功。infoFlags中的kVTEncodeInfo_FrameDropped标志表示这一帧被编码器丢弃了(通常因为输入太快,编码器跟不上),这在实时编码中是正常现象。 - 从
CMSampleBuffer中提取编码后的数据。关键是通过CMSampleBufferGetDataBuffer拿到CMBlockBufferRef,这里面存放着实际的H.264 NAL单元数据。 - 进一步从
CMBlockBuffer中获取内存指针和数据长度。 - 区分关键帧和非关键帧。通过检查
CMSampleBuffer的附件CMSampleBufferGetSampleAttachmentsArray,可以判断这一帧是否是关键帧(kCMSampleAttachmentKey_NotSync为false或不存在)。这对于直播推流时组包(如RTMP的FLV Tag)至关重要,因为关键帧需要携带SPS和PPS参数集。 - 提取SPS和PPS。对于H.264,序列参数集(SPS)和图像参数集(PPS)包含了解码整个视频流所必需的信息(如分辨率、档次级别等)。它们只在关键帧之前输出,或者通过会话属性
kVTCompressionPropertyKey_OutputConfiguration设置后定期输出。你需要从CMSampleBuffer的formatDescription(CMVideoFormatDescriptionRef) 中提取它们,并妥善保存,在推流或写文件时,在关键帧之前发送出去。
一个简化的回调处理逻辑示例(伪代码):
let outputCallback: VTCompressionOutputCallback = { (outputCallbackRefCon, sourceFrameRefCon, status, infoFlags, sampleBuffer) in guard status == noErr, let sampleBuffer = sampleBuffer else { if status == kVTInvalidSessionErr { // 会话失效,需要重建 } return } // 检查是否被丢帧 if (infoFlags & VTEncodeInfoFlags.frameDropped.rawValue) != 0 { print("帧被丢弃") return } // 判断是否为关键帧 let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as? [[CFString: Any]] let isKeyFrame = !(attachments?.first?[kCMSampleAttachmentKey_NotSync] as? Bool ?? true) // 提取格式描述和参数集(关键帧时) if isKeyFrame, let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer) { var spsSize: Int = 0 var spsPtr: UnsafePointer<UInt8>? var ppsSize: Int = 0 var ppsPtr: UnsafePointer<UInt8>? // 获取SPS CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDescription, parameterSetIndex: 0, parameterSetPointerOut: &spsPtr, parameterSetSizeOut: &spsSize, parameterSetCountOut: nil, nalUnitHeaderLengthOut: nil) // 获取PPS CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDescription, parameterSetIndex: 1, parameterSetPointerOut: &ppsPtr, parameterSetSizeOut: &ppsSize, parameterSetCountOut: nil, nalUnitHeaderLengthOut: nil) if let spsPtr = spsPtr, let ppsPtr = ppsPtr { let spsData = Data(bytes: spsPtr, count: spsSize) let ppsData = Data(bytes: ppsPtr, count: ppsSize) // 保存或发送spsData和ppsData } } // 获取编码数据块 guard let dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else { return } var totalLength: Int = 0 var dataPointer: UnsafeMutablePointer<Int8>? let status = CMBlockBufferGetDataPointer(dataBuffer, atOffset: 0, lengthAtOffsetOut: nil, totalLengthOut: &totalLength, dataPointerOut: &dataPointer) guard status == kCMBlockBufferNoErr, let dataPointer = dataPointer else { return } let data = Data(bytes: dataPointer, count: totalLength) // 将data(包含一个或多个NAL单元)放入队列,供后续处理(组包、发送、写入文件) }3.3 结束编码与资源清理
当所有帧都编码完成后,你需要结束编码会话并释放资源。顺序很重要:
- 标记结束:调用
VTCompressionSessionCompleteFrames,告诉编码器不会再有任何新的输入帧了。编码器会处理完内部缓冲的所有帧。 - 完成编码:调用
VTCompressionSessionCompleteFrames后,等待所有剩余的输出在回调中被处理完毕。 - 无效化会话:调用
VTCompressionSessionInvalidate。这个调用会阻塞,直到编码器内部所有资源被安全释放。必须在所有对会话的操作(包括可能还在执行的回调)都完成后调用。 - 释放会话:最后,调用
CFRelease释放VTCompressionSessionRef对象。
// 1. 标记结束 VTCompressionSessionCompleteFrames(compressionSession, untilPresentationTimeStamp: .invalid) // 2. (等待自己的输出队列处理完所有剩余数据) // 3. 无效化 VTCompressionSessionInvalidate(compressionSession) // 4. 释放 compressionSession = nil4. 避坑指南与性能调优:来自一线的实战经验
理论流程看起来清晰,但真机调试时坑点不少。下面分享几个我踩过并总结出来的关键点。
4.1 颜色空间与像素格式的匹配问题
CVPixelBuffer有多种像素格式(OSType),如kCVPixelFormatType_32BGRA、kCVPixelFormatType_420YpCbCr8BiPlanarFullRange(NV12)等。摄像头采集的输出通常是NV12(一种YUV420格式),这是硬件编码器最高效、最直接支持的格式。如果你用Metal或Core Image处理得到了BGRA格式的CVPixelBuffer,直接送给硬件编码器,编码器内部需要先做一次颜色空间转换,这会增加额外的开销和延迟。
最佳实践:尽量保证输入给
VTCompressionSession的CVPixelBuffer的格式是kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange(NV12,电视标准色域)或kCVPixelFormatType_420YpCbCr8BiPlanarFullRange(NV12,全色域)。如果你必须处理BGRA,可以考虑在GPU(Metal)上完成到YUV的转换,或者使用vImage等CPU库转换,但后者性能损耗较大。
4.2 内存管理与循环引用
VideoToolbox API是C接口,大量使用Core Foundation(CF)对象。在Swift/Objective-C混编环境下,内存管理要格外小心。
- 创建与释放:
VTCompressionSessionCreate创建的会话,最终必须用CFRelease释放(或Swift中设置为nil)。CMSampleBuffer和CMBlockBuffer在回调函数中由系统传递给你,你不需要释放它们,除非你显式地Retain了。通常,在回调函数中提取出Data后,这些缓冲区就会被系统回收。 - 循环引用:在回调函数中通过
refcon指针拿到的self(你的编码器管理类),如果被强引用并在回调中操作,要小心形成循环引用。确保你的类在销毁时,先无效化编码会话,再释放自身。使用Unmanaged传递上下文时,要明确是passUnretained还是passRetained,并在适当的时候release。
4.3 实时编码下的码率控制与帧率稳定
硬件编码器虽然快,但在复杂运动场景或低光环境下,为了维持设定的码率,它可能会采取更激进的量化参数(QP),导致画面出现块状模糊(马赛克),或者主动丢帧以降低瞬时码率。
- 动态码率策略:不要死板地固定一个平均码率。可以根据网络状况(直播时)或场景复杂度(通过分析图像内容)动态调整
kVTCompressionPropertyKey_AverageBitRate。VideoToolbox支持会话中途动态修改此属性。 - 关注
DataRateLimits:对于直播,DataRateLimits比AverageBitRate更能防止码率“爆表”。例如,设置[bytesPerSecond: 300_000, seconds: 1]意味着任何1秒内的平均码率不会超过300KB/s(约2.4Mbps),这能有效平滑码流,避免网络拥塞。 - 帧率自适应:如果发现编码队列积压或回调中
kVTEncodeInfo_FrameDropped标志频繁出现,说明输入帧率超过了编码器的处理能力。此时应该主动降低视频源的采集帧率或分辨率,而不是硬塞给编码器。AVCaptureSession可以动态调整videoZoomFactor和frameRate。
4.4 多会话管理与后台处理
一个App内同时运行多个硬件编码会话(比如画中画录制、同时推流到两个平台)会对系统造成很大压力。虽然A12芯片及之后的设备媒体引擎能力很强,但仍需谨慎。
- 串行优于并行:如果可能,尽量让编码任务串行执行。比如先编码完一段视频,再开始下一段。
- 后台编码:App进入后台后,所有视频采集都会停止。如果你需要在后台完成最后一小段视频的编码(比如停止录制后的收尾),你需要申请
UIBackgroundTaskIdentifier,并在有限的时间内(通常几秒)尽快完成编码和文件写入。进入后台后,CPU资源受限,编码速度可能会下降。 - 会话重建:如果编码过程中发生中断(如电话打断、用户切到其他App),
VTCompressionSession可能会失效(回调返回kVTInvalidSessionErr)。你需要捕获这个错误,并重建整个编码会话。重建时,记得重新设置所有参数,并重新发送SPS/PPS(如果是直播流)。
4.5 HEVC(H.265)编码的特别注意事项
HEVC能大幅节省码率,但兼容性是首要考虑。
- 系统版本:
kCMVideoCodecType_HEVC需要iOS 11.0+。 - 硬件支持:iPhone 6s (A9芯片) 及之后的设备才支持HEVC硬件编码。使用前可以通过
VTIsHardwareDecodeSupported(针对解码)或尝试创建会话并检查错误来探测。 - 颜色深度:HEVC支持Main 10 Profile,即10位色深。如果你的内容源是10-bit(如HDR视频),可以尝试配置
kVTCompressionPropertyKey_ColorPrimaries、kVTCompressionPropertyKey_TransferFunction和kVTCompressionPropertyKey_YCbCrMatrix来保留更广的色彩空间。但播放端的支持情况更复杂。 - 文件封装:HEVC视频流需要封装在支持它的容器里,如MP4(需要
‘hvc1’或‘hev1’编解码器标识)、QuickTime MOV。使用AVAssetWriter时,需要指定正确的outputSettings。
5. 进阶话题:与上层框架的协同与调试技巧
在实际项目中,我们很少直接裸用VideoToolbox,而是将其与AVFoundation、Core Media等框架结合。
5.1 与AVAssetWriter结合:高效本地录像
AVAssetWriter是iOS上写媒体文件的高级API。它内部也使用硬件编码器。但直接使用VideoToolbox+AVAssetWriter的输入AVAssetWriterInput的mediaDataHandler或requestMediaDataWhenReady队列,可以给你更精细的控制。
典型流程:
- 创建
AVAssetWriter和AVAssetWriterInput(视频)。 - 配置
AVAssetWriterInput时,在outputSettings中指定编码格式(如AVVideoCodecType.h264),它会自动匹配系统最佳编码器(通常是硬件)。 - 但是,如果你已经有了
VideoToolbox编码后的CMSampleBuffer,你可以通过AVAssetWriterInput的append(_:)方法直接写入。这里有个关键:AVAssetWriterInput期望的CMSampleBuffer的格式描述(CMFormatDescription)必须与其outputSettings兼容。通常,从VideoToolbox回调拿到的、包含H.264数据的CMSampleBuffer可以直接追加。 - 这种方式的优势在于,你可以将编码和写入文件异步化。编码回调线程快速将数据放入一个队列,另一个线程用
AVAssetWriter消费队列并写入文件,避免I/O阻塞编码。
5.2 性能 profiling 与 Instruments 工具
如何知道你的硬件编码是否高效?Xcode的Instruments是利器。
- Time Profiler:查看CPU占用。一个健康的硬件编码过程,CPU占用应该很低(<10%),主要的
VTCompressionSessionEncodeFrame和回调函数占用时间极短。如果看到VideoToolbox相关的函数占用大量CPU时间,可能是参数配置不当或输入格式问题。 - Core Animation:查看帧率(FPS)。如果UI卡顿,可能是编码占用CPU太多,或者编码输出回调处理太慢,阻塞了主线程(虽然回调不在主线程,但如果你在回调中做了同步操作影响了UI线程)。
- Energy Log:查看能耗影响。硬件编码的能耗曲线应该比较平缓。如果出现频繁的高能耗峰值,可能是编码会话频繁创建销毁,或者码率设置过高导致芯片持续高负载。
5.3 编码延迟的测量与优化
对于实时通信,端到端延迟是关键。编码延迟是其中一部分。你可以粗略测量:
- 在将
CVPixelBuffer送入VTCompressionSessionEncodeFrame时,记录时间戳T1。 - 在编码输出回调中,收到对应的
CMSampleBuffer时,记录时间戳T2。 T2 - T1即为这一帧的编码延迟。在iPhone旗舰机型上,1080p的硬件编码延迟通常在5-20毫秒之间。
如果延迟过大,检查:
- 是否开启了
kVTCompressionPropertyKey_RealTime。 kVTCompressionPropertyKey_AllowFrameReordering是否设为false(禁用B帧)。- 输入帧率是否远超编码器处理能力(导致排队)。
- 输出回调函数是否处理太慢(导致编码器输出缓冲区满)。
最后,硬件编码虽然省心高效,但它是一个“黑盒”,可调的参数远不如x264等软件编码器丰富。它的优化更多体现在对系统特性的理解、参数组合的尝试以及对异常情况的稳健处理上。多在不同型号的真机上测试,关注发热量和内存变化,是打磨一个优秀视频编码模块的必经之路。