硬解码出现画面回退分析
项目场景:
在开发实时监控视频预览库时,我们采用 FFmpeg + DXVA2 硬解码,并设计了一个渲染队列,存放解码出来待渲染的数据(队列大小可配,默认 6),以下是该队列的入队、出队接口:
// 入队接口
int InputData(AVFrame *data) {m_mutex.lock();if (m_dxvaDrawDataQue.size() >= m_nMaxSize) { // 队列已满if (m_bThrowPacket) { // 是否丢包av_frame_free(&data);} else {m_mutex.unlock();while (!m_bExit) { // 不丢包则循环等待队列有空位,每次休眠10ms后重试入队if (m_dxvaDrawDataQue.size() < m_nMaxSize) {m_mutex.lock();m_dxvaDrawDataQue.push_back(data);break;}Sleep(10);}}} else { // 队列未满则直接入队m_dxvaDrawDataQue.push_back(data);}m_mutex.unlock();return 0;
}
// 取数据接口
AVFrame* CDxvaDrawDataQueue::OutputData() {AVFrame *data = nullptr;if (!m_dxvaDrawDataQue.empty()) {m_mutex.lock();data = m_dxvaDrawDataQue.front();m_dxvaDrawDataQue.pop_front();m_mutex.unlock();}return data;
}
解码线程不断解码并将解码出来的数据入队,渲染线程不断从队列中取数据进行播放:
// 解码线程
int CDecoder::DecodePacket(AVPacket* inPacket, AVFrame* outFrame, int& gotFrame) {// ......while (true) {len = avcodec_decode_video2(m_video_codec_ctx, outFrame, &gotFrame, inPacket);if (len < 0) {// HW_WRITE_LOG(log_error, "avcodec_decode_video2 failed %d %s",len,buff);return -1;}if (gotFrame) {// 将解码出来的数据放到待渲染队列AVFrame *frame = av_frame_alloc();av_frame_move_ref(frame, outFrame);m_dxvaDrawQueue->InputData(frame);}}// ......
}
// 渲染线程
unsigned int CDecoder::DxvaDraw() {// ......while (!m_bExitDxvaDraw) {nowTime = timeGetTime();if (nowTime - lastDisplayTime >= nextDisplayTime) {delayTime = 50; // 假设帧率是20,则一帧需要显示的时间是50msAVFrame* data = m_dxvaDrawQueue->OutputData();if (data != nullptr) {m_ffdxva2->dxva2_retrieve_data_call(m_video_codec_ctx, data);av_frame_free(&data);ret = 0;} else {ret = -1;}drawEndTime = timeGetTime();drawTime = drawEndTime - nowTime;if (drawTime >= delayTime) { // 绘画的时间大于帧率的间隔时间,直接显示下一帧nextDisplayTime = 0;continue;} else {if (ret == 0) {nextDisplayTime = delayTime - drawTime - 1;} else { // 缓冲区空时nextDisplayTime = 0;}}lastDisplayTime = drawEndTime;}Sleep(2);}return 0;
可以看到,渲染线程会根据视频的帧率来控制渲染的速度,不会过快或过慢。
问题描述
渲染队列可以设置为两种模式:
- 当渲染队列设置为丢帧模式(m_bThrowPacket设置为true)时,队列满时,新解码帧直接丢弃。
- 当渲染队列设置为阻塞模式(m_bThrowPacket设置为false)时,队列满时,解码线程等待(10ms 重试),直到队列有空位。
测试过程中发现:
- 丢帧模式或者队列设置较大时(比如设置为6),解码线程速度非常快,但播放时会出现画面回退,即已经播放到某个时间点,突然又显示了更早的帧。
- 阻塞模式且队列设置较小时(比如设置为3),解码线程因等待被迫减速,整体速率接近播放速率。播放流畅,没有回退。
原因分析:
1、硬解码(DXVA2)的帧顺序问题
硬件解码器内部有 DPB(Decoded Picture Buffer,解码参考帧缓存),在解码 P 帧时会保留并输出参考帧。调用 avcodec_receive_frame() 得到的帧,往往是解码顺序(decode order),而不是显示顺序(presentation order)。如果应用层直接按解码顺序入队,就可能出现播放线程取到的帧时间戳(PTS)比上一帧更早,即画面回退。
2、队列过大或丢帧模式下
解码线程疯狂产出帧(快于播放线程)。播放线程在消费时,遇到硬件解码器「迟到」吐出的早期帧,出现时间戳倒退。缺乏节奏约束,乱序现象被放大。
3、队列较小 + 阻塞模式下
解码线程被迫等待,解码速率约等于播放速率。硬解码器不会积累太多乱序帧,播放线程几乎是「解完就消费」。解码与播放自然保持同步,没有回退。
4、为什么软解码没有问题?
FFmpeg 的软解码器在输出时,已经完成了 PTS 重排序:avcodec_receive_frame() 保证输出的是显示顺序帧。因此,即使解码很快,队列中始终是按时间顺序排列的帧,不会出现回退。