WebRTC-之音视频同步

在网络视频会议中, 我们常会遇到音视频不同步的问题, 我们有一个专有名词 lip-sync 唇同步来描述这类问题,当我们看到人的嘴唇动作与听到的声音对不上的时候,不同步的问题就出现了

而在线会议中, 听见清晰的声音是优先级最高的, 人耳对于声音的延迟是很敏感的

根据 T-REC-G.114-200305 中的描述

  • 大于~280ms 有些用户就会不满意
  • 大于~380ms 多数用户就会不满意
  • 大于~500ms 几乎所有用户就会不满意

我们就尽量使得声音的延迟在 280 ms 之内,这是解决 lip-sync 问题的前提, 声音不好的严重程序超过音视频不同步。

我们可以定义一个 sync_diff 值 来表示音频帧和视频帧之间的时间差

  • 正值表示音频领先于视频
  • 负值表示音频落后于视频

ITU 对此给出以下的阈值:

  • 不可感知 Undetectability (-100ms, +25ms)
  • 可感知 Detectability: (-125ms, +45ms)
  • 可接受 Acceptability: (--185ms, +90 ms)
  • 影响用户 Impact user experience (-∞, -185ms) ∪ (+90ms,∞)

(ITU-R BT.1359-1, Relative Timing of Sound and Vision for Broadcasting" 1998. Retrieved 30 May 2015)

当我们在播放一个视频帧及对应的音频帧的时候,要计算一下这个 sync_diff

ini 复制代码
sync_diff = audio_frame_time - video_frame_time

如果这个 sync_diff 大于 90ms, 也就是音频包到得过早,就会有音视频不同步的问题 - 声音听到了,嘴巴没跟上.

如果这个 sync_diff 小于 -185ms, 也就是视频包到得过早,就会有音视频不同步的问题 - 嘴巴在动,声音没跟上.

不同步的原因

这个问题的原因主要在于音频的采集, 编码,传输, 解码, 播放与视频的采集,编码,传输,解码以及渲染一般是分开进行的,因为音频和视频采集自不同的设备,即它们的来源不同,在网络上传输也会有延迟,也由不同的设备进行播放,这样如果在接收方不采取措施进行时间同步,就会极有可能看到口型和听到的声音对不上的情况。

由此派生出 3 个小问题:

  1. 如何将来自同一个人或设备的多路 audio 及 video stream关联起来?
  2. 如何将 RTP 中的时间戳 timestamp 映射到发送方的音视频采集时间
  3. 如何调整音频或者视频帧的播放时间,让它们怎么之间相对同步?

解决方案

1. 如何将来自同一个人或设备的音视频流关联起来?

对于多媒体会话,每种类型的媒体(例如音频或视频)一般会在单独的 RTP 会话中发送,发送方会在 RTCP SDES 消息中指明 接收方通过 CNAME 项关联要同步的RTP流, 而这个 CNAME 包含在发送方所发送的 RTCP SDES 中

SDES 数据包包含常规包头,有效负载类型为 202,项目计数等于数据包中 SSRC/CSRC 块的数量,后跟零个或多个 SSRC/CSRC 块,其中包含有关特定 SSRC 或 CSRC,每个都与 32 位边界对齐。

ini 复制代码
    0               1               2               3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |V=2|P|    SC   |  PT=SDES=202  |            length L           |
    +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
    |                          SSRC/CSRC_1                          |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                           SDES items                          |
    |                              ...                              |
    +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
    |                          SSRC/CSRC_2                          |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                           SDES items                          |
    |                              ...                              |
    +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+

CNAME 项在每个 SDES 数据包中都是必需的,而 SDES 数据包又是每个复合 RTCP 数据包中的必需部分。

与 SSRC 标识符一样,CNAME 必须与其他会话参与者的 CNAME 不同。 但 CNAME 不应随机选择 CNAME 标识符,而应允许个人或程序通过 CNAME 内容来定位其来源。

ini 复制代码
    0               1               2               3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |    CNAME=1    |     length    | user and domain name         ...
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

例如 Alice 向外发送一路音频流,一路视频流, 这两路流会使用不同的 SSRC, 但是在其所发送的 RTCP SDES 消息会使用相同的 CNAME.

  • RTP SSRC 1 ~ CNAME 1
  • RTP SSRC 2 ~ CNAME 1

2. 同步的时间如何计算

来自同一个终端用户的音频和视频, 在编码发送的 RTP 包中有一个 timestamp, 这个时间戳表示媒体流的捕捉时间。 同时, 作为发送者也会发送 RTCP Sender Report, 其中包含发送的 RTP timestamp 和 NTP timestamp 的映射关系,这样我们在接收方就可以把 RTP 包里的

对于每个 RTP 流,发送方定期发出 RTCP SR, 其中包含一对时间戳:

NTP 时间戳以及与该 RTP 流关联的相应 RTP 时间戳。

这对时间戳传达每个媒体流的 NTP 时间和 RTP 时间之间的关系。

先回顾一下 RTP packet 和 RTCP sender report

  • RTP 包结构
bash 复制代码
    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |V=2|P|X|  CC   |M|     PT      |       sequence number         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                           timestamp                           |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           synchronization source (SSRC) identifier            |
   +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
   |            contributing source (CSRC) identifiers             |
   |                             ....                              |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • RTCP Sender Report 结构
python 复制代码
         0                   1                   2                   3
         0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   header |V=2|P|    RC   |   PT=SR=200   |             length            |
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         |                         SSRC of sender                        |
         +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
   sender |              NTP timestamp, most significant word             |
   info   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         |             NTP timestamp, least significant word             |
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         |                         RTP timestamp                         |
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         |                     sender's packet count                     |
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         |                      sender's octet count                     |
         +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
   report |                 SSRC_1 (SSRC of first source)                 |
   block  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   1    | fraction lost |       cumulative number of packets lost       |
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         |           extended highest sequence number received           |
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         |                      interarrival jitter                      |
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         |                         last SR (LSR)                         |
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         |                   delay since last SR (DLSR)                  |
         +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
   report |                 SSRC_2 (SSRC of second source)                |
   block  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   2    :                               ...                             :
         +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
         |                  profile-specific extensions                  |
         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

通过 NTP timestamp 和 RTP timestamp 之间的映射, 我们可以知道 audio 包的时间和 video 包的时间。

具体的计算可以参见 WebRTC 的 RtpToNtpEstimator 类, 它将收到的若干 SR 中的 NTP time 和 RTP timestamp 保存下来,然后 应用最小二乘法来估算后续 RTP timestamp 所对应的 NTP timestamp, 大致为用最近 N=20 个 RTCP SR 包的 ntp timestamp 和 rtp timestamp 的构造出线性关系 y = ax + b, 通过最小二乘法来计算收到的 RTP 包对应的 ntp timestamp.

arduino 复制代码
// Converts an RTP timestamp to the NTP domain.
// The class needs to be trained with (at least 2) RTP/NTP timestamp pairs from
// RTCP sender reports before the convertion can be done.
class RtpToNtpEstimator {
      public:
            //...

            enum UpdateResult { kInvalidMeasurement, kSameMeasurement, kNewMeasurement };
            // Updates measurements with RTP/NTP timestamp pair from a RTCP sender report.
            UpdateResult UpdateMeasurements(NtpTime ntp, uint32_t rtp_timestamp);

            // Converts an RTP timestamp to the NTP domain.
            // Returns invalid NtpTime (i.e. NtpTime(0)) on failure.
            NtpTime Estimate(uint32_t rtp_timestamp) const;

            // Returns estimated rtp_timestamp frequency, or 0 on failure.
            double EstimatedFrequencyKhz() const;

      private:
            // Estimated parameters from RTP and NTP timestamp pairs in `measurements_`.
            // Defines linear estimation: NtpTime (in units of 1s/2^32) =
            //   `Parameters::slope` * rtp_timestamp + `Parameters::offset`.
            struct Parameters {
                  double slope;
                  double offset;
            };

            // RTP and NTP timestamp pair from a RTCP SR report.
            struct RtcpMeasurement {
                  NtpTime ntp_time;
                  int64_t unwrapped_rtp_timestamp;
            };

            void UpdateParameters();

            int consecutive_invalid_samples_ = 0;
            std::list<RtcpMeasurement> measurements_;
            absl::optional<Parameters> params_;
            mutable RtpTimestampUnwrapper unwrapper_;
};

3. 调整播放和渲染时间

一般我们会以 audio 为主, video 向 audio 靠拢, 两者时间一致也就会达到 lip sync 音视频同步

  1. audio 包先来, video 包后来: audio 包放在 jitter buffer 时等一会儿, 但是这个时间是有限的, 音频的流畅是首先要保证的, 视频跟不上可以降低视频的码率
  2. video 包先来, audio 包后来: video 包要等 audio 包来, 这是为了让音视频同步要付出的代价

一般以音频为主流 master stream,视频为从流 slave stream。 一般方法是接收方维护音频流的缓冲区的管理,并通过将视频 RTP 时间戳转换为正确从属于音频流的时间戳来调整视频流的播放。

当带有RTP时间戳 RTPv的视频帧到达接收器时,接收器通过四个步骤将RTP时间戳 RTPv 映射到视频设备时间戳VTB( Video Time Base),如图所示。

  1. 使用 Video RTCP SR 中的 RTP/NTP 时间戳对建立的映射,将视频 RTP 时间戳 RTPv 映射到发送方 NTP 时间。

  2. 根据该 NTP 时间戳,使用 Audio RTCP SR 中的 RTP/NTP 时间戳对建立的映射,计算来自发送方的相应音频 RTPa 时间戳。 此时,视频RTP时间戳被映射到音频RTP 包的相同时间基准。

  3. 根据该音频 RTP 时间戳,使用卡尔曼滤波的方法计算音频设备时间基准中的相应时间戳。 结果是音频设备时间基准 ATB(Audio Time Base) 中的时间戳。

  4. 根据 ATB,使用偏移量 AtoV 计算视频设备时基 VTB 中的相应时间戳。

接收方需要确保带有 RTP 时间戳 RTPv 的视频帧使用所计算出的发送方视频设备时间基准 VTB 播放。

ini 复制代码
      AtoV = V_time - A_Time/(audio sample rate)

注:

  • AtoV: 音频相较视频的偏移量
  • ATB: Audio device Time Base 音频设备的时间基准
  • VTB: Video device Time Base 视频设备的时间基准

具体方法可以参见 www.ccexpert.us/video-confe...)

WebRTC 的做法原理上差不多,实现略有不同,可以参见 WebRTC 的源代码 StreamSynchronization 类和 RtpStreamsSynchronizer 类

大致上它会计算出 video 的延迟

ini 复制代码
current_delay_ms = max(min_playout_delay_ms, jitter_delay_ms + decode_time _ms + render_delay_ms)

然后再计算视频相对于音频的延迟 relative_delay_ms,

  • 如果它大于0, 视频比音频慢,减小视频延迟(主要是调整 jitter buffer delay),或者是增大音频延迟, 取决于阈值 base_target_delay_ms
  • 如果它小于0, 音频比视频慢,减小音频延迟,或者是增大视频延迟, 取决于阈值base_target_delay_ms

base_target_delay_ms 的比较逻辑参见StreamSynchronization::ComputeDelays,

arduino 复制代码
if (diff_ms > 0) {
      // The minimum video delay is longer than the current audio delay.
      // We need to decrease extra video delay, or add extra audio delay.
      if (video_delay_.extra_ms > base_target_delay_ms_) {
            // We have extra delay added to ViE. Reduce this delay before adding
            // extra delay to VoE.
            video_delay_.extra_ms -= diff_ms;
            audio_delay_.extra_ms = base_target_delay_ms_;
      } else {  // video_delay_.extra_ms > 0
            // We have no extra video delay to remove, increase the audio delay.
            audio_delay_.extra_ms += diff_ms;
            video_delay_.extra_ms = base_target_delay_ms_;
      }
      } else {  // if (diff_ms > 0)
      // The video delay is lower than the current audio delay.
      // We need to decrease extra audio delay, or add extra video delay.
      if (audio_delay_.extra_ms > base_target_delay_ms_) {
            // We have extra delay in VoiceEngine.
            // Start with decreasing the voice delay.
            // Note: diff_ms is negative; add the negative difference.
            audio_delay_.extra_ms += diff_ms;
            video_delay_.extra_ms = base_target_delay_ms_;
      } else {  // audio_delay_.extra_ms > base_target_delay_ms_
            // We have no extra delay in VoiceEngine, increase the video delay.
            // Note: diff_ms is negative; subtract the negative difference.
            video_delay_.extra_ms -= diff_ms;  // X - (-Y) = X + Y.
            audio_delay_.extra_ms = base_target_delay_ms_;
      }
}

更多细节在 WebRTC 的代码中

  • class StreamSynchronization
  • class RtpStreamsSynchronizer

通过StreamSynchronization::ComputeDelays计算出音频和视频的相对延迟,如果相对延迟很小( < 30ms), 则无需调整音视频的播放时间,如果相对延迟很大, 则以 80ms 的幅度进行逐步调整。 与传统的只调视频延迟,不调音频延迟, WebRTC 会两边都调点,使得音视频的时间彼此靠近,前提是音频的延迟是在上面提到的可接受范围之内。

参考资料

相关推荐
ggtc20 小时前
WebRTC入门
webrtc·netcore
亿只王菜菜3 天前
WebRtc实现1V1音视频通话
spring boot·websocket·webrtc·实时音视频
音视频牛哥8 天前
Linux平台下RTSP|RTMP播放器如何跟python交互投递RGB数据供视觉算法分析
音视频开发·视频编码·直播
拖孩10 天前
💥我在 Chatterbox(话匣子)中 WebRTC 的使用-上篇(基本介绍)
前端·javascript·webrtc
树獭非懒10 天前
Android车载开发启示录(四)
android·架构·音视频开发
Fairy_sevenseven10 天前
【九】【QT开发应用】WebRTC的sigslot源码和使用WebRTC的sigslot使用编写信号槽
开发语言·qt·webrtc
mortimer10 天前
语言无界:视频翻译技术原理与流程探索
开源·openai·音视频开发
2401_8581205311 天前
微软Edge浏览器与WebRTC:实现下一代网络通信
前端·edge·webrtc
metaRTC14 天前
metaRTC8.0,一个全新架构的webRTC SDK库
音视频·webrtc
DogDaoDao16 天前
openh264 宏块级码率控制源码分析
音视频·webrtc·视频编解码·h264·openh264·码率控制