跨平台双向视频通话:五段链路与码流格式的统一

从零做一个全平台播放器·约 1560 字

两台设备互看,一台拍一台看,方向反过来也一样。这条链拆开是五段:拉流、解码、图像处理、编码、推流,每段都留了不止一种做法——拉流推流可以在两套实现里挑,解码编码可以在软硬之间挑。真正麻烦的地方不在段内,在段与段的接缝上:同是 H264/H265,网络来的和文件里装的格式不一样,各平台编码器吐出来的也不一样,接错了缝画面就花。这篇讲五段各自的可换点,以及码流格式怎么判、怎么转、配置帧按什么顺序给。

五段链路,每段都有第二种做法

拉流和推流:ffmpeg 一套,zlmediakit 一套,别的协议源也能扩进来。解码:软解硬解都行,ffmpeg 或者平台原生(MediaCodec、VideoToolbox)。编码同理。图像处理是一层独立挂着的:缩放、旋转、镜像、翻转、水印、字幕都能叠在这一段。数据源也是可扩的:截屏、摄像头、麦克风,各自有现成的设备源;各平台怎么采,前面讲采集的那几篇写过。

段与段怎么拼,按需求定。媒体源可以不解码,编码数据直接转发过去;也可以解了再编,中间做图像处理。低功耗平台上,解码到渲染、图像处理到硬编这两段可以整段留在 GPU 上走。限定用 AAC、H264、H265 这几个编码时,整条链可以完全不碰 ffmpeg。

五段链路,每段都有两种做法

同一段码流,两种装法

H264/H265 编出来的是一段段 NAL 单元,单元之间的分隔方式有两种,这是所有接缝麻烦的根。一种叫 annexb,单元前头放起始码(四字节 0001 或三字节 001);另一种叫 avcc/hvcc,单元前头放四字节的长度。装法按来源分:本地文件和 HLS 是长度头那种;网络流是起始码那种。编码器吐出来的也分家:Android 原生硬编出起始码那种,Apple 原生硬编出长度头那种。

用 ffmpeg 拉流,基本保留来源本来的样子,而且它的包里常把多个单元合在一起——参数集组加一个 I 帧挤在一个包里,或者一个很大的 I 帧被拆到几个包里。用 zlmediakit 拉流则统一成起始码那种;同一图像帧的多个单元,项目侧会再合并成一个大包才往下走,免得一个大 I 帧被拆散了不好管。

只顺着 ffmpeg 一条龙拉流、解码、编码、推流,这些可以全不管;哪一段换了实现,哪一段就得自己面对装法问题。

先判格式,再决定转不转

麻烦在于:四字节起始码和四字节长度头,肉眼分不出;三字节起始码开头的数据,又可能正好撞上长度头的低位。做法是先按长度头试一遍:取前四个字节当长度,顺着跳到下一个单元,看能不能自洽地走到整包末尾——对得上就是长度头那种,对不上就是起始码那种。判定每包做一次就够,没见过一段流里来回切换的。

方便起见,包入队时看到三字节起始码就补成四字节,后面要转长度头也顺。

这个判定函数还修过一个坑:包里混进垃圾数据时,按长度往前跳的游标会绕回去,循环走不出来,IO 线程就吊死在那里。后来给游标加了界,超界即判非、逐环前进,才把这个死循环堵上。

码流格式的判定,与按下游要求的转换

配置帧的拆与序

配置帧就是 VPS、SPS、PPS 这些不含画面、只描述画面怎么解的参数集,藏在 ffmpeg 交过来的 extradata 字段里。起始码那种,按起始码切开就是;长度头那种是另一套结构,得按结构逐字段解:跳过版本头、读长度字段宽、再逐个取参数集。给解码器的顺序有讲究:PPS 里的参数要读 SPS 的,SPS 又要读 VPS 的,所以 VPS 在前、SPS 次之、PPS 最后,乱了就初始化失败。

转换的原则按下游要什么来:Apple 平台解码要长度头那种,Android 解码要起始码那种,推流要起始码那种;转发到媒体源时,统一转成起始码那种最稳——实测里原始是长度头的流转到起始码源有时出问题,反方向则都能走。所以数据进源这一环只做拆参数集、合并被拆散的帧,原则上不动装法;到了上面列的那几种情况才动手转。

两台设备各走一遍

一台 iOS、一台 Android 对着通话。iOS 这侧:ffmpeg 拉流,从编码参数里取出参数集、判装法、单独存好;用参数集初始化原生解码器(这一步给的要去掉前头四字节);读包,起始码的转成长度头再送解码器,Apple 硬解还要把 SEI 这类非数据单元滤掉;解出的图像与相机画面一起过图像处理;按尺寸、帧率、码率起编码器,编出的参数集用对应接口单独取;推流对象初始化后,参数集补上起始码头送进去,编出的包转成起始码那种再入队。

Android 这侧:zlmediakit 拉流,参数集一般跟在第一个 I 帧前头,拉流对象存着,新参数集来了比一比变没变,决定要不要重置解码器;用带四字节起始码的参数集初始化原生解码器;读包直接判装法,不是起始码就转;解出的图像与 Camera2 采到的画面一起处理;MediaCodec 起编码器,它会在第一个 I 帧前自动发参数集;推流对象初始化时把参数集按顺序合成一个大包写进编码参数;编出的包本来就是起始码那种,直接入队。

一台 iOS、一台 Android 各自的组合

音频的配置帧:G711 没有,AAC 靠 adts 或 asc

音频没有这么折腾。G711 这类没有配置帧的概念;AAC 也只要知道采样率、声道、码率和档次就能手搓出配置帧,它的两种装法里,adts 每帧自带头、asc 给一次全局配置,现在 asc 用得更多——拿到采样率和档次这些基本信息就够初始化了。

到这儿,双向通话那条链就说完了。下一篇是反方向的最后一环:把这一章的采集、硬编正式接到 WebRTC 的轨道与编码器上,用它的网络推出去。

引擎 avox 在 GitHub 开源:github.com/xxxzhou/avox,判定函数、参数集拆解与两端组合都在里面。

音视频开发 实时通信 WebRTC H264 H265 annexb 跨平台