Dropbox Replay 如何让所有人保持同步

如何为远程分布式团队重现面对面放映室的体验?这是我们打造新视频协作工具 Dropbox Replay 的原因之一。客户告诉我们,转向虚拟办公后,曾经简单直接的审片过程变成了冗长、低效的视频通话。过去的线下工作流程显然没有顺畅地转移到线上。

考察虚拟放映工具市场时,我们看到的大多是好莱坞级别的昂贵方案,或用视频会议工具拼出的笨拙体验。它们都没有提供我们设想的协作式、实时、同步播放体验:共享反馈、光标与播放控制,让人仿佛共同置身放映室。我们认为有机会打造一种易于使用而又高质量的在线放映体验,大家能像面对面一样实时协作,看到相同画面,并无缝地对正在观看的内容作出实时批注。因此,我们创建了 Replay 的 Live Review 功能。

不过,让多人参与的虚拟放映室保持同步,比想象中困难。Live Review 中的任何人都能随时暂停、调整播放速度,或拖动到另一帧。这让放映开放而协作,却也意味着每个人可能同时发出冲突指令。如果两个人同时改变视频位置,会怎样?

要让 Live Review 正常工作,所有人的播放状态必须及时收敛到一致。当某人准备讨论一帧画面时,他必须能相信其他人也正看到同一帧。

状态变化

在 Live Review 会话中,我们关心两类状态:单个客户端状态与共享客户端状态。某人的鼠标位置,或他在视频帧上画出的内容,属于单个客户端状态。它们不会与其他客户端冲突,因此比较容易处理:将变化发送到服务器,再由服务器转发给其他客户端即可。

共享客户端状态则负责让会话中所有客户端的本地播放保持同步。某人加入会话时,客户端与 Replay 服务器建立 WebSocket 连接。我们使用开源 Go 库 Gorilla,Dropbox 的其他部分也使用它。每当有人播放、暂停或改变视频位置,客户端都会将变化发给服务器;服务器更新共享状态,再发给会话中的其他所有人。

播放状态采用 Protocol Buffers 编码,提供一种可扩展、具有模式定义而且节省空间的数据封装方式。Protocol Buffers 与 WebSocket 的组合很合适:前者不处理消息分帧,把线上传输格式交给开发者决定;后者基本不关心内容,提供已分帧、可靠且有序送达的文本或二进制消息,还处理 ping/pong 心跳,使会话能经过那些可能关闭空闲连接的代理或防火墙而保持存活。

理想情况下,每次只有一个人操作视频。但多数放映不是这样:每个人都有话要说,或有内容想让大家看到,而且常常同时发生。正因为任何人都能随时影响播放,协议才必须稳健地处理多个并发交互,并为所有参与者产生一致结果。

建立事件顺序

一种做法是把“视频停在某帧”或“光标到了某位置”等所有状态变化发给其他客户端。但共享状态下,如果多个客户端同时改变状态,就很容易出现不一致。例如 Patty 作出变化并发给 Steven,但在消息到达前,Steven 也作出了变化并发送出去。

直接交换状态变化时,Patty 与 Steven 可能采纳对方状态、丢失自身状态,最终不同步。
直接交换状态变化时,Patty 与 Steven 可能采纳对方状态、丢失自身状态,最终不同步。

如图,两人很容易采用对方状态而丢失自己的状态。对 Patty 而言,她先跳到第120帧,随后 Steven 让她跳到第240帧;对 Steven 而言,他先跳到第240帧,随后 Patty 让他跳到第120帧。如何在这种情况下保持同步?

可以尝试为消息加入时间戳,让每个客户端判断是否收到旧消息并忽略它,但这依赖各客户端时钟同步。既然我们可能不需要知道某事件发送的准确时刻,何不把排序工作交给服务器?

服务端同步服务可以为来自各客户端的消息建立逻辑时钟,更准确地说,是 happened-before(先发生)关系。没有服务器时,客户端必须具有非常精确的时钟,或进行更复杂的协商。将消息交给服务器,由它建立本地事件先后顺序并广播给所有客户端,就能以更简单的方式达到同一结果。

不错,但还不够好

WebSocket 已保证消息按顺序送达,剩下的问题是确保服务器按正确顺序处理收到的消息。我们的服务器如何实现?


type PlaybackState struct {
  frame     int64
  rate      float32
}
...
// Each connection is assigned a unique sessionId
// Handled by a seperate Goroutine 
// Protobuf messages are decoded from binary websocket messages
func handleMessage(msg *SyncMessage, sessionId string) {
  switch messageType := msg.GetMessageType().(type) {
  ...
  case *Message_Playback: {
      // mutex is a sync.RWMutex
      mutex.Lock() // Lock the state or wait if another session is holding the lock
      // Update the state
      playbackState = PlaybackState{
        frame:     frame,
        rate:      rate,
      }

      // Send the new state to all users, including the sender
      SendMessageToAllIncludingSender(sessionId, msg)
      mutex.Unlock() // Unlock only once we've sent the message to all clients
    }
  ...
  }
}

同步服务保证:对每条播放消息的响应先发送给所有客户端,随后才开始处理下一条消息,从而确立统一的先发生关系。它还会把消息回送给发送者,让发送者也能了解时间线。为什么这一点重要?

服务器把消息回送给发送者后,Patty 与 Steven 最终收敛到同一位置。
服务器把消息回送给发送者后,Patty 与 Steven 最终收敛到同一位置。

回送消息后,两人最终到达同一位置。这很好。不过,虽然客户端收敛到相同状态,他们走过的路径仍然不同。未必总是坏事,但我们看看各自的体验。

Steven 的体验

  1. 开始时暂停在第120帧。
  2. 按下播放。
  3. 视频开始播放。
  4. 随后看到 Patty 把视频暂停在第0帧,他的视频也变成相同状态。

Patty 的体验

  1. 开始时暂停在第120帧。
  2. 跳到第0帧,并保持暂停。
  3. 由于 Steven 的消息,视频又从第120帧开始播放。
  4. 短暂延迟后,视频跳回第0帧,并保持暂停。

Steven 的体验还算不错,Patty 的体验却令人意外又笨拙。我们可以做得更好。

让所有人获得顺畅体验

前面提到,同步服务充当播放消息的逻辑时钟,确保接收消息的顺序反映在发送给每个客户端的顺序中,也包括最初的发送者。我们能否利用这一点改善 Patty 的体验?可以。

从 Patty 跳到第0帧,到她收到服务器回送的自己的消息之间,她收到的每条状态变化消息,都有一个重要特性:按服务器确立的顺序,它们都发生在 Patty 的帧变化之前。

为什么能确定?同步服务逐条、按顺序处理并响应消息。因此,在自己的消息被回送之前收到的任何消息,必然排在她最初的消息之前;客户端可以安全地忽略这些消息。

Patty 的消息在 Steven 的消息被处理并发送给所有客户端之后才会被处理,回送顺序可用于忽略较早状态。
Patty 的消息在 Steven 的消息被处理并发送给所有客户端之后才会被处理,回送顺序可用于忽略较早状态。

再看各自的体验:

Steven 的体验

  1. 开始时暂停在第120帧。
  2. 按下播放。
  3. 视频开始播放。
  4. 看到 Patty 暂停在第0帧,自己的视频也同步过去。

Patty 的体验

  1. 开始时暂停在第120帧。
  2. 跳到第0帧,并保持暂停。

最终,Steven 和 Patty 都获得了顺畅体验。

亲自试试

当然,还有其他同步方案:构建一个跨客户端记录、转发和重放事件的系统,或使用操作转换等已有同步算法。这些方法在技术上都能保持客户端同步,但同样重要的是:无论采用什么方案,Live Review 都必须让用户感觉良好。优先考虑用户体验,使我们最终选择的算法,比仅关注工程问题时更简单。

我们认为,最终体验顺畅而无缝,正是接收作品反馈时所需要的。不过,不必只听我们说:在原文发表时,Dropbox Replay 已开放免费试用的 beta 版本。欢迎创建自己的 Live Review 会话,并告诉我们你的想法。

另外:我们在招聘

你喜欢构建新事物吗?你是否是一位充满好奇心、愿意探索新想法的工程师?Dropbox 正在招聘。

Replay 团队小巧、敏捷、富有创造力。我们专注于突破边界,致力于让用户生活更轻松、更高效。我们一直寻找聪明而好奇的工程师:愿意学习新知识、迎接更大挑战,并构建客户喜爱的产品。如果你有能力、热情和积极性,欢迎加入 Dropbox。访问招聘页面申请。

特别感谢 Dropbox Replay 团队。


来源:Dropbox Replay 如何让所有人保持同步。作者:Alan Rogers、Daniel Wagner、Siya Yang(2021年11月23日)。原文版权归原作者或所属机构所有。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容