乐趣区

5步告诉你QQ音乐的完美音质是怎么来的,播放器的秘密都在这里

欢迎大家前往腾讯云 + 社区,获取更多腾讯海量技术实践干货哦~
本文由 QQ 音乐技术团队发表于云 + 社区专栏

一、问题背景与分析
不久前,团队发现其 Android 平台 App 在播放 MV 视频《凤凰花开的路口》时,会带有如电流声一般的杂音,这影响了用户体验。研发同学在初步定位时,发现有如下特征:

Android 平台杂音问题必现;
iOS、PC 平台能正常播放,没有噪音。

然而,各平台都是统一用 HLS 格式播放,即源头都是一样的。对于该问题,我们的定位思路如下:

梳理视频播放流程;
找到切入点排查。

二、播放流程概览

分析播放流程如上图(图中内容从左往右),概括其关键步骤如下:

播放器初始化:

创建读数据线程:read_thread;
创建存放 audio 解码前数据的队列:audioq;
创建存放 audio 解码后数据的队列:sampq。

数据读取:

①创建 context;
②探测协议类型:avformat_open_input;
③探测媒体类型:avformat_find_stream_info;
④获取音视频流:av_find_best_stream;
⑤打开媒体解码器:stream_component_open;
⑥读取媒体数据,获得 AVPacket:av_read_frame(ic, pkt);
⑦音视频数据分别送入 audioq 中;
重复⑥、⑦步骤到数据完毕。

音频解码:

在 audio_thread 中对 audioq 中的数据进行 decoder_decode_frame 解码;
解码后的帧 AVFrame 存放到 sampq 中;

音频播放:

aout_thread_n 中,通过调用回调接口 sdl_audio_callback,对 sampq 中的音频帧数据进行解码成 PCM 数据;
写入 PCM 数据到 buffer 数组,并由 AudioTrack 播放。

三、问题分解与切入
在梳理出播放流程后,标记出找到有可能出错的环节,方便进行“分层定位”(图中黄色标记)

播放下载文件是否有问题;
数据读取是否有问题;
音频解码逻辑是否有问题;

AudioTrack 的设置是否有问题;

接下来,根据难易程度,对上述环节逐个验证。
1、播放下载文件是否正常
把 Android 平台播放的 ts 文件与各平台的进行比对,发现两者一样,该环节正常。
2、AudioTrack 设置是否正常
通过日志检查 AudioTrack 以下配置参数:

采样率
位深
频道

以上参数设置的值与音频流的相符合,该环节正常。
3、音频解码逻辑是否有问题
验证解码逻辑是否有问题,可以通过对 PCM 数据进行分析来确认。对 aout_thread_n 进行修改,将 PCM 数据额外输出到本地,并与正常的 PCM 数据进行对比。
正常 PCM 数据频谱图:

异常 PCM 数据频谱图:

正常 PCM 数据波形图:

异常 PCM 数据波形图:

对比分析可得出:

从频谱图中看出,异常的 PCM 在人耳十分敏感的频响(1000~8000Hz)区域内的音频数据严重缺失,导致“杂音问题”
从波形图中看出,异常的与正常的无声区和有声区都吻合,若解封装、解码逻辑出现异常,极大几率是呈现无波动(一条直线的形式)情况。因此可以先大胆假设解码、解封装逻辑是符合预期的

若解码逻辑正常,再结合之前已经验证文件下载正常。可以推测是数据读取环节出现异常。
4、数据读取是否有问题
通过对数据读取的各步骤增加日志后,发现在 av_find_best_stream 音频流选择时出现异常:ffmpeg -i 发现,该视频 ts 分片有 2 个音频流

通过强制分别读取两条音频流数据播放,发现:

第一条正常播放(PCM 数据正常)
第二条播放杂音(PCM 数据异常)
Android 平台选择了第二条进行播放

基于此,也就验证了在第 3 步中的假设是正确的。
由上分析,可以得出结论:Android 平台选择了第二条数据有问题的流进行播放。
四、问题根源:音频流选择
1、选择方式
分析代码,大致如下所列,av_find_best_stream 函数选择音频流,该函数会根据 2 个主要参数进行选择:

各音频流的在探测媒体类型(avformat_find_stream_info)时,额外解码出来的帧数(选择多的)
各音频流的比特率(选择高的)

int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type,
int wanted_stream_nb, int related_stream,
AVCodec **decoder_ret, int flags)
{
for (i = 0; i < nb_streams; i++) {
count = st->codec_info_nb_frames; // 音频流探测中解码的帧数
bitrate = avctx->bit_rate;// 音频流的比特率
multiframe = FFMIN(5, count);
// 先比较解码帧数,再比较音频流比特率,谁大谁选
if ((best_multiframe > multiframe) ||
(best_multiframe == multiframe && best_bitrate > bitrate) ||
(best_multiframe == multiframe && best_bitrate == bitrate && best_count >= count))
continue;
best_count = count;
best_bitrate = bitrate;
best_multiframe = multiframe;
ret = real_stream_index;// 最后选择的流 index
best_decoder = decoder;
}
return ret;
}
在该视频中,我们可以看到:

codec_info_nb_frames
bit_rate

audio_stream 1
38
122625

audio_stream 2
39
126375

第二条流的解码帧数和比特率要比第一条高,因此选择了第二条流播放
2、对比同类方案
分析了以上选择规则后,我们对各平台、框架进行了选择规则的对比:

备注:

ExoPlayer 对多音频流的 ts 分片支持不完善(issue),因此测试时需要调整相关接口。但选择规则依然以上述所示(DefaultTrackSelector)
iOS 和 PC 平台采用闭源组件,因此测试时使用了“互换两条音频流顺序”的方法进行测试。互换后,两平台都播放了杂音音频流 ffmpeg -i INPUT_FILE -map 0:0 -map 0:2 -map 0:1 -c copy -y OUTPUT_FILE

QuickTime 同样是闭源,互换音频流后无法明显差别,通过合成第三条音频流,来验证是它是对所有音频流全播放 ffmpeg -i INPUT_FILE_1 -i INPUT_FILE_2 -map 0:0 -map 0:1 -map 0:2 -map 1:0 -c copy OUTPUT_FILE

3、总结
从以上数据看到,iOS 和 PC 平台会默认选择第一条流,而在 Android 平台的 FFmpeg 和 ExoPlayer 会根据音频流属性来选择数值更好的一条。

“默认选择第一条”方案能更容易地把音源问题暴露。
“比较音频流属性”方案能更大几率地选择质量更好的流来提升用户体验。

但以上 2 个选择方案都无法识别“内容异常”的音频流。
五、问题解决方案
因此,处理该问题,需要从音源上进行修复和规避,我们的建议是从源头杜绝,从终端规避:

编辑重新上架正常音源;
短期内增加双音频流的检测上报,帮助后台、编辑进行复查;
长远看由后台开发工具,分别对存量视频进行双音频流检测和对增量视频保证只转码单音频流;

参考资料

https://ffmpeg.org/doxygen/2.8/
https://github.com/google/Exo…
https://www.jianshu.com/p/daf…
https://www.jianshu.com/p/a6a…
http://km.oa.com/articles/sho…
https://codeday.me/bug/201707…

相关阅读 wamp2.0 配置 Zend Optimizer 藏匿在邮件里的“坏小子”打造一个个人阅读追踪系统【每日课程推荐】机器学习实战!快速入门在线广告业务及 CTR 相应知识

退出移动版