共计 5946 个字符,预计需要花费 15 分钟才能阅读完成。
介绍移动端 Android/iOS 硬解用法的文章有很多,本文将以笔者在实际开发工作中的经验为基础,抽出几个比较关键的部分来跟大家分享,旨在解决实际工作中可能遇到的花屏、(半边)绿屏、播放不完整等问题。
本文将以目前广泛应用的 H.264 编码的视频为例来说明,主要包含:H.264 码流数据结构说明、解码器的初始化、seek、前后台切换、无缝分辨率切换、播放结束时的处理以及 iOS 如何避免下半部分绿屏的问题。
一、H.264 码流数据结构说明
1. 理解码流数据结构的重要性
我们讲支持硬解,提高硬解兼容性,实际上就是对码流数据的结构进行处理以符合平台硬解要求,因此对码流数据结构的理解是必不可少的。
2. SPS/PPS 与 IDR 帧
SPS(Sequence Parameter Set) 序列参数集、PPS(Picture Parameter Set)图像参数集,包含了图像编码的各种参数信息,是作为解码器初始化所必须的参数信息。
IDR(Instantaneous Decoding Refresh)帧,也就是即时解码刷新帧,直观意思就是解码器在接收到 IDR 帧后会刷新参考帧缓存。IDR 帧前后的视频帧不会有任何参考关系,解码器可以从任何一个 IDR 帧开始解码。
3. H.264 的 NAL 单元
NALU 结构图示:
H.264 标准中,视频流是由 NAL(Network Abstraction Layer)单元组成的(简称 NALU),每个 NALU 中可能是 IDR 图像、SPS、PPS、non-IDR 图像等。
上图中示意的 NALU 单元是以 startcode 方式分割的,关于 NALU 的分割方式将在后面说明。另外,NALU 内容中添加了防竞争字节,也就是说在一个 NALU 中,我们不可能再找到匹配的 startcode.
H.264 流的 NALU 组成图示
从上图可以看到,一个视频帧中可能可能包含多个 NALU, 此时可以称该视频帧为多 slice 视频帧(一个 NALU 中包含该视频帧的一个 slice)。
NAL Header 的结构说明
其中 nal_unit_type 是我们关心的字段,该字段标识了当前 NALU 的类型,我们可以通过将 NALU 中第一个字节 &0x1F 的方式来得到 NALU 类型。NALU 类型的具体定义如下图所示:
NALU 类型定义
其中 5 代表上面提到的 IDR 帧数据,7、8 分别代表 SPS/PPS 数据。
- AVCC 与 Annex-B
H.264 码流分为 AVCC 与 Annex- B 两种组织格式。
• AVCC 格式 也叫 AVC1 格式,MPEG- 4 格式,字节对齐,因此也叫 Byte-Stream Format。用于 mp4/flv/mkv 等封装中。
• Annex- B 格式 也叫 MPEG-2 transport stream format 格式(ts 格式), ElementaryStream 格式。用于 TS 流中(以及使用 TS 作为切片的 hls 格式中)。
这两种格式的区别有两点:
(1)NALU 的分割方式不同;
(2)SPS/PPS 的数据结构不同。
• AVCC 格式使用 NALU 长度(固定字节,字节数由 extradata 中的信息给定)进行分割,在封装文件或者直播流的头部包含 extradata 信息(非 NALU),extradata 中包含 NALU 长度的字节数以及 SPS/PPS 信息。
• Annex- B 格式使用 start code 进行分割,start code 为 0x000001 或 0x00000001,SPS/PPS 作为一般 NALU 单元以 start code 作为分隔符的方式放在文件或者直播流的头部。
AVCC 格式的 extradata 格式定义在“ISO_IEC_14496-15″ 文档中,Annex- B 格式的 SPS/PPS 定义可以在 ”ISO_IEC_14496-10″ 文档中找到。
MediaCodec 与 VideoToolBox 使用的数据格式
Android 的硬解码接口 MediaCodec 只能接收 Annex- B 格式的 H.264 数据,而 iOS 平台的 VideoToolBox 则相反,只支持 AVCC 格式。
这就导致:
• 在 Android 平台硬解播放 flv/mp4/mkv 等封装的视频时,需要将 AVCC 格式的 extradata 以及 NALU 数据转为 Annex- B 格式;
• 在 iOS 平台播放 ts 或 ts 切片的 hls 视频时,需要将 Annex- B 格式的 SPS/PPS NALU 转为 AVCC 格式的 extradata,以及将其他以 size 方式分割的 NALU 转为 start code 方式。
二、解码器的初始化及数据输入
初始化解码器,除了配置输入视频流的的编码格式、宽高以及输出格式之外,还需要配置一些额外的信息。对于 H.264 视频,需要填充的就是我们前面提到的 SPS/PPS 信息。
1. Android 平台 MediaCodec 的初始化
我们需要将 Annex- B 格式的两个 SPS/PPS NALU 单元通过 setByteBuffer 方法,以 ”csd-0″ 为名称(或 SPS 设为 ”csd-0″, PPS 设为 ”csd-1″)设置到 MediaFormat 对象中,并调用 configure 接口配置到 MediaCodec 中去。
MediaCodec 设置 SPS/PPS 信息的示例代码
MediaCodec mediaCodec = MediaCodec.createDecoderByType(“video/avc”);
MediaFormat mediaFormat = MediaFormat.createVideoFormat(“video/avc”, width, height);
// extradata 中是 Annex- B 格式的 SPS、PPS NALU 数据
mediaFormat.setByteBuffer(“csd-0”, extradata);
// …
mediaCodec.configure(mediaFormat, surface, 0, 0);
// …
如上节所述,对于 mp4/flv/mkv 等封装,我们得到的是 AVCC 格式的 extradata,需要先将该 extradata 转换为 Annex- B 格式的两个 NALU, 然后用 startcode 进行分割。
Android 平台在配置解码方式时,最好使用 MediaCodec 直接渲染到 Surface 的方式,一是可以避免不同硬件平台繁杂的 YUV 格式兼容,二是在解码渲染高分辨率的视频时可以有非常明显的效率提升。
2. iOS 平台 VideoToolBox 接口的初始化
VideoToolBox 针对 AVCC 格式和 Annex- B 格式的 SPS/PPS 信息设置,分别提供了两个方法:
• CMVideoFormatDescriptionCreate: 可以设置 AVCC 格式的 extradata 信
• CMVideoFormatDescriptionCreateFromH264ParameterSets: 用来设置 Annex- B 格式的 SPS/PPS NALU 信息(需要去掉 startcode)
需要注意,iOS 平台不支持隔行 H.264 视频的解码,需要在创建 videoToolBox 前从 SPS 中判断当前视频是否隔行编码。
3. 数据格式的转换
如前所述,Android 平台只接受 Annex- B 格式以 startcode 分割的 H.264 NALU;iOS 平台则相反,只接受 AVCC 格式以 size 分割的 NALU. 在原视频流格式不匹配时需要进行相应的转换。
iOS 还有以下的一些限制需要留意:
(1)如果源视频流本身已经是 AVCC 格式,但 NALU size 的大小是 3 个字节,而非 4 字节时,需要转为 4 字节格式。具体的话,需要先更改 extradata 中标识 NALU size 的字段,然后每个视频帧中的 NALU size 都要改成 4 个字节。
(2)如果一个视频帧内有多个 NALU(多 slice),那必须将这些 NALU 打包到一个 CMSampleBuffer 中,一次性送给解码器。
三、seek 时的处理
编码后的视频帧之间存在着参考关系,我们无法直接从任意一帧开始解码,只能从可随机访问帧开始,在 H.264 中就是 IDR 帧。
1. 从 IDR 帧开始解码
对于点播视频,mp4/flv/mkv 的头信息中都会保存整个视频的 IDR 帧索引,seek 时需要定位到原 seek 位置附近的 IDR 帧再送数据给解码器。如果要实现短视频中的精确 seek 逻辑,可以先 seek 到离目标位置最近的上一个 IDR 帧开始解码,但不输出图像,直到目标位置的视频被解码出来。
2. 刷新解码器
进行 seek 操作时,除了要保证从 IDR 帧开始之外,还需要在送新的 IDR 帧数据前对解码器进行刷新操作。
• Android 平台可以通过调用 MediaCodec 的 flush()接口来实现。
• iOS 平台则需要重新创建 videoToolBox.
四、前后台切换
对 Android、iOS 平台,都存在 App 切后台,播放器渲染 View 被销毁而导致解码出错的情况。
1. 切回前台的处理
App 切到后台时,iOS 的 videoToolBox session 会失效,切回前台后原 session 也不能继续使用,需重新创建 videoToolBox 实例;Android 平台在配置了 Surface 的情况下,如果 Surface 被销毁,则在切回前台时也需要配置新的 Surface 来重新创建并初始化 MediaCodec.
如果我们要提高用户体验,实现前后台切换时的无缝播放,而不是重新拉流,那么可以在用户切后台的时候暂停播放,切回前台时重新创建解码器,继续从原位置开始播放。
不过参考前面 seek 章节的说明,我们恢复播放的位置很可能不是 IDR 帧,这种情况下就会出现切回前台后画面会先黑一段时间,直到下一个 IDR 帧被解码。黑屏的时间会跟视频流的 IDR 帧间隔有关,最差情况下黑屏时间接近 IDR 帧间隔。为了尽量避免黑屏现象的出现,我们可以参考前面精确 seek 的处理,在解码过程中一直缓存当前 GOP(Group Of Picture)的视频帧数据,在恢复时从当前 GOP 的 IDR 帧开始解码但不输出图像,直到恢复点。
不过上述方案也无法 100% 解决黑屏问题,解码恢复点前的视频数据本身会有时间消耗,GOP 越大,解码恢复可能需要的时间也就越长,黑屏时间也就会越长。
2. Android 平台使用 TextureView 避免 Surface 被销毁
对 Android 平台,我们也可以通过使用 TextureView 渲染来尽量避免 Surface 被销毁。
具体实现上,可以:
(1)在 TextureView 的 onSurfaceTextureAvailable 回调中保存当前创建的 SurfaceTexture;
(2)App 切后台时,TextureView 的 onSurfaceTextureDestroyed 回调中返回 false,不让系统销毁当前的 SurfaceTexture;
(3)在下一次 App 切回前台,onSurfaceTextureAvailable 回调中,将前面保存的 SurfaceTexture 通过 setSurfaceTexture 接口设置给 TextureView,并销毁回调参数中传回的 surfaceTexture;
(4)播放器销毁时,需要销毁保存的 surfaceTexture.
五、无缝分辨率切换的处理
考虑到用户网络的差异性,以及不同时间段的拥堵状况不同,为了兼顾拉流清晰度与流畅度,我们可以通过实时检测用户的网络情况,并动态切换视频的分辨率、码率来提高播放体验。
rtmp 直播,http/flv 直播,hls 直播以及 hls 点播可以支持动态分辨率切换。
分辨率切换时需要拿到新的 SPS/PPS 并重启解码器
• 对于 rtmp, http/flv 直播,以及 mp4 分片的 hls 视频,分辨率切换时我们能够拿到新的 AVCC 格式的 extradata(使用 ffmpeg 解封装时这个信息是在 AVPacket 的 sidedata 中), 此时需要用新的 extradata 数据重新创建解码器,所需的分辨率信息可以从 extradata 中解析出来。
• 而对于 ts 切片的 hls 直播点播视频,SPS/PPS 信息是以 Annex- B 格式保存在正常的 NALU 中,而且每个 IDR 帧前都会有 SPS/PPS 的 NALU。对此,我们需要监控每个收到的视频包,获取其 NALU 类型,如果是 SPS/PPS, 则从中解析出分辨率等信息,如果有变化,则用新的 SPS/PPS 重新创建解码器。
六、播放完成时避免遗漏最后几帧
前面我们提到过,编码后的视频帧之间存在着参考关系,而且存在双向参考帧(B 帧)的视频流其解码输出顺序和输入的顺序是不同的,同时解码器在异步模式下也不会立即返回解码后的视频帧,这就导致我们在输入最后一帧数据给解码器后,可能还会有一些视频帧没有输出。
为了避免遗漏最后几帧的情况,我们需要做一些处理:
• Android 平台需要给 MediaCodec 送入一个带有 BUFFER_FLAG_END_OF_STREAM 标记的 buffer 数据(可以是空 buffer),然后等待 MediaCodec 输出带有该标记的内容,再销毁解码器,结束播放。
• iOS 平台需要在送完最后一帧数据后,调用 VTDecompressionSessionWaitForAsynchronousFrames 接口,该接口会等待所有未输出的视频帧输出结束后再返回。
七、VideoToolBox 兼容不标准的多 slice 视频
在 iOS 平台的硬解的实践中,我们可能会遇到如下图的这种情况(上面一部分有画面,下面部分是绿屏):
这种现象实际上就是多 slice 视频的组织格式不符合 VideoToolBox 的要求引起的。
以上图的视频为例,该视频流的每一帧是由 3 个 slice 构成的,对于 VideoToolBox 可以正常解码的组织格式应该如下图所示:
而该视频的帧组织方式则如下图所示:
可以看出,该视频混用了 AVCC 与 Annex- B 格式的分隔符,导致 iOS VideoToolBox 只能解码第一个 slice 单元,从而出现下半部分绿屏的情况。
• 对于这类问题视频的处理:如果是源视频流可控,可以调整源视频流的打包方式,按第一种图示的方式打包。
• 对于不可控的场景,播放器也可以做下兼容:因为一个 NALU 中的内容一定是不包含 startcode 的,所以如果在一个 NALU 中找到了 startcode,就可以将其处理成第一种图示中的格式。
想要阅读更多技术干货、行业洞察,欢迎关注网易云信博客。
了解网易云信,来自网易核心架构的通信与视频云服务。
__
网易云信(NeteaseYunXin)是集网易 18 年 IM 以及音视频技术打造的 PaaS 服务产品,来自网易核心技术架构的通信与视频云服务,稳定易用且功能全面,致力于提供全球领先的技术能力和场景化解决方案。开发者通过集成客户端 SDK 和云端 OPEN API,即可快速实现包含 IM、音视频通话、直播、点播、互动白板、短信等功能。