| 14 min read

原文地址: https://medium.com/netflix-techblog/modernizing-the-web-playback-ui-1ad2f184a5a0

这一篇是奈飞前端工程师分享 Web团队在构建新的播放器时候遇到的困惑和解决思路,我们在设计播放器的时候,可能需要考虑到诸多方面,UI 扩展,测试,底层设计,面向多业务多团队等。希望这篇文章可以帮助到你。

从2013年开始,Netflix 的团队一直将重心放到开发视频播放的新特性,而始终未关注播放的视觉UI的改进。

而在过去两年,Web UI 团队制定了一个非常长远的计划,给我们的会员提供更加出色的播放体验。 播放由下面三个维度构成:

  • 播放前(Pre Play,前贴) 主要是视频播放前给用户呈现的内容
  • 视频播放中(Video Playback, 中场) 当用户正在观看视频,用户进行视频播放、暂停控制,调节音量等
  • 播后结束(Post Play, 后贴)当用户观看的视频结束的时候,出现下一集或者推荐内容等

在经过复杂的 AB 测试, 以及随后的总结,我们完成了我们播放器的 UI,下面是我们的分享这一路的心得体会。

成功没那么容易

从2016开始,我们优先是使用 React 重构我们的播放组件。在2015年的夏天,由于我们剩余的网站也全部迁移到 React ,但当时的播放器 UI 还是使用的自己的框架(vanilla JavaScript framework)。只有少数工程师了解该框架的使用, 可以进行 Bug 的修复或者新功能的开发。迁移至 React 的话,更多的开发工程师可以参与进来进行播放的体验构建。

随着开发资源的投入增加,我们需要抹平 React 和原有框架之间的差异, 使用React构建UI 组件 有利于降低原有的复杂性。
如同大多数奈飞的产品上线一样,我们也需要对播放器新UI 进行 AB 测试。希望我们的视觉和数据团队,会在 AB 测试投入足够多的精力。 我们控制的参考元对象是用自己开发的框架,以及原有视觉,而实验的特性则是新的使用 React 构建的新的视觉的播放 UI。

我们非常期待这一行为,似乎觉得前方一片希望。

然而我们很快意识到,我们太过快的去进行开发和设计,而没有足够的思考我们是否需要这样做。

我们努力的去写新的组件,移植逻辑,以及重构新的 UI, 2016年底,一切都就绪,然而,现实的结果是我们失败了。

一开始便倾覆所有

用户在使用新的播放 UI 后,观看的内容变少了。我们都很困惑为什么测试会不如预期。如果用户在使用新的 播放 UI 时候没有遇到任何困难的话,现在各个平台早已经接入了新的播放 UI。 我们需要挖掘出其中的原因,找出为什么新的播放 UI 破坏了用户体验。

变化原子性

我们初始化的测试有个致命问题,我们同时更改了视觉上的设计以及底层 UI 的架构设计。当我们去看不好的测试结果的时候,实际上我们并不能够找到什么原因造成的。究竟是 UI 设计还是程序架构,或者两者都有,我们无从而知。

通过较为复杂查找,我们最终确定新的视觉设计以及前移至react都给会员带来了影响。这个给了我们非常深刻的教训,告诉我们确保变化的独立性。

渲染差异

迁移至react 我们基本意味着对重建了播放上各个纬度的架构调整。通过 AB 测试的结果,我们发现了使用 react 构建的组件导致了启动响应较慢,以及播放的掉帧。

这个发现很让我们意外,通过深度的对比,我们自有的框架和react的实现,我们找到了性能差距。我们原有的框架是直接监听 video 事件来获取 UI 的状态变化。每一个组件的 class 都是会创建一个节点,然后通过通过播放器的 emit 来获取数据,然后根据事件响应来更新节点属性。然而,在react中,我们是通过根组件来获取数据流,然后这些数据再回传递至子组件。每一次播放器的数据更新都会造成re-rendering,这带来了很大的性能体验差异。

反复尝试

在知道影响我们初始化测试的因素后,我们开始尝试修复这些问题,以及重新调整我们的 AB 测试计划。

测试计划

我们计划接下来的ab 测试需要关注到架构的变化上来,迁移至 react 并不意味着需要改变原有的视觉设计。计划是需要复制原有的视觉设计基础上进行组件的开发。

关注速度

为了修复启动速度,我们需要找出是什么地方的渲染导致了时间的过多消耗。在关键的播放行为中我们加入了时间跟踪,这些跟踪记录主要是被标记在这些组件中。

// Inside Player.jsx...

componentDidMount() {
    performance.mark(
        `playbackMilestone:start:${this.state.playbackView}`
    );
}

componentDidUpdate(prevProps, prevState) {
    if (prevState.playbackView !== this.state.playbackView) { 
        performance.mark(
            `playbackMilestone:end:${this.state.playbackView}`
        );
        performance.mark(
            `playbackMilestone:start:${this.state.playbackView}`
        );
    }
    if (this.state.playbackView === PlaybackViews.PLAYBACK) {
        // serialize and log playback performance data.
    }
}

这些数据展示了组件在渲染视图的时间消耗。react 确实花了很长的时间相比我们原有的框架设计。这也证实了我们平行加载渲染播控组件的方式不是很友好。通过在播放前确保我们的控制组建已经就位,在播放器加载的时候, 我们改善了我们渲染的次数。

接下来我们需要阻止丢帧的发生。已经潜入到组件的性能测试工具会告诉我们渲染的时间,我们想到了一些方法去优化渲染时间:

  • 遵从react的实践,我们所有的组件都是依照 react 的最佳时间来开发。使用 来判断是否触发渲染

  • 降低高阶组件的数量我们尽可能的避免使用高阶组件,将它改成传统的函数,活着将逻辑调整到父组件里面去。

  • 去掉属性对象的展开 展开会造成更多的时间消耗

  • 可观察性。查看原有框架的说明书,对播放器的状态动态监测加入到变量观察中,这个能够降低根组件渲染周期

通过视觉设计和性能的优化,新的 AB 测试开始部署了。经过漫长的等待,结果终于来了,不过还是让人沮丧。会员依旧没能增加太多的观看量。在2017年的夏天,我们正式推出了基于 react 编写的播放控制。

Under the Hood: Simplifying Playback Logic

简化播放逻辑

为了使 react 的组件开发可以非常方便的访问以及各个团队的开发,我们要做相同的逻辑,比如交互的标题,电影以及电视剧的播放,视频预览,以及播后的内容展示。

我们选择 Redux 来做播放数据的流入以及复杂的业务逻辑控制。Redux 是前端工程非常出名的框架,它能够满足我们基本的目标。通过联合 Redux 的数据,我们实现了各个团队的无缝对接,为它们提供了标准,可预测的方式去实现复杂的业务逻辑。

分离视频和UI的生命周期

使用 UI 组件控制视频逻辑会导致更慢的用户体验。一般UI组件都有自己定义的一系列生命周期。而创建视频播放的逻辑则隐藏在这些UI的各个方法中。在播放开始前, 用户必须要等待特定的组件被调用。在初始化后,用户一直在等待,播放最终被加载出来然后才可以看到视频内容。

组件是服务端渲染好后的,但是这些 Dom 并不包含用于加载的 Video(Video Element) 以及buffer 数据。 在 react 中,客户端的 UI 需要自己重新构建初始化的 Dom ,然后才会触发生命周期的方法器开始加载视频。

然而一些逻辑用于控制播放,实际是在 UI 组件树外面,他可以在任何别的应用进行集成。这些逻辑是在 UI 树渲染之前进行加载。通过创建视频,同时进行渲染 UI,它可以帮助应用有更多的时间进行创建,初始化,以及进行视频的播放缓存。而当UI 完成渲染完,用户可以快速的进行播放。

标准化播放数据结构

因为播放是有一些列动态的事件组成,面临的一个问题是在应用的不同业务方面只关心自己的播放状态。比如,应用中的某个部分,只负责创建视频,而别的部分负责根据用户的个性化特性进行配置,又或者某些负责进行播放,暂停快进的控制。

为了进行封装,我们构建了标准的数据结构针对每个视频播放,这样无论是业务逻辑还是 UI 控制都能进行访问。这些机制可以帮助多个视频播放,多个 UI 在单一的数据集会更容易测试。

标准的播放数据,可以在任何视频资源下进行创建,一个自定义 video 库,又或者标准的 html video 元素。使用初始化的标准数据结构,释放了 UI 能力,无需关心特点的视频方法实现。

对多个视频播放实例的支持

如果我们拥有播放数据针对存在的单一资源的视频,它允许应用定义业务逻辑,以及协调单一或者多个视频播放。如果每个视频都是隐藏在一个特定 UI 组件实例下,这些组件则存在跨不同业务的UI 。因此这些独立逻辑可以支持更多的 UI 播放数据以及多个视频播放器:

  • 声音静音控制
  • 播放暂停
  • 优先自动播放
  • 允许共存的播放器数量

实现应用的状态管理

为了提供一个更好结构的独立的 UI 状态管理,我们觉得再度利用 Redux。 然而我们也知道使用一个项目而运行各个团队是非常难以管理的,他们可以添加或者移除逻辑,然后它们必须是独立并不需要所有的使用 case 。因此我们创建了一个极为轻量级的控制层在 Redux 上面,运行我们打包文件,关联到特定的业务逻辑,然后再组合成成具体的 Redux 应用。

我们系统中对业务层的划分就是一个静态的对象离,包括下面的一些东西:

  • 状态数据结构
  • 状态 reducers
  • Actions
  • 中间件
  • 用于查询状态的 API

应用可自己选择组合,而不是全部使用。当一个业务方创建了一个应用,业务方可以自动绑定到业务自己的状态,它并没有权限访问别的部分状态。更好的是,最终外面的 API 都是一致,无论怎样的接入。

我们授权了两者场景,单一的 redux 应用,然后每个部分都能知道完整的状态,又或者多个业务需要自己管理应用的子状态。辨别不同区业务层的逻辑的好处是可以概括性的进行业务变更,包括轻松进行增加,删除以及暂停,而别的应用而不需要终止任何事情。

启用可插拔的逻辑控制

由于我们业务层面的影响,我们能够继续关于播放核心逻辑的控制,而别的团队可以自己实现比如交互标题这些功能。 交互的标题需要自定义逻辑,和自定义的 UI 让用户可以自行选择播放什么内容。现在我们有了 UI 然后通过包含逻辑的状态,在多个前端上我们有一个系统去进行管理。我们依旧继续了很多 AB 测试,对逻辑的封装让我们让开发可以轻松根据测试结果进行逻辑或者功能的迭代。拥有一个比较强制数据节后和持续的构建,考虑到不同领域的逻辑能够帮助我们更好的辨别和确认不同方面的是明显不一致的。通过添加一些可以控制的开关,和可预测的以及放弃一些绝对的自由度去做这些事情最终解放了我们的生产率,别的团队可以快速的添加更多的功能。

代价与成长

在改善后状态管理以及开发模式,更多开发者渐入进来,我们的最后一步就是更新视觉UI效果。

前面我们了解到需要做变化的独立性,这个计划只关心 UI 对播放体验的影响,而不会去改变技术架构什么的。

利用我们的实现,结合 Redux 以及扩展组件,可以更好的进行 AB 测试。在 2018 年我们重新发布了新的测试,在秋天我们得到了想要的结果,会员更倾向于使用新的播放器,新的视觉帮助他们更好进行播放快进和后退。

最终的 AB 测试非常好实现以及分析了。犯过的错误以及学到的教训,帮助我们理解更为深刻,坚决不要试着一次性做太多的事情的实践。

You Can Speak "Hi" to Me in Those Ways