ffmpeg5.0版本发布了,使用最新版本的ffplay工具去播放一个speex编码的音频flv文件,发现音频不正常, 视频呈现快进现象.

问题分析

ffplay播放没有任何报错,debug日志也没有什么信息.

使用ffmpeg将音频转码成aac, 发现也是一样的问题,转码后的媒体文件同样不正常.

这个flv文件应该是没有问题的,因为我在另一个环境下播放过,是完全正常的, 另一个环境ffmpeg版本是4.2.

所以这应该是从4.2到5.0版本引入的问题.

ffprobe分析一下这个文件

1
2
3
ffprobe -i xx.flv -show_packets > packet.txt

ffprobe -i xx.flv -show_frames > frame.txt

分析后发现了大概问题所在了,我这个flv文件的speex编码是20ms的frame_size, 封装时是6个编码包 聚合为一个音频包,对应libspeexenc中的frames_per_packet编码参数.最新版的ffmpeg解码speex音频 时只解出了1/6的音频samples,看起来像是每个音频包只解码出了第一个音频frame,120ms的音频包解码后 只剩下了20ms.

git log -p -- libavcodec/libspeexdec.c查看这个解码器提交历史, 2022年有几个小改动,看起来都没有动过逻辑,再早 的改动是2019年的了,那应该是较老的版本了.

再次确认了一下ffmpeg 4.2版本,解码这个flv文件确实是完全正常的.

排查到这里,这个问题就显得很奇怪了,难道不是解码器的问题吗?

git bisect找问题

ffmpeg提交众多,现在确认老版本正常,新版本有问题,但是又无法很快的精准找到问题原因,于是我想到了 git bisect, 这个命令能二分查找哪个版本引入的问题,git bisect的要求是需要二分查找的中间版本能判定是否问题版本.

好了,这个问题天然适合使用git bisect来快速查找,首先需要准备一个判定脚本,用来判定当前版本是否问题版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
cat test.sh

#!/usr/bin/env bash

cd ~/source/FFmpeg
make clean && rm xx.flv xx
./configure --prefix=/usr --enable-debug --disable-stripping --enable-libx264 --enable-libspeex --enable-gpl 

make -j16

./ffmpeg -i ~/Videos/44.flv -c:a aac xx.flv -y
./ffprobe -show_packets xx.flv  >xx
num=$(grep "audio" xx | wc -l)
echo "audio num: "$num
if [ $num -lt "330" ];then
    exit 1
fi

echo "ok, good test"

脚本先编译当前版本的ffmpeg,然后命令行转码后分析音频数量,这个文件使用正常版本分析出的数量大于330, 异常版本小于这个数字,则返回非0码退出.

启动git bisect

1
2
3
git bisect start [commit-bad] [commit-good]

git bisect run ./test.sh

接下来等它自动二分查找就行了,这个过程时间比较长,因为ffmpeg编译比较耗时,在1000多个提交范围里,大约10次查找,结果就出来了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
148ada5577262c6c18ae97604df8fe1c18b096e2 is the first bad commit
commit 148ada5577262c6c18ae97604df8fe1c18b096e2
Author: Paul B Mahol <onemda@gmail.com>
Date:   Mon Sep 13 17:00:38 2021 +0200

    avcodec: add native Speex decoder

 Changelog                 |    1 +
 doc/general_contents.texi |    4 +-
 libavcodec/Makefile       |    1 +
 libavcodec/allcodecs.c    |    1 +
 libavcodec/speexdata.h    |  780 ++++++++++++++++++++++
 libavcodec/speexdec.c     | 1590 +++++++++++++++++++++++++++++++++++++++++++++
 libavcodec/version.h      |    2 +-
 7 files changed, 2376 insertions(+), 3 deletions(-)
 create mode 100644 libavcodec/speexdata.h
 create mode 100644 libavcodec/speexdec.c
bisect found first bad commit%

见证真相的时候到了,git bisect 明确指出了哪个提交是第一个坏的提交,看看提交信息,add native Speex decoder, 恍然大悟,原来有问题的是native speex decoder, 不是基于libspeex的那个decoder.难怪之前查找libspeexdec没有什么信息.

ffmpeg从这个提交把speex解码器默认切换成了他自己实现的解码器,而不是第三方的libspeexdec,而这个解码器在处理多frames聚合一包音频的flv文件时是有问题的.

仍然使用libspeexdec试试播放这个flv文件,一切正常

1
ffplay -acodec libspeex xx.flv

原因分析

知道了这个问题是ffmpeg的native speex decoder的问题,来看代码,这个decoder初始化时会去根据extra_data初始化一些解码参数,但是speex封装到flv容器中是没有类似aac,h264的sequence header信息的. 所以初始化的时候使用了一些默认的参数,比如frames_per_packet就被默认初始化为1.

解码时通过一个循环解码每个音频包,由于这里frames_per_packet被初始化为1,所以只能解码得出第一个音频frame.这就是问题所在了.

1
2
3
4
5
6
7
for (int i = 0; i < s->frames_per_packet; i++) {
    ret = speex_modes[s->mode].decode(avctx, &s->st[s->mode], &s->gb, dst + i * s->frame_size);
    if (ret < 0)
        return ret;
    if (avctx->ch_layout.nb_channels == 2)
        speex_decode_stereo(dst + i * s->frame_size, s->frame_size, &s->stereo); data: dst + i * s-
}

这个问题根因是因为speex封装到flv没有提供头信息表明它的一些解码参数,我去查了flv标准文档,它是支持speex编码的,但是里头并没有明确说明是否应该包含头信息.而ogg格式的封装是有这个头信息的.

那么我的flv源文件是怎么来的呢,也是通过ffmpeg系列编码封装得到的.

1
ffmpeg -i media.mp4 -c:a libspeex -frames_per_packet 3 -c:v copy xx.flv

使用这样的ffmpeg命令得到的flv文件就是不含speex头信息的.

那么为什么libspeexdec这个解码器就能正常工作呢,看看它的代码吧, 它的初始化流程和native speex decoder基本一样,只是解码过程中,没有依赖与frames_per_packet的值,而是直接将一个AVPacket的数据送入libspeex解码器接口中,由第三方的libspeex解码器处理解码,最终能够的到正确的解码数据.