1. 为什么H.264至今仍是音视频开发绕不开的“地基”?
你写一个播放器,调用FFmpeg解码,底层大概率还是在和H.264打交道;你做嵌入式摄像头,芯片手册里写的“支持H.264 Baseline Profile”是硬指标;你调试Qt5.15的QVideoSink渲染,发现YUV数据流里夹着的NALU头,第一个字节永远是0x00000001——这串魔数背后,就是H.264的起始码。这不是历史遗留,而是现实选择:截至2024年,在安防IPC、车载DVR、远程医疗会诊终端、工业视觉检测设备这四大高可靠性场景中,H.264编码占比仍稳定在73%以上。不是因为没人推AV1或VVC,而是H.264把“压缩效率—计算开销—硬件兼容性”这个三角关系,钉死在一个极难被撼动的平衡点上。
我2013年第一次在TI DM8168开发板上跑通H.264解码时,用的是x264库的medium preset,单帧解码耗时18ms;到2024年,同一块板子刷上新固件,用同样的码流,耗时压到了4.2ms——性能涨了4倍多,但核心算法模块的函数名、宏定义、NALU解析逻辑,几乎没变。这就是H.264的“顽固性”:它不炫技,但足够扎实。你可能觉得“都2024年了还讲H.264太老”,可当你在Linux下用v4l2-ctl --set-fmt-video=width=1920,height=1080,pixelformat=H264配置USB摄像头,或者在Qt里写QMediaRecorder::setVideoCodec("video/x-h264")时,你调用的每一个API,背后都是对H.264规范第8章“Slice语法”的直接映射。所谓“老”,其实是成熟;所谓“基础”,其实是所有上层功能的承重墙。今天不把GOP怎么切、宏块怎么划分、帧内预测怎么选模式这些细节掰开揉碎,后面调参优化时连日志里的mb_type=I_PCM都看不懂,更别说解决“花屏卡顿是B帧参考链断裂还是SPS丢失”这种真问题。
提示:别被“H.264已过时”的舆论带偏。实际项目中,80%的音视频问题根源不在编解码器选型,而在对H.264底层机制理解不足导致的参数误配。比如把
keyint=30(GOP长度)设成keyint=1,看似关键帧密集利于seek,实则让I帧占比飙升至40%,码率暴涨且解码压力翻倍——这种坑,只看API文档根本填不上。
2. GOP:不只是“关键帧间隔”,它是时间维度上的压缩策略总控开关
很多人把GOP(Group of Pictures)简单等同于“I帧间隔”,这是最危险的误解。GOP本质是一组按特定依赖关系组织的帧集合,它的结构决定了整个视频流的时间冗余消除能力、随机访问粒度、错误恢复韧性,甚至影响硬件解码器的DMA搬运策略。一个典型的GOP结构如IBBPBBPBBP(P帧后跟两个B帧),但H.264标准允许更复杂的层级结构,比如IPPP...(无B帧)、IBBBPBBBP(多B帧),甚至I[PP]B[PP]B...(B帧参考双向P帧)。关键不在帧类型本身,而在帧间依赖图的拓扑结构。
我们拆解一个真实案例:某款4G执法记录仪要求“断网续传时丢弃中间B帧,仅保留I/P帧”。表面看是丢帧逻辑,实则暴露对GOP理解的致命缺陷——B帧的解码依赖前向I/P帧和后向P帧,若只保留I/P帧而丢弃B帧,解码器会因缺少参考帧而崩溃。正确做法是:在编码端将GOP设为IPPP...(即禁用B帧),此时每个P帧只依赖前一个I或P帧,丢弃任意P帧都不会导致后续帧解码失败。这说明GOP设计必须与业务场景强耦合:直播低延迟场景常用短GOP(如keyint=15),因B帧引入的解码延迟不可控;而监控录像存储场景则倾向长GOP(keyint=120),用B帧大幅降低码率,牺牲的是随机seek精度。
再深挖一层:GOP的“开放”与“闭合”特性直接影响错误传播。闭合GOP(Closed GOP)要求每个GOP内的I帧不参考前一个GOP的帧,这样网络丢包只影响当前GOP;开放GOP(Open GOP)允许B帧跨GOP参考,虽提升压缩率,但一个GOP的丢包可能污染后续多个GOP。实测数据显示,在30%丢包率的弱网环境下,闭合GOP的花屏持续时间比开放GOP缩短62%。这解释了为什么海康、大华的SDK默认开启open_gop=0——不是技术保守,而是工程妥协。
2.1 GOP参数实战陷阱:keyint、min-keyint、scenecut的协同逻辑
H.264编码器(如x264)提供三个核心GOP控制参数,它们不是独立调节的,而是一个动态博弈系统:
keyint=N:理论最大GOP长度,即强制插入I帧的间隔。但实际I帧出现时机受场景切换影响。min-keyint=M:最小GOP长度,防止I帧过于密集。若设为min-keyint=15,即使场景剧烈变化,也不会在15帧内插入第二个I帧。scenecut-threshold=T:场景切换检测阈值,范围0-100。值越小越敏感,T=40时,镜头快速推近可能触发I帧;T=5时,仅重大场景切换(如黑场转亮场)才触发。
三者关系可用一个公式表达:
实际I帧间隔 = max(min-keyint, min(keyint, 场景切换检测结果))
举个反例:某项目将keyint=30、min-keyint=1、scenecut-threshold=0,本意是“每30帧强制I帧,但允许随时插I帧”。结果编码器在运动剧烈的足球赛画面中,每2-3帧就插一个I帧,I帧占比达78%,码率暴增210%。根因是scenecut-threshold=0让场景检测失效,编码器退化为“每帧都当新场景处理”。修复方案是:scenecut-threshold=30(适配体育画面动态),min-keyint=15(防I帧泛滥),keyint=60(放宽上限)。实测I帧占比降至12%,码率下降37%,且关键帧分布更均匀。
注意:Qt5.15的QMediaRecorder在Linux下使用GStreamer后端时,
key-int-max参数对应x264的keyint,但scenecut-threshold需通过x264enc的tune=zerolatency隐式启用。直接调用setVideoSettings()无法设置该阈值,必须用QMediaRecorder::setOutputLocation()配合自定义pipeline字符串注入。
2.2 GOP与硬件解码器的隐性契约:为什么你的I帧总被丢弃?
在嵌入式开发中常遇到诡异现象:明明编码器设置了keyint=30,用ffprobe -show_frames确认I帧存在,但硬件解码器(如Rockchip RK3399的MPP)输出的帧序列里I帧却消失。这不是bug,而是H.264标准与硬件实现的“灰色地带”博弈。硬件解码器为节省片上内存,会对GOP结构做预判:若检测到连续多个I帧(如场景切换频繁),可能主动跳过部分I帧,只保留第一个作为同步点。验证方法很简单:用ffmpeg -i input.h264 -vf "select='eq(pict_type,I)'" -vsync 0 -f null -统计I帧数量,再对比解码器输出日志中的I frame count。若后者显著少于前者,说明硬件做了裁剪。
解决方案分三层:
- 编码端规避:启用x264的
--no-scenecut参数,强制关闭场景切换检测,用固定GOP长度保证I帧规律性; - 驱动层适配:修改RK3399的MPP驱动,在
mpp_dec_send_frame()中增加I帧计数校验,对异常密集I帧做缓冲合并; - 应用层兜底:在Qt的
QAbstractVideoSurface::present()回调中,用QVideoFrame::frameTime()检测时间戳跳跃,若跳跃超过1000ms/帧率,则主动触发QMediaRecorder::stop()再start()重建解码上下文。
这揭示了一个残酷事实:H.264的“标准”只是纸面协议,真正落地时,每个芯片厂商都在自己的解码器里写了私有优化逻辑。开发者必须同时懂标准、懂芯片手册、懂驱动源码,三者缺一不可。
3. 宏块:H.264的原子操作单元,一切压缩逻辑的物理载体
H.264把一帧图像划分为16×16像素的宏块(Macroblock, MB),这是所有压缩操作的最小单位。注意,这里说的“16×16”是亮度分量(Y)的尺寸,色度分量(U/V)因4:2:0采样,实际是8×8。宏块不是简单的像素块,而是承载着预测模式、残差数据、量化参数、运动矢量的复合体。你可以把它想象成一个微型工厂:输入是原始像素,输出是压缩后的比特流,中间所有工序(帧内预测、帧间运动补偿、DCT变换、量化、熵编码)都在这个16×16空间内完成。
宏块划分的灵活性是H.264超越MPEG-2的关键。MPEG-2强制所有宏块为16×16,而H.264支持子宏块划分(Sub-MB Partition):一个16×16宏块可进一步划分为16×8、8×16、8×8、8×4、4×8、4×4等7种形状。这种灵活性让运动补偿更精准——比如一个横向移动的汽车,用16×8划分能更好拟合其运动轨迹;而一个旋转的风扇叶片,则适合8×8划分。但代价是编码复杂度飙升:x264的--me umh(umh全搜索)模式下,一个宏块的运动估计耗时是--me dia(钻石搜索)的3.2倍。
我们用真实数据说话:在1080p@30fps视频中,若禁用子宏块划分(--subme 0),编码速度提升41%,但PSNR下降2.3dB,主观画质出现明显块效应;若启用--subme 7(最精细),PSNR提升1.8dB,但编码耗时增加2.7倍。工程实践中,安防监控场景常用--subme 5(兼顾速度与质量),而蓝光制作则用--subme 9(极致质量)。有趣的是,Qt5.15的QVideoEncoderSettings未暴露subme参数,必须通过QMediaRecorder::setOutputLocation("file:///dev/null?x264opts=subme=5")这种hack方式注入。
3.1 宏块类型解密:从mb_type字段读懂每一帧的“压缩意图”
H.264码流中,每个宏块头部都有一个mb_type字段,它像DNA一样编码了该宏块的全部压缩策略。以x264的mb_type为例,其二进制编码直接对应宏块行为:
mb_type=0:I_4×4(帧内4×4预测)mb_type=1:I_16×16(帧内16×16预测)mb_type=2:P_L0_16×16(P帧,单向16×16运动补偿)mb_type=3:P_8×8(P帧,8×8子宏块划分)mb_type=4:B_L0_L1_16×16(B帧,双向16×16预测)
关键洞察在于:mb_type不仅决定解码方式,更暴露编码器的决策逻辑。比如在低码率场景下,mb_type中I_4×4占比飙升,说明编码器放弃大块预测,转而用更细粒度的4×4块适应纹理细节;而在高运动区域,mb_type=3(P_8×8)频繁出现,表明编码器正用子宏块应对复杂运动。用ffprobe -show_frames -select_streams v解析码流,可统计各mb_type占比,这是调优的黄金指标。
实操技巧:当遇到“运动区域模糊”问题时,不要盲目调高码率,先检查mb_type分布。若P_8×8占比低于15%,说明子宏块划分不足,应调高--subme;若I_4×4占比超60%,则可能是--qcomp(量化曲线)过陡,需降低--qcomp 0.6让量化更平滑。
3.2 宏块边界:为什么你的视频总在16像素处出现“撕裂感”?
几乎所有H.264视频在放大观察时,都会在16×16像素边界处出现轻微的亮度/色度不连续,俗称“块效应”。这不是编码错误,而是量化误差在宏块边界累积的必然结果。DCT变换后,高频系数被粗量化,解码时逆变换产生的误差在宏块内部相对均匀,但在边界处因相邻宏块量化参数不同而突变。H.264标准为此设计了去块滤波器(Deblocking Filter),但它只在宏块边界执行,且强度受qp(量化参数)控制:qp越大(码率越低),滤波强度越强。
然而,硬件解码器常为省电关闭去块滤波。实测显示,Rockchip MPP在filter=0模式下,1080p视频的块效应PSNR比filter=1低4.7dB。解决方案分软硬两条路:
- 软件侧:用FFmpeg的
-vf deblock滤镜后处理,但增加CPU负载; - 硬件侧:在RK3399的MPP初始化时,调用
mpp_api->control(ctx, MPP_DEC_SET_DEBLOCKING, &deblock)启用滤波,并设置deblock=1(标准强度); - 编码侧:启用x264的
--deblock参数,让编码器在码流中嵌入滤波强度信息,硬件解码器可据此自适应。
记住:块效应不是缺陷,而是H.264在“压缩率—计算量—画质”三角中主动选择的折衷点。接受它,然后用去块滤波去管理它,这才是工程师思维。
4. 帧内压缩:用空间冗余换时间,I帧的“静态艺术”
帧内压缩(Intra Prediction)是H.264的基石能力,它不依赖其他帧,仅利用当前帧内邻近像素的空间相关性进行预测。I帧100%由帧内压缩构成,但P/B帧中的I_4×4宏块也用此技术。其核心思想是:人眼对绝对亮度不敏感,但对亮度变化(梯度)极其敏感。因此,与其编码原始像素值,不如编码“预测值与实际值的差值(残差)”,而预测值由邻近已解码像素生成。
H.264为4×4亮度块定义了9种预测模式(Mode 0-8),为16×16亮度块定义了4种模式(Mode 0-3),色度块另有4种模式。以最常用的4×4 Mode 0(Vertical)为例:用上方一行像素(A-P)预测当前块,第i行第j列的预测值 = 上方第j列像素值。这样,一个4×4块只需传输16个残差值,而非16个原始像素值。实测显示,在平坦区域,Mode 0的残差均值仅0.8,而Mode 3(DC模式)均值达3.2——预测越准,残差越小,后续DCT变换后零系数越多,熵编码更高效。
4.1 帧内预测模式选择:编码器如何“猜”出最优模式?
模式选择不是穷举,而是基于率失真优化(RDO)的智能决策。编码器对每个4×4块尝试所有9种模式,计算:
J = D + λ × R
其中D是预测残差的失真(SSD),R是编码该模式所需比特数,λ是拉格朗日乘子(与QP相关)。J值最小的模式胜出。
这里的关键是λ的计算:λ = 2^((QP-12)/3)。QP=12时λ=1,QP每+6,λ翻倍。这意味着高QP(低码率)时,编码器更看重R(比特数),倾向选择残差小但编码开销低的模式(如DC模式);低QP(高码率)时,λ减小,编码器更容忍R增大,追求D最小化,会选更复杂的预测模式(如Diagonal)。这解释了为什么同一场景下,QP=18的码流中I_4×4模式多为Mode 2(Horizontal),而QP=24时Mode 0(Vertical)占比飙升——编码器在低码率下放弃了复杂预测,转向更鲁棒的垂直预测。
4.2 帧内压缩的实战瓶颈:为什么I帧总是“又大又慢”?
I帧体积大是共识,但“慢”常被忽视。x264编码I帧耗时通常是P帧的3-5倍,根因在于帧内模式决策的计算爆炸。一个1080p帧含720×1280/256=3600个宏块,每个宏块含16个4×4块,每个4×4块需试9种模式,仅模式决策就需计算3600×16×9=518,400次SSD。更糟的是,4×4模式决策结果影响16×16模式选择(因16×16预测需4×4残差),形成级联依赖。
工程优化有三招:
- 提前终止:x264的
--ipratio 1.4参数限制I帧QP比P帧高1.4倍,避免I帧过度精细编码; - 模式剪枝:
--partitions i4,i8禁用8×8帧内预测,减少计算量; - 并行加速:启用
--threads auto,但注意Linux下线程数超过CPU核心数反而降速,实测--threads 4在4核ARM平台最佳。
在Qt5.15中,若用QMediaRecorder录制I帧密集的视频(如keyint=1),会发现QMediaRecorder::status()频繁返回QMediaRecorder::RecordingStatus,这是因为I帧编码阻塞了采集线程。解决方案是:在QMediaRecorder::setVideoSettings()中设置bitRate=5000000(5Mbps),并添加x264opts="keyint=30:intra-refresh=1"启用帧内刷新,让I帧能量分散到多个宏块,避免单帧编码峰值。
5. 帧间压缩:用时间冗余换空间,P/B帧的“动态智慧”
帧间压缩(Inter Prediction)是H.264的“灵魂”,它通过运动估计(Motion Estimation)和运动补偿(Motion Compensation)消除时间冗余。P帧用前向参考(只参考前面的I/P帧),B帧用双向参考(参考前后I/P帧)。其威力惊人:一个1080p P帧体积通常只有I帧的15%-25%,B帧更可低至8%-12%。
运动估计的核心是运动矢量(Motion Vector, MV),它表示当前宏块在参考帧中的位移。H.264支持1/4像素精度MV,通过参考帧像素的双线性插值得到亚像素位置。例如MV=(12.25, -5.75),意味着当前宏块在参考帧中位于(12, -5)像素位置,再插值计算0.25和0.75分量。这大幅提升预测精度,但也让运动估计成为编码器最耗时的环节——x264中--me umh模式下,运动估计占总编码时间68%。
5.1 运动搜索算法:从全搜索到UMH,精度与速度的永恒博弈
x264提供5种运动估计算法,选择取决于场景:
dia(钻石搜索):快速,适合实时编码,但易陷入局部最优;hex(六边形搜索):比dia精度高12%,耗时增25%;umh(非对称十字六边形):x264默认,精度最高,耗时基准;esa(全搜索):理论最优,但耗时是umh的8.3倍,仅用于离线制作;tesa(增强型全搜索):esa改进版,精度略升,耗时略降。
实战建议:安防监控用--me hex(平衡),直播用--me dia(低延迟),电影制作用--me umh(保质量)。有趣的是,Qt5.15的GStreamer后端默认用dia,若需更高精度,必须在pipeline中显式指定x264enc me=hex。
5.2 B帧的双向魔法:为何它既是“压缩利器”又是“延迟元凶”?
B帧的双向预测是H.264的王牌,但也是双刃剑。一个B帧需同时参考前一个I/P帧(L0)和后一个I/P帧(L1),解码顺序与显示顺序分离。例如显示序列为I B B P B B P,解码序列为I P B B P B B——B帧必须等后面的P帧解码完才能开始解码。这引入解码延迟:B帧数越多,延迟越大。bframes=3时,最小延迟为3帧(约100ms@30fps)。
然而,B帧极大提升压缩率。实测显示,在相同PSNR下,bframes=3比bframes=0码率低38%。工程取舍原则是:
- 低延迟场景(如视频会议):
bframes=0,用P帧+高码率保实时性; - 存储场景(如行车记录):
bframes=3,用B帧+CRF模式保画质; - 混合场景(如云游戏):
bframes=2,折中延迟与码率。
在Linux Qt5.15中,若用QMediaRecorder启用B帧,需确保后端GStreamer版本≥1.16,并在x264enc参数中加入bframes=2:b-adapt=2(自适应B帧决策)。否则可能出现gst_element_set_state failed错误——这是旧版GStreamer对B帧支持不完善导致的。
6. 从理论到实战:用FFmpeg和Qt5.15亲手构建H.264分析流水线
纸上得来终觉浅,下面用真实命令和代码,带你打通H.264开发的“任督二脉”。我们以一段1080p@30fps的测试视频为样本,构建完整的分析-编码-验证闭环。
6.1 第一步:深度解析码流结构,看清每一个NALU的“心跳”
用FFmpeg的-vbsf trace_headers过滤器,可逐字节解析H.264码流:
ffmpeg -i input.mp4 -c:v copy -vbsf trace_headers -f null -输出中关键字段解读:
nal_unit_type=5:IDR帧(关键I帧),sps_id=0, pps_id=0指明参数集索引;nal_unit_type=1:非IDR的I/P/B帧,first_mb_in_slice=0表示该slice从宏块0开始;slice_type=2:I slice,slice_type=7:P slice,slice_type=5:B slice;mb_type=3:P_8×8宏块,mb_type=0:I_4×4宏块。
更直观的方式是用h264bitstream工具(需源码编译)生成HTML报告:
h264bitstream -a -o report.html input.h264打开report.html,可交互查看每个NALU的语法元素、宏块类型热力图、运动矢量场——这是定位“花屏在哪个宏块发生”的终极武器。
6.2 第二步:用Qt5.15定制H.264编码参数,绕过API限制
Qt5.15的QMediaRecorder对H.264参数支持有限,必须用GStreamer pipeline注入:
// C++代码片段 QString pipeline = "appsrc name=source ! videoconvert ! " "x264enc speed-preset=ultrafast bitrate=2000 " "key-int-max=30 intra-refresh=true " "pass=quant bframes=2 b-adapt=2 " "! video/x-h264,profile=baseline " "! filesink location=output.h264"; QMediaRecorder *recorder = new QMediaRecorder; recorder->setOutputLocation(QUrl::fromLocalFile("file:///dev/null")); recorder->setVideoSettings(QVariantMap{ {"encodingSettings", QVariantMap{ {"pipeline", pipeline} }} });关键点:
speed-preset=ultrafast对应x264的--preset ultrafast,牺牲压缩率换速度;intra-refresh=true启用周期性帧内刷新,避免IDR帧冲击;profile=baseline确保兼容性,避免main或highprofile的硬件解码问题。
6.3 第三步:用FFmpeg验证编码效果,量化每一处优化
编码完成后,用以下命令做三重验证:
# 1. 统计关键帧分布 ffprobe -v quiet -show_entries frame=pict_type -of csv input.h264 | grep -c "I" # 2. 分析宏块类型占比 ffprobe -v quiet -show_entries frame=mb_type -of csv input.h264 | \ awk -F',' '{count[$2]++} END {for (i in count) print i, count[i]}' # 3. 测量实际码率与PSNR ffmpeg -i input.mp4 -i output.h264 -lavfi psnr="stats_file=psnr.log" -f null -psnr.log中avg_psnr值是黄金指标:avg_psnr > 38dB为优秀,35-38dB为合格,<35dB需调优。若PSNR达标但主观有块效应,检查mb_type中I_4×4占比是否超70%——这提示帧内预测过细,应调高--qcomp。
经验之谈:我在调试一款国产AI摄像头时,发现夜间红外模式下PSNR骤降5dB。用
ffprobe分析发现mb_type=0(I_4×4)占比达92%,根因是红外画面噪声大,编码器误判为高纹理需精细预测。解决方案是:在红外模式下启用--noise-reduction 1000,让编码器先滤噪再预测,PSNR回升至37.2dB,且I_4×4占比降至45%。这印证了一条铁律:H.264调优不是调参数,而是调编码器对场景的理解。
7. 那些年踩过的坑:H.264开发中最痛的5个“常识性错误”
最后分享5个血泪教训,它们不写在任何官方文档里,却让无数开发者深夜抓狂。
7.1 错误1:认为“H.264文件后缀是.h264就一定是裸流”
真相:.h264后缀只是约定俗成,文件可能是裸NALU流,也可能是MP4容器封装的H.264。用file input.h264命令看文件头:若显示data,大概率是裸流;若显示ISO Media, MP4 v2,则是MP4封装。裸流可直接喂给硬件解码器,MP4需先demux提取视频轨道。曾有项目因混淆二者,把MP4文件当裸流送入RK3399 MPP,导致MPP_ERR_VPU_CODEC_NOT_SUPPORT错误——解码器在找NALU起始码,却看到ftypmp42魔数。
7.2 错误2:用ffmpeg -i input.mp4 -c:v libx264 -preset fast output.h264生成的文件无法被硬件解码
根因:libx264默认输出highprofile,而多数嵌入式解码器只支持baseline。修复命令:
ffmpeg -i input.mp4 -c:v libx264 -profile:v baseline -level 3.1 output.h264-level 3.1确保符合1080p@30fps的约束(最大宏块数18150),这是硬件兼容的生死线。
7.3 错误3:Qt5.15中QVideoSink显示绿屏,日志显示Invalid YUV format
真相:H.264解码输出的YUV格式是yuv420p,但Qt的QVideoFrame要求QVideoFrame::Format_YUV420P。若用QVideoSink::present()直接传QVideoFrame,需确保frame.pixelFormat() == QVideoFrame::Format_YUV420P。常见错误是解码器输出nv12格式,需在pipeline中加videoconvert转换:
appsrc ! decodebin ! videoconvert ! videoscale ! capsfilter caps="video/x-raw,format=I420" ! appsink7.4 错误4:设置keyint=1后,视频体积暴增但seek依然卡顿
原因:keyint=1强制每帧I帧,但H.264的随机访问点(RAP)不仅是I帧,还需是IDR帧(清空参考队列)。普通I帧(non-IDR)仍依赖前帧,seek时需解码从上一个IDR开始的所有帧。正确做法:keyint=1必须配合--intra-refresh或--no-scenecut,确保每个I帧都是IDR。
7.5 错误5:在Linux下用v4l2-ctl配置H.264摄像头,pixelformat=H264成功但无数据输出
排查路径:
v4l2-ctl --all确认摄像头支持H.264输出;v4l2-ctl --get-input检查输入源是否激活;dmesg | grep -i "v4l2"看内核是否有VIDIOC_STREAMON: Invalid argument错误;- 最常见原因是:USB摄像头需先
v4l2-ctl --set-fmt-video=width=640,height=480,pixelformat=YUYV初始化,再切H.264。直接切H.264会失败——这是USB Video Class协议的握手要求。
这些坑,每一个都曾让我在凌晨三点对着示波器波形发呆。但正是它们,把H.264从纸面标准,锻造成手里的真家伙。