Netflix应用程序可在数百台智能电视,流媒体棒和付费电视机顶盒上运行。 Netflix合作伙伴工程师的作用是帮助设备制造商在其设备上启动Netflix应用程序。在本文中,我们讨论了一个特别困难的问题,该问题阻止了在欧洲推出设备。
2017年底,我正在召开电话会议,在新的机顶盒上讨论Netflix应用程序的问题。盒子是一个新的具有4k播放功能的Android TV设备,基于Android开源项目(AOSP)5.0版,又名“棒棒糖”。我已经在Netflix工作了几年,并交付了多种设备,但这是我的第一台Android TV设备。
涉及该设备的所有四个参与者都在通话中:欧洲一家大型付费电视公司(运营商)启动了该设备,承包商集成了机顶盒固件(集成商),系统上架了。芯片提供商(芯片供应商)和我自己(Netflix)。
集成商和Netflix已经完成了严格的Netflix认证过程,但是在电视运营商的内部试用期间,该公司的一位高管报告了一个严重的问题:Netflix在其设备上的播放“停顿了”,即视频播放时间很短。 ,然后暂停,然后重新开始,然后暂停。它并没有一直发生,但可以可靠地在打开包装盒后的几天内开始发生。他们提供了一个视频,看起来很糟糕。
设备集成商找到了重现此问题的方法:反复启动Netflix,开始播放,然后返回设备UI。他们提供了一个脚本来自动化该过程。有时只需要五分钟,但是脚本始终可以可靠地重现该错误。
同时,一家芯片供应商的现场工程师诊断出了根本原因:Netflix的Android TV应用程序Ninja不能足够快地传送音频数据。口吃是由设备音频管道中的缓冲区不足引起的。当解码器等待忍者传送更多的音频流时,播放停止,然后在有更多数据到达时恢复播放。集成商,芯片供应商和运营商都认为问题已被发现,并且向我传达的信息很明确:Netflix,您的应用程序中存在错误,需要对其进行修复。我可以听到操作员声音中的压力。他们的设备很晚,超出了预算,他们期望我能取得结果。
我对此表示怀疑。相同的Ninja应用程序可以在数百万个Android TV设备上运行,包括智能电视和其他机顶盒。如果忍者存在错误,为什么只在此设备上发生?
我首先使用集成商提供的脚本来重现问题。我联系了芯片供应商的同事,问他以前是否曾见过这样的事情(他从未见过)。接下来,我开始阅读Ninja源代码。我想找到提供音频数据的精确代码。我认识到很多,但是我开始失去回放代码中的情节,需要帮助。
我上楼去,发现了在忍者中编写音频和视频管道的工程师,他给了我有关代码的导览。我自己花一些时间在源代码上,以了解其工作部分,并添加自己的日志记录以确认自己的理解。 Netflix应用程序很复杂,但最简单的方法是从Netflix服务器流式传输数据,在设备上缓冲几秒钟的视频和音频数据,然后一次将视频和音频帧一次传送到设备的播放硬件。
我们花点时间讨论一下Netflix应用程序中的音频/视频管道。在每个机顶盒和智能电视上,直到“解码器缓冲区”为止的所有内容都是相同的,但是将A / V数据移入设备的解码器缓冲区是在其自己的线程中运行的特定于设备的例程。此例程的工作是通过调用Netflix提供的API来保持解码器缓冲区已满,该API提供下一帧音频或视频数据。在忍者中,此作业由Android线程执行。有一个简单的状态机和一些逻辑来处理不同的播放状态,但是在正常播放下,线程会将一帧数据复制到Android播放API中,然后告诉线程调度程序等待15毫秒,然后再次调用处理程序。创建Android线程时,您可以请求重复运行该线程,就像在循环中一样,但是调用处理程序的不是Android Thread调度程序,而是您自己的应用程序。
要播放60fps的视频,这是Netflix目录中提供的最高帧速率,设备必须每16.66毫秒渲染一次新帧,因此每15毫秒检查一次新样本就足够快,足以领先于Netflix可以提供的任何视频流。因为集成商已经确定音频流是问题,所以我将特定的线程处理程序归零,该线程处理程序将音频样本传递给Android音频服务。
我想回答这个问题:多余的时间在哪里?我假设处理程序调用的某些功能将是罪魁祸首,所以我假定假定有罪代码会在处理程序中散布日志消息。很快就会发现,处理程序中没有任何异常行为,即使回放停顿了,处理程序也仅在几毫秒内运行。
最后,我关注了三个数字:数据传输率,调用处理程序的时间和处理程序将控制权传回Android的时间。我编写了一个脚本来分析日志输出,并制作了下图,这给了我答案。
橙色线是数据从流缓冲区移至Android音频系统的速率,以字节/毫秒为单位。您可以在此图表中看到三种不同的行为:
数据速率达到500字节/毫秒的两个尖刺部分。此阶段正在缓冲,开始播放之前。处理程序正在尽可能快地复制数据。
中间的区域是正常播放。音频数据以约45字节/毫秒的速度移动。
当音频数据以接近10字节/毫秒的速度移动时,卡顿区域位于右侧。这不够快,无法维持播放。
不可避免的结论:橙色线确认了芯片供应商工程师的报告:忍者没有足够快地传送音频数据。
要了解原因,让我们看看黄色和灰色线条说明了什么。
黄线显示在处理程序例程中花费的时间,该时间是根据记录在处理程序顶部和底部的时间戳计算的。在正常和断断续续的播放区域中,在处理程序中花费的时间是相同的:大约2毫秒。峰值显示了由于设备上其他任务花费的时间而导致运行时间变慢的情况。
灰线是调用处理程序的两次调用之间的时间,它讲述了一个不同的故事。在正常播放情况下,您可以看到处理程序大约每15毫秒被调用一次。在断断续续的情况下,右侧大约每55 ms调用一次处理程序。两次调用之间需要额外花费40毫秒,并且无法跟上播放的速度。但为什么?
我向集成商和芯片供应商报告了我的发现(看,这是Android Thread调度程序!),但他们继续反对Netflix的行为。您为何不每次调用处理程序时仅复制更多数据?这是一个公平的批评,但是改变这种行为涉及比我准备做的更深刻的改变,因此我继续寻找根本原因。我研究了Android源代码,并了解到Android线程是一个用户空间构造,并且线程调度程序使用epoll()系统调用进行计时。我知道不能保证epoll()的性能,因此我怀疑是某些因素在系统地影响epoll()。
这时,我被芯片供应商的另一位工程师救了出来,他发现了一个错误,该错误已在名为Android的下一个版本的棉花糖中修复。 Android线程调度程序会根据应用程序是在前台运行还是在后台运行来更改线程的行为。后台线程被分配了额外的40 ms(40000000 ns)等待时间。
Android本身的管道中深处的一个错误意味着,当线程移至前台时,会保留此额外的计时器值。通常,音频处理程序线程是在应用程序位于前台时创建的,但是有时线程创建的时间要早一些,而Ninja仍在后台。发生这种情况时,播放会停顿。
这不是我们在此平台上修复的最后一个错误,但它是最难找到的。它在Netflix应用程序之外,在播放管道之外的系统一部分中,并且所有初始数据都指向Netflix应用程序本身中的错误。
这个故事确实体现了我所爱的工作的一个方面:我无法预测我们的合作伙伴会向我提出的所有问题,而且我知道要解决这些问题,我必须了解多个系统,与出色的同事合作,并不断推动自己学习更多。我所做的事情直接影响到真实的人和他们对优质产品的享受。我知道人们在客厅里欣赏Netflix时,我是实现这一目标的团队的重要组成部分。