音频那一节介绍了 FLV 中 AAC 的封装。下面看一下对视频的封装。看本节内容之前最好过一下基础部分的三篇文章。
iOS 音频 - 音频基本知识介绍
视频的采集-颜色模型和采样
视频的画面类型
H264 编码分析

FLV 视频封装

FLV 格式

image.png

Video Tag Data

回忆一下当 TagType = 0x9 的时候,表 tagData 为视频数据。参考文章中对 TagData 字节序的说明
image.png
当 CodeID = 7 时代表Data 内部为 AVCVIDEOPACKET。这个图相对比较旧了,记得之前 VP8和H264 之争吗。H264 编码分析中提到的 H264的专利问题。

AVCVIDEOPACKET

现在开始关注一下 AVCVIDEOPACKET 内部是如何分布的。可以看到当第一个字节为0 时代表序列头(对应 h264 sps和 pps中的大部分值),1 代表 NALU,这个已经很熟悉了。
image.png
当 AVPacketType = 0, 发送视频的参数信息
image.png
上面的参数很多可以在 sps 或 pps 的参数中见到。比如 AVCLevelIndication 对应的是 level_idc。如果是 NALU 单元,则如下。可以看到不是我们熟悉的 0x000001 分割,而是使用了一个 Nalu Len 的长度进行的数据分割,这个是 RTP 流的格式。在H264 编码分析中我们提到过两种H264的流格式。
image.png

到这里我们基本就完成了 FLV 的 VideoData 是如何封装 H264数据。

小结

画一个鸟瞰图,下面是一个 VideoTagData 的层级结构。h264InFlv.drawio
h264InFlv.svg

实战分析

分析 RTMP 中的 h264 单元

我们打开一段Wireshark 抓包的 RTMP协议的包的流看一下
Screen Shot 2021-02-08 at 5.42.15 PM.png
这个是RTMP 的第一个VideoData,可以根据 Body 中 CodeId = 7 确定这是一个 AVCVIDEOPACKET 的包。通过查看这个包的第一个字节 AVPacketType = 0得出这是一个参数记录包。虽然这里显示的 keyframe 这并不是关键帧,而是视频参数 sps 和 pps 的一些值。

再看紧着的下一个包
Screen Shot 2021-02-08 at 5.53.57 PM.png
同样是从0x17开始,第一个0x01 说明 AVPacketType = 01,则他的data 部分应该是 Nalu 数据。蓝色线部分是3个字节的 compositiontime。从绿色开始就是 Data 的部分了。根据定义这部分数据是 naluLength + nalu。长度在这里可以看到是用4个字节代表的(绿色部分),第一个0x28 使用计算器看一下可以知道 Nalu的 type = 8,这个 nalu 是一个 pps nalu。从0x28开始数4个字节,下一个绿色部分代表是紧着的 nalu 的长度,第一个值是0x25 计算得到 Type 是5,这是一个 IDR nalu 单元。下面的可以依次类推。

修复 MOV 文件

这个例子来自我们开发过程的一个 bug。摄像头发出的流在录制后, iOS 无法播放,一直是绿屏,但是可以通过 VLC 播放,也可以通过 Android 播放。

解题思路

为了引出这个话题前,先说明一下问题的解决思路(如果不喜,可以跳过进入下一节如何 debug。)。为什么讲这个呢,因为知识是有限的,但是作为工程师会经常解决未知的问题。下面我先说明一下这个问题的解题思路

  • 先找到一些可以帮助你分析的工具,比如 ffmpeg,ffprobe,mediaInfo
  • 找一个正常的尺子,这里是指找一个正常的视频,最好之间相差的越少越好,便于单一变量突出问题。
  • 然后做一些对比实验,重复几次,找出异常点
  1. iOS 无法播放,Mac 也无法播放,VLC 可以播放,VLC 硬解无法播放
  2. ffmpeg copy抽出的 h264 的流重新封装后 iOS/Mac 可以播放
    1. 抽出命令 ffmpeg -i test.mov -c copy test.h264
    2. 封装命令 ffmpeg -i test.h264 -c copy new.mov
  3. ffmpeg 直接copy 转码一个新的 MOV或mp4无法播放
    1. ffmpeg -i test.mov -c copy test.mp4
  4. 使用 ffmpeg 推正常视频流,iOS,Mac 可以播放
    1. ffmpeg -re -stream_loop -1 -i new.mov -vcodec copy -f flv "rtmp://xxx"
  5. 通过 diff 工具查看这两个视频的mediainfo 的工具

这些比对我们基本得出的结论是

  • ffmpeg 本身封装 mov 的能力不会有问题
  • 自己撰写的封装代码多次实验后没有问题
  • 视频编码格式可能存在一些问题,通过 meidaInfo 的多次对比发现异常点

有了这些后我没有再去过多分析 ffmpeg 代码,着手研究 h264 的编码格式。问题解决后,最好是再深挖一下这个领域到自己满意。

Debug 视频

下面是我截取了3帧的视频。
test-green-bad.mov.zip
本节想告诉如何 Debug 一些视频,查看它的编码是否出错。并通过 Mediainfo 和 sublimeText或 hexdump 命令修复它。
使用 MediaInfo 打开这个视频,跳过视频的头部,进入h264 流的地方。 可以看到这个nalu header 第一部分是 size,并不是 0x000001,所以这是一个 rtp 包的h264编码方式。
Screen Shot 2021-02-09 at 9.10.26 AM.png
第一个问题我们如何找到字段对应的二进制呢?
观察 mediaInfo 最左边给出了字节的位置 0x0024(16进制)转为10进制是36。使用 Sublime 打开这个文件,可以看到如下的编码,每一行是16个字节(0x0010),所以可以根据 mediainfo 给出的信息找到第一个 nalu 单元的头。

  1. 0000 0014 6674 7970 7174 2020 0000 0200
  2. 7174 2020 0000 0008 7769 6465 0001 53b3
  3. 6d64 6174 0001 4e6e 28ee 3cb0 0000 0001
  4. 25b8 4001 db82 9835 d469 eabd c185 5f17
  5. 7207 89c1 9f1a 000a 5d05 f2ac ea93 3b38
  6. 87cb 11b3 1196 4b43 7ce0 ab05 097b 2c98
  7. 5087 f6fb 6781 59d0 4dcd 5e4c d506 cf12

另一个简单的办法是看 MediaInfo 给出的 pps 第一个 size 的值是 0x0001 4e6e ,这个是原始流中的二进制,通过搜索 0x0001 4e6e 很快也可以找到这个值。Screen Shot 2021-02-09 at 10.17.06 AM.png
在第三行第3列。通过之前的环节,我们可以看到熟悉的0x28 & 0x1f = 0x8 这个是 pps 的 type。前面的4个字节就应该是它的长度,应该是 0x0000 0004,但是这里给了一个很大的数,这明显是不对的。

image.png
image.png
再看MediaInfo 的第一个 IDR 帧是没有分析出来的,第四行的第一个 0x25 & 0x1f = 0x5 这个是 IDR 的帧,但是前面的值是0x0000 0001,难道采用了 Annex-B 模式的流格式,查看后面 slice 采用的也是 RTP 包的封装格式。所以 IDR 帧的 Nalu length 也是不对的。那这个长度该怎么计算呢,根据 mediaInfo 左侧提供的地址,我们使用 0x14e96 - 0x0002c - 0x04 = 0x14e66。为什么要减去个4呢? 参考 RTP 包的定义,4个字节长度+n 个字节的 nalu 组成。 将0x0000 0001 改成 0x0001 4e66 即可。
修复后的视频在这里,可以在 iOS 和 Mac 上播放了。
test-green.mov.zip

代码定位

通过 Xcode 自带的Debug 功能我们来看一下 h264 的一帧。
本节通过在 ijkplayer 中增加了一部分分流录制代码,进行演示。使用了 ffmpeg 的 AVPacket 结构体。关于 ffmpeg 的结构体说明参看这里
Screen Shot 2021-02-09 at 10.42.11 AM.png
可以看到清晰的两个nalu 单元,第一个是 pps,第二个是 IDR。通过调试的方式看到流的原始编码方式可以更快的帮助我们定位问题。
Screen Shot 2021-02-09 at 10.48.55 AM.png