Firefox 多流和重新协商用于 Jitsi Videobridge

Firefox 多流和重新协商用于 Jitsi Videobridge

作者注:Firefox 在 Firefox 38 中实现了对多流和重新协商的支持。本文讨论了 Jitsi Videobridge(一个 WebRTC 服务)团队如何与 Firefox WebRTC 团队合作,使 Jitsi 的多方视频会议在 Firefox 中良好运行。在此过程中,系统双方都发现并修复了若干问题。Firefox 40(我们新发布的 开发者版)及更高版本包含所有这些修复程序。这篇文章由 Jitsi 工程师 George Politis 撰写,假设您具备一些 WebRTC 的基本知识 及其工作原理。

Firefox 是第一个实现符合规范的“统一计划”以支持多流的浏览器,Chrome 将迁移到此计划,但尚未实现。因此,当前在 Chrome 上运行的服务需要进行一些修改才能在 Firefox 上运行。我鼓励所有已添加或正在考虑添加多流支持的服务提供商尝试 Firefox 40 或更高版本,并告知我们您的使用情况。谢谢。

Maire Reavy
Web RTC 工程经理

简介

你们当中许多 WebRTC 开发人员可能已经遇到过 Jitsi Videobridge 这个名称。多方视频会议可以说是 WebRTC 最受欢迎的用例之一,一旦您开始寻找允许您实现它的服务器,Jitsi 的名称就是您首先遇到的名称之一。

一段时间以来,许多 JavaScript 应用程序一直在使用 WebRTC 和 Jitsi Videobridge 为其用户提供丰富的会议体验。该桥梁提供了一种轻量级的方式(考虑路由与混合)来进行高质量的视频会议,因此它也获得了相当多的关注。

问题在于,直到最近,使用 Jitsi Videobridge 的应用程序只能在有限的浏览器集中运行:Chromium、Chrome 和 Opera。

此限制现已解除!

在 Mozilla 和 Jitsi 开发人员数月的努力下,Firefox 和 Jitsi 都添加了缺失的部分,现在可以协同工作了。

虽然这不是地球上最困难的项目,但它也不是轻松愉快的散步。在这篇文章中,我们将向您详细介绍我们协作冒险的具体细节。

一些基础知识

Jitsi Videobridge 是一款开源(LGPL)轻量级视频会议服务器。诸如 Jitsi Meet 之类的 WebRTC JavaScript 应用程序使用 Jitsi Videobridge 提供高质量、可扩展的视频会议。Jitsi Videobridge 接收来自每个参与者的视频,然后将其中一些或全部视频转发给其他人。IETF 将 Jitsi Videobridge 称为 选择性转发单元 (SFU)。有时此类服务器也称为视频路由器或 MCU。大多数现代视频会议系统(如 Google Hangouts、Skype、Vidyo 等)都使用相同的技术。

从 WebRTC 的角度来看,每个浏览器都与 Videobridge 建立了一个 PeerConnection。浏览器通过该 PeerConnection 发送和接收所有音频和视频数据。

Jitsi Videobridge based 3-way call

在基于 Jitsi Videobridge 的会议中,所有信令都通过一个名为“Focus”的独立服务器端应用程序进行。它负责管理每个参与者与 Videobridge 之间的媒体会话。Focus 与给定参与者之间的通信通过 Jingle 完成,而 Focus 与 Jitsi Videobidge 之间的通信则通过 COLIBRI 完成。

统一计划、计划 B 和生命、宇宙以及一切的答案

在讨论 Firefox 和 Chrome 之间用于多方视频会议的互操作性时,不可能不谈论一下(或很多!)统一计划计划 B。这是 WebRTC 端点之间协商和交换多个媒体源(即 MediaStreamTracks 或 MST)的两个竞争性 IETF 草案。统一计划已纳入 JSEP 草案捆绑协商草案,这些草案即将成为 IETF 标准。计划 B 于 2013 年到期,理论上没有人应该再关心它了……至少在理论上是这样。

实际上,计划 B 仍然存在于 Chrome 及其衍生产品(如 Chromium 和 Opera)中。Chromium 错误跟踪器中实际上有一个问题,即添加对 Chromium 中的统一计划 的支持,但这需要一些时间。另一方面,Firefox,最近,实现了统一计划。

开发人员实现许多 WebRTC 基于视频会议的解决方案,并希望同时支持 Firefox 和 Chrome,他们必须处理这种情况,并在 Chrome 和 Firefox 之间实现某种互操作性层。当然,Jitsi Meet 也不例外;一开始,假设计划 B 是理所当然的,因为这是 Chrome 实现的,而 Firefox 还没有 多流支持。因此,Jitsi 的大多数抽象都是围绕着这个假设构建的。

统一计划和计划 B 之间最本质的区别在于它们如何表示媒体流轨道。统一计划扩展了在 SDP 中编码此信息的标准方式,即让每个 RTP 流(即 SSRC) 出现在其自己的 m 行 上。因此,每个媒体流轨道都由其自己的唯一 m 行表示。这是一个严格的一对一映射;单个媒体流轨道不能跨越多个 m 行,单个 m 行也不能表示多个媒体流轨道。

计划 B 采用了不同的方法,并在 SDP 中创建了一个层次结构;m= 行定义了一个“信封”,指定编解码器和传输参数,a=ssrc 行用于描述该信封内的各个媒体源。因此,通常,计划 B SDP 有三个通道,一个用于音频,一个用于视频,一个用于数据。

实现

在 Jitsi 端,从一开始就很明显,所有魔法都应该在客户端发生。Focus 使用 Jingle 与客户端通信,Jingle 又被转换为 SDP,然后传递给浏览器。网络上没有 SDP 传输。此外,端点和 Jitsi Videobridge 之间没有信令通信,而是 Focus 使用 COLIBRI 来仲裁此过程。因此,Jitsi 团队的问题是:“鉴于我们在所有可以想象的地方都有假设计划 B 的代码,那么从 Jingle 到 Firefox 的统一计划的最简单方法是什么?”

在最初的几次尝试中,Jitsi 团队试图在出现特定于计划 B 的代码的地方提供通用抽象。这本来可以奏效,但在同一时期,Jitsi Meet 正在进行一些大规模的重构,并且传入的统一计划补丁不断被破坏。最重要的是,由于 Firefox 中的多流支持处于非常早期的阶段,因此 Firefox 的崩溃频率高于其工作频率。结果:进展为 0。甚至可以说,由于浪费了时间,进展是负面的。

是时候改变方向了。Jitsi 团队决定尝试更通用的解决方案来解决问题,并在更低的层级上处理它。其想法是构建一个 PeerConnection 适配器,它会将正确的 SDP 提供给浏览器,即 Firefox 的统一计划和 Chrome 的计划 B,并且会向应用程序提供计划 B SDP。引入 sdp-interop

SDP 互操作性层

sdp-interop 是一个可重用的 npm 模块,它提供了两种简单的方法

  • toUnifiedPlan(sdp),它接收一个 SDP 字符串并将其转换为统一计划 SDP。
  • toPlanB(sdp),它(不出所料)接收一个 SDP 字符串并将其转换为计划 B SDP。

PeerConnection 适配器包装了 setLocalDescription()setRemoteDescription() 方法以及 createAnswer()createOffer() 方法的成功回调。如果浏览器是 Chrome,则适配器不执行任何操作。另一方面,如果浏览器是 Firefox,则 PeerConnection 适配器执行以下操作

  • 在调用 setLocalDescription()setRemoteDescription() 方法之前,调用 sdp-interop 模块的 toUnifiedPlan() 方法,从而将应用程序的计划 B SDP 转换为 Firefox 可以理解的统一计划 SDP。
  • 在调用 createAnswer()createOffer() 成功回调之前,调用 toPlanB() 方法,从而将 Firefox 的统一计划 SDP 转换为应用程序可以理解的计划 B SDP。

这是一个基于 adapter.js 构建的示例 PeerConnection 适配器

function PeerConnectionAdapter(ice_config, constraints) {
    this.peerconnection = new RTCPeerConnection(ice_config, constraints);
    this.interop = new require('sdp-interop').Interop();
}

PeerConnectionAdapter.prototype.setLocalDescription
  = function (description, successCallback, failureCallback) {
    // if we're running on FF, transform to Unified Plan first.
    if (navigator.mozGetUserMedia)
        description = this.interop.toUnifiedPlan(description);

    this.peerconnection.setLocalDescription(description,
        function () { successCallback(); },
        function (err) { failureCallback(err); }
    );
};

PeerConnectionAdapter.prototype.setRemoteDescription
  = function (description, successCallback, failureCallback) {
    // if we're running on FF, transform to Unified Plan first.
    if (navigator.mozGetUserMedia)
        description = this.interop.toUnifiedPlan(description);

    this.peerconnection.setRemoteDescription(description,
        function () { successCallback(); },
        function (err) { failureCallback(err); }
    );
};

PeerConnectionAdapter.prototype.createAnswer
  = function (successCallback, failureCallback, constraints) {
    var self = this;
    this.peerconnection.createAnswer(
        function (answer) {
            if (navigator.mozGetUserMedia)
                answer = self.interop.toPlanB(answer);
            successCallback(answer);
        },
        function(err) {
            failureCallback(err);
        },
        constraints
    );
};

PeerConnectionAdapter.prototype.createOffer
  = function (successCallback, failureCallback, constraints) {
    var self = this;
    this.peerconnection.createOffer(
        function (offer) {
            if (navigator.mozGetUserMedia)
                offer = self.interop.toPlanB(offer);
            successCallback(offer);
        },
        function(err) {
            failureCallback(err);
        },
        constraints
    );
};

超越基础

像生活中的大多数事物一样,sdp-interop 并非“完美”,它做出了一些假设并存在一些限制。首先也是最重要的是,不幸的是,计划 B 的提议/应答没有足够的信息来重建等效的统一计划提议/应答。因此,虽然从统一计划到计划 B 很容易(有一些限制),但如果没有保留某些状态,则反过来是不可能的。

例如,假设 Firefox 客户端从 Focus 收到加入大型通话的提议。在 原生 create answer 成功回调中,您将获得一个包含多个 m 行的统一计划应答。您使用 sdp-interop 模块将其转换为计划 B 应答,并将其交给应用程序执行其操作。在稍后的某个时间点,应用程序将调用适配器的 setLocalDescription() 方法。适配器必须将计划 B 应答转换回统一计划应答,才能将其传递给 Firefox。

这是棘手的部分,因为您不能天真地将任何 SSRC 放入任何 m 行,每个 SSRC 应该放回原生 create answer 成功回调中的原始应答中的同一 m 行。m 行的顺序也很重要,因此每个 m 行必须与原生 create answer 成功回调中的原始应答中的位置相同(与统一计划提议中的 m 行位置匹配)。此外,禁止删除 m 行,如果不再使用,则必须将其标记为非活动状态。例如,在进行重新协商时,将计划 B 提议转换为统一计划提议时,也必须考虑类似的因素。

sdp-interop 通过缓存最新的统一计划提议和最新的统一计划应答来解决此问题。当从计划 B 转到统一计划时,sdp-interop 使用缓存的统一计划提议/应答,并从那里添加缺失的信息。您可以 在这里 确切地了解是如何完成的。

另一个限制是,在某些情况下,统一计划 SDP 无法映射到计划 B SDP。如果统一 SDP 有两个音频 m 行(例如)具有不同的媒体或传输属性,则在尝试将它们压缩到单个计划 B m 部分中时,无法协调这些属性。这就是为什么 sdp-interop 只能在传输属性相同(即使用捆绑和 rtcp-mux)并且给定媒体类型的每个 m 行的所有编解码器属性完全相同的情况下才能工作。幸运的是,Chrome 和 Firefox 默认都执行这两项操作。(这可能是 Chrome 实现统一计划并非易事的原因之一。)

最后一个软限制是,SDP 互操作性层仅在 Firefox 回答呼叫时进行了测试,而不是在 Firefox 发出呼叫时进行了测试,因为在 Jitsi 架构中,端点始终由 Focus 邀请加入呼叫,而从未发出呼叫。

远远超出基础

即使有了 SDP 互操作性层,也必须克服许多困难才能将 Firefox 支持引入 Jitsi Videobridge,并且 Mozilla 在解决所有这些困难方面提供了 巨大 的帮助。在大多数情况下,问题很容易修复,但需要时间和精力才能识别出来。为了参考(以及为了好玩!),我们将在这里简要介绍其中一些问题。

我们最初遇到的令人不快的意外之一是,有一天 Jitsi 原型实现突然停止工作了。在 Mozilla 在 Firefox 中启用 DTLS 1.2 后不久,DTLS 协商就开始失败,事实证明,Firefox 和我们基于 Bouncy Castle 的堆栈之间的 DTLS 版本协商存在问题。RFC 在记录层版本方面有点模棱两可,但我们假设 openssl 规则是标准,并 修补 了我们的堆栈以按照这些规则运行。

另一个小问题是 Firefox 缺少 msids,但 Mozilla 已经友好地解决了这个问题。

接下来,Jitsi 团队遇到了一个非常奇怪的问题,即 Firefox 端的远程视频播放会冻结或根本无法启动。解码器正在停顿。奇怪的是,在测试环境(局域网条件下),这个问题似乎只有在 SDP 中发出 goog-remb 信号时才会触发。经过一番挖掘,发现问题与 goog-remb 无关。真正的问题是 Jitsi Videobridge 正在向 Firefox 中继 RED,但后者 目前不支持 ulpfec/red,因此没有任何数据传输到解码器。发出 goog-remb 信号可能会告诉 Chrome 从流开始就将 VP8 封装到 RED 中,甚至在检测到数据包丢失之前。(由于添加任何冗余数据会带来开销,因此通常最好仅在网络状况需要时才激活它。)Jitsi Videobridge 现在在向 Firefox(或任何其他不支持 ULPFEC/RED 的客户端)流式传输时,会将 RED 解封装成纯 VP8。

Jitsi 团队还发现并修复了 Jitsi 代码库中的一些问题,包括我们堆栈中一个非零偏移量错误,可能位于 SRTP 变换器内部,导致 SRTP 身份验证失败。

最后,也许最重要的是,在典型的启用多流的会议中,Firefox 会创建两个(可能三个)sendrecv 通道(用于音频、视频,以及潜在的数据)和 N 个 recvonly 通道,其中一些用于传入音频,一些用于传入视频。这些 recvonly 通道将使用内部生成的 SSRC 发送 RTCP 反馈。麻烦就从这里开始了。

这些 recvonly 通道的内部生成的 SSRC 仅 Firefox 知道。客户端应用程序(因为它们未包含在 SDP 中)、Jitsi Videobridge 或其他端点(特别是 Chrome)都不知道。

使用捆绑时,Chrome 会丢弃来自 未声明 SSRC 的 RTCP 流量,因为它使用 SSRC 来确定 RTCP 数据包是否应发送到发送音频或发送视频通道。如果它找不到将 RTCP 数据包分派到的位置,则会将其丢弃。Firefox 不会受到影响,因为它以不同的方式处理此问题。执行过滤的 WebRTC 代码位于 bundlefilter.cc 中,该文件未包含在 mozilla-central 中。不幸的是,我们(Jitsi)在我们的网关中实现了相同的过滤/解复用逻辑。

这非常重要,因为来自 recvonly 通道的 PLI/RR/NACK 等,尽管它们可能到达 Chrome,但会被丢弃,因此典型结果是 Firefox 端的解码器停顿。Mozilla 在 Bug 1160280 中通过在 SDP 中公开 recvonly 通道的 SSRC 来修复了此问题。

结论

这是一段非常有趣的旅程,但我们快到了!Firefox Nightly(v41)和 Firefox 开发者版 40 已经具备了所有必要的组件,并且基于 Jitsi 的多对多会议可以使用多流正常工作。

Jitsi 需要解决的最后几件事之一是在 Firefox 中支持模拟广播。Jitsi 的模拟广播实现严重依赖于 MediaStream 构造函数,但它们 目前在 Firefox 中不可用。Jitsi 团队正在研究一种不需要 MediaStream 构造函数的替代方法。当 Jitsi 在 Firefox 上运行时,桌面共享是另一个重要的缺失项,但它目前也正在开发中。

换句话说,Firefox 和 Jitsi 即将成为最好的朋友!

关于 George Politis

更多 George Politis 的文章…

关于 Maire Reavy

Maire 是 Mozilla WebRTC 团队的工程经理。

更多 Maire Reavy 的文章…


14 条评论

  1. Adam Brault

    这是个好消息!Maire,非常感谢您在这个领域做出的巨大贡献。我们对 Firefox 即将推出的多流和 SFU 支持感到非常兴奋。:D

    2015 年 6 月 3 日 10:37

  2. Gustavo Garcia

    George 和 Maire 的文章非常有趣。非常感谢。我希望我们能够尽快使用 RtpSender/Receiver,并且不用担心 Plans :)

    2015 年 6 月 3 日 23:44

  3. Michael

    真是个好消息!期待 Firefox 40 发布。

    2015 年 6 月 4 日 03:14

  4. foss

    大家好,文章很棒,感谢所有为此付出努力的人。

    问题

    在 meet.jit.si 中无法静音麦克风(仅音频,摄像头已禁用)。我点击麦克风图标,但没有任何反应。

    屏幕共享将非常棒。

    2015 年 6 月 4 日 06:42

    1. George Politis

      您好 foss,感谢您的关注并告知您的体验!您描述的情况听起来像是 JItsi 端的错误。您能否在此处提交一个问题 https://github.com/jitsi/jitsi-meet/issues

      2015 年 6 月 4 日 08:30

  5. Matěj

    是否有某个网站可以尝试一下?

    2015 年 6 月 4 日 08:10

    1. George Politis

      是的,您可以在此处尝试 https://meet.jit.is/。请注意,您需要 FF >= 40!

      2015 年 6 月 4 日 08:25

      1. Matěj

        https://meet.jit.is/ 导致“服务器未找到”

        2015 年 6 月 4 日 08:43

        1. George Politis

          糟糕,抱歉,那里有个错字 :-) 正确的地址应该是 https://meet.jit.si/

          2015 年 6 月 4 日 09:02

  6. Oswaldo Saumet

    George,这是一个好消息,非常感谢您发布此重要公告。

    此致

    2015 年 6 月 8 日 00:38

  7. danzo

    感谢大家,工作很棒。

    我在使用 Firefox 的 Videobridge 时遇到了一些问题

    1. 我无法通过 REST 指定这些属性:,type=ccm/nack/goog-remb,subtype=fir/pli,分别在 name=VP8 中。也在父级中

    2. 在父级属性中被完全忽略。

    快速提问,如果 Chrome 正在发送而 Firefox 正在接收,SIM ssrc-group 是否向后兼容?

    迫不及待地期待完整的屏幕共享支持。我个人喜欢 Firefox 提出的白名单提案,类似于授权音频/视频设备的方式。

    2015 年 6 月 9 日 20:35

    1. George Politis

      您好 danzo,感谢您的关注!您指的是哪个 REST API?模拟广播目前与 FF 不兼容,但它是我们的优先事项,我们正在努力解决。

      2015 年 6 月 10 日 01:19

      1. danzo

        啊,没关系。我以为是 libjitsi 的底层问题,但我发现问题出在 videobridge api 上。现在可以用了。

        确保一下,如果其中一个连接的端点可能是 Firefox,我是否_不应_对任何客户端和 videobridge 使用模拟广播?

        2015 年 6 月 10 日 03:49

        1. George Politis

          没错,如果您希望获得 FF 支持,则_不应_使用模拟广播,对此表示歉意。我们的目标是在月底之前解决此问题。

          2015 年 6 月 10 日 04:18

本文的评论已关闭。