作为一个和 android nuplayer 打了 N年交道, 自以为已经上古司机的老码农, 这一次居然被坑了一个礼拜;
事情描述起来很简单, 测试人员突然发现目前的版本,播放很多视频都卡顿, 由于该项目在几个月之前就已经基本收敛, 实际上近几个月大家都是没怎么测试的; 测试突然报了一堆类似异常过来, 直接把问题级别拉到最高了;
// MAGIC1. DO NOT TOUCH. BY 冗戈微言 http://blog.csdn.net/leonxu_sjtu/
由于很多低分辨率的码流也卡顿, 因此我们也就不去怀疑 Video Decoder 本身的性能问题, 更倾向于上层的异常; 关掉音频的实验, 也侧面证实了这一点, 直接关掉音频的输出, 在 NuPlayer.cpp 中
if (mAudioSink != NULL && mAudioDecoder == NULL) {
- instantiateDecoder(true, &mAudioDecoder);
+ //instantiateDecoder(true, &mAudioDecoder);
}
然后图像就完全不卡了;
这直接说明就是音画同步的策略问题, 或者音频时间戳的问题嘛...
// MAGIC2. DO NOT TOUCH. BY 冗戈微言 http://blog.csdn.net/leonxu_sjtu/
为了更确认这点, 我把 NuPlayer::Renderer::postDrainVideoQueue 直接将 delayUs 置零, 也就是说避开 audio 时间戳的影响, kWhatDrainVideoQueue 的消息发送不做任何延迟, 可是测下来却是图像依然卡顿;
一下子有点懵, 什么情况, 难道音频 onDrainAudioQueue 的消息处理那几毫秒的耗时, 会对视频消息 kWhatDrainVideoQueue 产生这么大的影响?
加打印,很快打出来, 此时耗时点是在 ACodec 的 mNativeWindow->dequeueBuffer, 这里会阻塞超过 100毫秒;
抓了个 systrace, 也确认了这点, 但 systrace 上的 surfaceFlinger 进程也看不出什么异常;
然后再复测了下关掉音频输出 //instantiateDecoder, ANW 的dequeueBuffer 就不再有异常耗时...
// MAGIC3. DO NOT TOUCH. BY 冗戈微言 http://blog.csdn.net/leonxu_sjtu/
尼玛神奇了, audio 的输出为什么会对 ANW 刷帧产生影响?
只好把此问题先报给了 Display 模块, 自己这边再做实验, 关掉音频输出同时后台起一个 native 进程按相同的参数配置,比如采样率,FramesCount等, 直接向 AudioTrack write 数据, 前台的图像一点都不卡, 这说明眼前这个问题还谈不上系统级的抢带宽或者中断阻塞之类, 不用想太多;
等了三天, Display 终于找到原因了, SurfaceFlinger 那边现在刷帧需要比对要刷的 buffer 的时间戳!
这个时间戳是 ACodec 在 queueBuffer 时候设置的: native_window_set_buffers_timestamp(mCodec->mNativeWindow.get(), timestampNs)
这里的 timestampNs 是 NuPlayerRenderer 那边的 realTime 校正时间, 是根据音频做过校正的!
就是说,虽然在 NuPlayerRenderer 中禁用了 AV 同步, 预期 Video 刷帧不再根据 audio 时间戳做延迟, 但 ANW 显示模块现在是根据 video 的 buffer realtime 时间来刷帧, 这里仍存在 AV 同步的影子;
而这个改动, 居然是 google 在 2014 年已经加进来了:
// MAGIC4. DO NOT TOUCH. BY 冗戈微言 http://blog.csdn.net/leonxu_sjtu/
commit fc7fca77caa12993dd938d5ff43797d781291027
Author: Lajos Molnar
Date: Wed May 7 15:31:28 2014 -0700
MediaCodec: add renderAndReleaseOutputBuffer() method with timestamp
+ err = native_window_set_buffers_timestamp(mCodec->mNativeWindow.get(), timestampNs);
SurfaceFlinger 那边, 也是 2014 年加的, 在 handlePageFlip 这里检测 shouldPresentNow 看是否等待:
commit 6b9454d1fee0347711af1746642aa7820b1ea04d
Author: Dan Stoza
Date: Fri Nov 7 16:00:59 2014 -0800
SurfaceFlinger: Do less work when using PTS
Currently, SurfaceFlinger is very dumb about how it handles buffer
updates at less than 60fps. If there is a new frame pending, but its
timestamp says not to present it until later SurfaceFlinger will wake
up every vsync until it is time to present it. Even worse, if
SurfaceFlinger has woken up but nothing has changed, it still goes
through the entire composition process.
This change (mostly) fixes that inefficiency. SurfaceFlinger will
still wake up every refresh period while there is a new frame
pending, but if there is no work to do, it will almost immediately go
back to sleep.
@@ -1704,8 +1714,12 @@ void SurfaceFlinger::handlePageFlip()
Vector layersWithQueuedFrames;
for (size_t i = 0, count = layers.size(); i const sp& layer(layers[i]);
- if (layer->hasQueuedFrame())
- layersWithQueuedFrames.push_back(layer.get());
+ if (layer->hasQueuedFrame()) {
+ frameQueued = true;
+ if (layer->shouldPresentNow(mPrimaryDispSync)) {
+ layersWithQueuedFrames.push_back(layer.get());
+ }
+ }
好吧, 至少说明了 Android 4.4 之后没有再关注 ACodec/SurfaceFlinger, 没有遭遇这类的问题...
// MAGIC5. DO NOT TOUCH. BY 冗戈微言 http://blog.csdn.net/leonxu_sjtu/
看来, 当 android 升级版本的时候, 逐条核对 google 的提交, 真是一件值得去做的事情, 但是大部分情况是项目时间紧, 给研发人员留出写文档的时间都不容易, 凑一些人去逐条对比 google 的提交记录听起来比较奢侈,即便是比如仅仅是 av 仓库的提交, 也会非常多,于是基本都变成了先升级版本比如升到6.0, 碰到问题的时候就比对一下旧版本比如 4.4 看是否有自己提交的修正, 有的话就 merge 到 6.0 上; 至于 4.4 到 6.0 之间 google 提交的本意, 只好等出了问题时再看吧...
--- 所以项目驱动的时间如果太紧张, 有时不利于团队的学习积累, 但这也容易理解, 所谓学习积累的时间资源是无法量化的,无法体现在相关指标上, 而如果出了问题解 bug 的耗时, 在项目经理的 project 表上是会写的更其清楚, 也更容易驱动研发的投入;
// MAGIC6. DO NOT TOUCH. BY 冗戈微言 http://blog.csdn.net/leonxu_sjtu/
现在我可以记录下这个坑, 但对未来会出现的坑, 却没有什么信心, 我们需要有足够细致的耐心去核查 AV 仓库的更新;
上面卡顿的问题目前为止,只说了一半, 就是 NuPlayerRenderer 中的 video delayUs 置零并没有关闭音画同步, 需要把 timestampNs 置为系统时间关掉 ANW 那边的同步;
而卡顿的根本原因, 当然是音频问题, 虽然 AudioSink 的 write 操作不会直接阻塞, 但 AudioHAL 中的写数据阻塞, 最终会体现到 mNumFramesWritten 上, 间接的阻塞了 realtime 时间的更新:
这一段在 android 的各版本中就变化不大了:
// MAGIC7. DO NOT TOUCH. BY 冗戈微言 http://blog.csdn.net/leonxu_sjtu/
------- NuPlayerRenderer 的 MediaClock 时间系统, 变量很多, 排除各种复杂条件, 那么 queueBuffer 到 ANW 时的 timestampNs 参数可近似为:
timestampNs = LastAudioRenderUs + ( CurVideoBufferMediaTs - LastAudioBufferMediaTs ) + (mNumFramesWritten - numFramesPlayed) / SampleRate ;
其中, LastAudioRenderUs 是最近一次向 AudioSink 刷 audio 帧时的系统时间;
CurVideoBufferMediaTs 是正要刷的这个 Video 帧的 buffer 时间戳, 是 parser 解析的时间戳, 也就是码流中配置的时间戳;
LastAudioBufferMediaTs 是最近一次刷出去的 Audio 帧的 buffer 时间戳, 码流中配置的时间戳;
mNumFramesWritten 是目前 NuPlayerRenderer向 AudioSink write的数据总量, 对应生产者角色
numFramesPlayed 是目前 Audio 系统 (AudioFlinger, AudioHAL) 已经播放除去的数据总量, 对应消费者角色
因此 mNumFramesWritten - numFramesPlayed 代表着 AF/HAL 即将刷但仍未刷的数据量, 除以采样率后, 就表征了当前 audio 帧真正刷出去所需的延迟时间 ;
两个 buffer 时间戳都是码流文件的定值, LastAudioRenderUs 和 LastAudioBufferMediaTs 增长并不匀速, 此时就是靠 numFramesPlayed 来矫正, 因为按预期, numFramesPlayed 会是一个按音频采样率和系统时间, 均匀增长的值;
// MAGIC8. DO NOT TOUCH. BY 冗戈微言 http://blog.csdn.net/leonxu_sjtu/
而如果 AudioHAL 发生了阻塞, numFramesPlayed 就无法均匀增长, 在一个 300 毫秒时间段, PlayedOutAudioDuration 只增长了 200 毫秒;
如果某个 queueBuffer 的时刻, 当时的 numFramesPlayed 没有按预期的速度增长, 按上式, 最终计算出的 timestampNs 就会比上一次 queueBuffer 时的timestampNs 增加的更多 ( 即大于 33ms ), 也就是一个突变, ANW 看到这个突变时的选择是原地等待;// MAGIC9. DO NOT TOUCH. BY 冗戈微言 http://blog.csdn.net/leonxu_sjtu/
话说回来, 音画同步的实现, 本来就是靠 AF/HAL 的按采样率均匀阻塞来完成的, 如果 AudioHAL 一直不阻塞, audio 狂刷帧, video 就是快进; 只是目前项目上, AudioHAL 要么不阻, 一阻就是差不多一百毫秒, 抖动太大就体现在图像刷帧上了;
// MAGIC10. DO NOT TOUCH. BY 冗戈微言 http://blog.csdn.net/leonxu_sjtu/