花屏问题
-
丢失参考帧导致的
一般 H.264 码流有 I、B、P 三种帧类型,I 帧是关键帧,B 帧是双向预测内插编码帧,P 帧是前向预测编码帧。
I 帧由于是帧内压缩,因此可以独立解码播放,而 B 帧,一旦丢失了 I 帧或者后面的 P 帧,则会解码失败,而 P 帧一旦丢失了前面的 I/B/P 帧,也会导致解码失败。
对于丢失了参考帧而导致的解码失败,一般就会出现花屏的现象,花屏的严重程度依赖于丢失的参考帧对即将解码的帧的重要程度。
那么,什么情况下会丢失参考帧呢 ?
首先,推流/播放的代码层面,需要注意,不要丢弃编码后、解码前的视频帧数据,不过实际场景中,遇到下面的情况,难免还是会产生丢帧:
网络不好,编码后的数据发不出去
系统低内存,队列里面无法承受更多的帧数据
因此,在这些极端的情况下,不得不丢帧的话,最合理的策略就应该是一次丢一整个 GOP,即:一旦开始丢了一个 I 帧,那么在遇到下一个 I 帧之前的所有视频帧,均丢弃掉,这样即可有效避免播放器端产生解码花屏。
-
播放器没有从关键帧开始解码
原理依然如上面所述,如果不从关键帧开始解码,则必然会由于丢失了参考信息而导致解码花屏。
因此,播放器,无论是首播,还是断网重连后,都应该判断第一帧视频是否是关键帧,如果不是,则应该等到第一个关键帧到达之后再送入解码器。
基于 ffmpeg 的播放器,如何判断关键帧,可以参考文章:《FFMPEG Tips (3) 如何读取每一帧的信息》
-
推流端图像尺寸和格式处理不当
图像的格式和尺寸,都是非常重要的参数,一定要严格配置正确。
比如:如果采集到的视频是 NV21 ,编码器只支持 I420,那么编码出来的图像自然会出现颜色问题。
比如:在一些场景切换的过程中,前后摄像头切换,视频的尺寸可能发生了变化,但是剪裁、处理、编码模块没有相应的修改尺寸,那么,也会出现各种视频错乱的现象。
-
如何判断是音频帧还是视频帧
int video_stream_idx = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
int audio_stream_idx = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
每一个 AVPacket 都有一个成员变量:stream_index,由该成员变量即可判断这个 Packet 到底是音频还是视频了:
if (avpkt.stream_index == video_stream_idx) {
LOGD("read a video frame");
} else if (avpkt.stream_index == audio_stream_idx) {
LOGD("read audio frame);
}
-
如何判断是否为关键帧
if (avpkt.flags & AV_PKT_FLAG_KEY) {
LOGD("read a key frame");
}
-
如何获取帧的数据和大小
帧的数据和大小直接定义在 AVPacket 结构体中,对应的成员变量如下:
// 压缩编码的数据,一帧音频/视频
uint8_t *data;
// 数据的大小
int size;
-
如何获取帧的时间戳信息
每一个帧都可能携带有 2 个时间戳信息,一个是解码时间戳 dts,一个是显示时间戳 pts,解码时间戳告诉我们什么时候需要解码,显示时间戳告诉我们什么时候需要显示,只有在码流中存在 B 帧的情况下,这两个时间戳才会不一致。
这些时间戳信息不一定存在于码流中(取决于生产端),如果不存在,则其值为:AV_NOPTS_VALUE
一定要选择正确地方式打印时间戳,时间戳是使用 long long 来表示的,即 int64_t,因此打印的时候,需要使用 “%lld” 来打印,例如:
while (!interrupt) {
int ret = av_read_frame(player->ic, &avpkt);
if (ret < 0) {
break;
}
if (avpkt.stream_index == video_stream_idx) {
LOGD("read video frame, timestamp = %lld \n”, avpkt.pts);
} else if (avpkt.stream_index == audio_stream_idx) {
LOGD("read audio frame, timestamp = %lld \n”, avpkt.pts);
}
}
由此,我们就可以通过这些 log 信息调试一下某一段音视频流的时间戳是否正确,比如是否出现了时间戳的回滚和错乱,则必然会导致播放端出现音视频不同步或者显示异常等情况。