开发者投稿|深入Storyteller:实时协同Tutorial编辑器

作者:余彦臻

深入 Storyteller:实时协同 Tutorial 编辑器

在刚刚结束的 RTE 2020 创新挑战赛中我提交了名为 Storyteller 的作品,其功能是一个“实时协同的交互式 tutorial 编辑器”。在本文中我会深入 Storyteller 的技术细节,聊聊声网 Agora 的实时服务如何在这个场景下大放异彩。

从零开始设计

当我开始构思这个作品时,我有两方面的考量:

  • 这个作品要在什么场景中解决什么问题
  • 声网 Agora 所提供的实时服务会为作品带来哪些助力

下文也会始终围绕这两方面的思考进行分享。

在寻找灵感时,我首先从日常的工作中入手。工作中我主要负责的产品是一个 toB 基础设施类软件的 Web 控制台,我们发现对于这类产品大家都有快速向客户演示的需求。

在过去,为了更好的演示,我们往往会选择以下几种形式:

  • 搭建真实环境用于演示。这一方式的弊端是资源消耗大,例如需要使用 3 台物理服务器搭建;另一方面由于是新环境,可以演示的内容也较少。
  • 录制视频。这一方式观看者参与感较差。
  • 基于 mock 数据开发 Demo 程序。这一方式开发和维护成本很高,并且有一些底层行为难以模拟。

因此我就将目标定为实现一种超越以上所有方式的演示工具,既能够像视频一样稳定、轻量,又能够像真实程序一样进行交互,同时还不需要编写代码,任何人都可以制作。

我将这种形式称为“交互式 tutorial”,而 Storyteller 就是一个“交互式 tutorial 编辑器”,两者的关系与视频和视频编辑器类似。

另一方面,我开始思考实时服务如何与 Storyteller 相结合。

事实上,同为“实时”,声网 Agora 的实时服务也和我们在 Web 场景中常见的 websocket 等通信方式有所不同,它与 WebRTC 更为相似,但提供了更多可靠性、传输性能方面的优化。

而这类实时服务最大的特点就是点对点的传输能力。也就是客户端之间可以直接通信,而不需要将数据传递给一台中心化的服务器再进行分发。所以在我的设想中,应该将这样的网络链路优势最大化,才能够让 Storyteller 真正插上实时协作的翅膀。为此,我准备在 Storyteller 中增加两个功能:

  1. 多人实时协同编辑。传统的视频编辑类软件大多数单机软件,而 Storyteller 可以在这方面做出突破,将多人实时协同的能力集成到 tutorial 编辑的过程里,加速 tutorial 的制作。
  2. 云端录制视频旁白。在一些需要讲解的场景,仍然可以将视频和交互式 tutorial 相结合,一同播放。而这个视频的录制也可以包含在 Storyteller 内,并且基于声网 Agora 云端录制的能力,获得最优的视频上传链路。

交互式 tutorial 的技术细节

首先我们可以通过两张示意图快速理解 Storyteller 编辑器和交互式 tutorial 的形态。

Storyteller 编辑器:

交互式 tutorial:

从示意图中可以看出,交互式 tutorial 的实现中有一个非常重要的细节:Web 视图静态化。

Web 视图静态化

所谓“静态化”是与动态的 Web 内容相对应的。举例来说,如果我们只是记录了示例中 TODO list 内的操作:

  1. 00:01 - 完成 Todo 3
  2. 00:05 - 添加新 Todo 4
  3. 00:10 - 将已完成的 Todo 2 标记为未完成

当再次在包含真实动态逻辑的 TODO list 中再次执行这些操作记录,可能就会得到完全不同的结果,甚至执行失败。

这也就是交互式 tutorial 需要解决的核心问题:像视频一样将 Web 视图中的内容及其变化保存下来,以特定的数据格式记录,再通过 JS 重建视图及变化达到回放的效果。

与普通视频相比,这样的录制方法有以下优点:

  • “画质”无损,因为最终重建并播放的是真实的 DOM,所以不会像图像数据被压缩损失画质。
  • 回放时可感知上下文。同样因为重建了完整的 DOM,所以交互式 tutorial 可以在任意时间点知道当前 DOM 的结构,也就可以在其基础上进行精细的编辑,例如给特定 DOM 增加 tooltip 提示。
  • 易于压缩,最终文件体积与同时长的视频相比更有优势。

当然这样的录制实现并不容易,可以简单列举一部分需要解决的问题:

  1. 将 Web 视图制作为可序列化的快照。与 HTML 等内容不同,这个快照需要记录更多的视图状态,并将动态脚本内容移除。
  2. 对所有可能对 Web 视图造成变化的因素进行观测,并记录为可序列化的事件。这一实现复杂且细节众多,但也是高性能录制的基础。
  3. 将各处的相对路径转为绝对路径,保证回放时的稳定性。
  4. 回放时通过复制 CSS 样式的方式模拟 hover 行为。

在 Storyteller 内我使用了由我开源维护的项目 rrweb 以实现这部分功能,更多与录制回放相关的技术细节可以从仓库的文档中进行了解。

完成基本的操作录制后,还需要给录制好的内容增加一定的特效,才能让交互式 tutorial 的内容变得更为丰富,我将这部分功能称为“剪辑 tutorial”

剪辑 tutorial

由 rrweb 录制的内容是一系列按时间线排列的快照与事件,所以剪辑 tutorial 的过程就是在同一条时间线上插入特定的事件,在特定的时间点进行执行。示意图如下:

图中蓝色的 tooltip 特效就是通过 Storyteller 的编辑器插入到录制的数据当中。在特定的时间点展示 tooltip 并暂停,就能够实现 tutorial 教学的效果。

除了基本的 tooltip 之外,Storyteller 还基于同样的方式拓展出更多的“特效”,例如:

  • 快进
  • 显示/隐藏鼠标
  • 显示旁边视频

实时协同

Storyteller 内交互式 tutorial 的数据结构其实非常适合增加实时协同的功能,因为所有的快照、事件、特效数据都是可以序列化的,也就可以通过网络传输给其它编辑器客户端,让多个用户同时编辑同一份 tutorial。

但是在开始实现之前,还需要确定 Storyteller 的实时协同在客户端之间是传递全部数据还是部分数据。

传递部分数据示意图:

1601958629492

所谓传递部分数据,就是在多个协同编辑的客户端之外,还有一个中心化的后端服务器。交互式 tutorial 的数据(示意图中的 story1, …, story3)都持久化的保存在后端服务器中。

当第一个客户端需要编辑时,就像服务器请求对应的 story1 数据。而当第一个客户端向第二个客户端发起协同编辑的邀请时,只会将 ID: 1 这一标识进行传递,第二个客户端获得 ID 后自行向服务器请求,得到相同的 story1 数据。至此两个客户端就获得了相同的数据,可以开始进行协同编辑,而后续的编辑动作因为数据量相对较少,只需直接在客户端直接同步,并定时保存至后端即可。

全部数据传递示意图:

1601958972101

而所谓的传递全部数据,则是不依赖一个中心化的后端服务器,由第一个客户端在分享时将本地的 story1 数据全量传递给第二个客户端,双方也就达到了相同的状态。

两种实现方式各有利弊,为了充分体验声网 Agora 实时消息 SDK 的功能,我选择了后者。

因为传递的数据都是可序列化的文本格式,所以我使用声网 Agora 实时消息的 SDK 作为网络传输层,它提供了客户端之间点对点的高可用传输。

在使用的过程中,实时消息 SDK 的时延和稳定性都明显优于基于 websocket server 实现的网络传输层。考虑到实际场景中可能存在客户端之间物理距离很近、距离中心化服务器很远的场景,两者的差距可能会比本地测试更为明显。

从上图的示意中就可以看出,基于实时消息 SDK 的点对点传输(黑色箭头)传输链路明显比经过中心化服务器转发(绿色箭头)的链路更为高效。

在完成这部分的实现之后,我也发现以下几点可以进一步优化的内容:

  1. Storyteller 第一版的实时交互实现了一个非常简单的协议,规定了几种语义,例如 sync-story, add-effect 等。在真实场景中,我们可以使用更为成熟的理论例如 CRDT 来描述协同数据的格式及变化,以保证在多个客户端之间不会产生数据冲突。
  2. 声网 Agora 实时消息 SDK 有发送数据量和频率的限制,因此也不适合直接传输大量数据。如果将 Storyteller 重新改造为传递部分数据的实现方式,可以与实时消息 SDK 结合的更好。

云端录制视频旁白

上文中我们提到过,在播放交互式 tutorial 的同时,我们可以插入视频类的特效,以起到视频旁白的效果。这个功能最初的灵感来源于我翻阅声网 Agora 实时服务文档时看到的“云端录制”服务。

在仔细阅读云端录制的文档之前,我还在设想如何获取各个客户端录制的音视频流数据,再在本地进行合并后上传。但当理解云端录制的工作方式后,我发现它的数据流更为合理,并且非常易于使用。

我们同样使用示意图帮助理解云端录制中各方的关系,但首先我们明确云端录制中各方的名称:

  • client 1,参与编辑的一个客户端。
  • client 2,参与编辑的另一个客户端。
  • cloud recording,云端录制服务。
  • object storage,用于保存录制视频的第三方对象存储。

设想的录制场景是 client 1 和 client 2 加入到同一个实施音视频 channel 中,一起完成这段旁边视频,并最终将这段视频上传至第三方对象存储中,再在交互式 tutorial 的数据中记录对应视频的 URL 地址,在对应时间点开始播放。

从云端录制的 API 上分析,它非常巧妙地以一个类似 shadow client 的形式加入了 channel 中,因此也就能够接收到 channel 内各个客户端的音视频数据流。与此同时,云端录制提供了一系列 API 用于控制:

  • 开始录制的时机
  • 录制的 channel
  • channel 内需要录制的客户端
  • 是否需要将多个客户端的音视频流合并

这样的设计带来了很大的灵活性,并且避免了在一个客户端进行这些操作再上传所带来的开销。

云端录制也对各种常用的对象存储都做了比较好的支持,只需要通过 API 配置对象存储的权限和元数据信息,就能够非常便捷地完成上传。

总结

在完成 Storyteller 的过程中,我对 rrweb 的适用场景有了更多探索,也尝试了一些实时协同场景的实现方案。

另一方面,也验证了声网 Agora 实时消息 SDK 的通用性。事实证明,实时消息 SDK 可以更广泛的被应用于各类数据传递的场景,而不只是作为音视频场景的辅助传递一些聊天信息。

最后,声网 Agora 云端录制的精巧方案也让我对一个统一、稳定的实时网络的价值有了更深的理解。

希望这篇文章可以对读者们也有所启发。

2赞