libwebrtc リップシンク

注意

この資料の正確性を保証しません。

重要

この資料についての問い合わせは Sora のサポート範囲には含まれません。

この資料は libwebrtc M99 (4844) 時点の資料です。

ここでは libwebrtc でのリップシンク処理(映像および音声ストリームのタイムスタンプ同期処理)について記載しています。

概要

  • 同期処理は、主に以下の三つのパートで構成される:

    1. 映像・音声ストリームから同期用の情報(主にパケットのタイムスタンプ)を取得部分

    2. 映像・音声ストリームを同期するために必要な遅延時間の計算部分

      • 片方が遅れている場合には、進んでいる方のストリームに対して遅延を課してタイミングを合わせるイメージ

    3. 上で求めた同期用の遅延時間のストリームへの反映部分

  • RtpStreamsSynchronizer がこのような同期処理のハブとなるクラス

    • 上述の 1 と 3 については、映像・音声ストリームと情報のやり取りが必要になるが、それは Syncable という抽象クラスを通して行われる

      • 映像では VideoReceiveStream2 が、音声では AudioReceiveStream が、 Syncable の実装クラスとなる

      • RtpStreamsSynchronizer はこれらのインスタンスをメンバ変数として保持しており、必要に応じてストリームの情報を取得したり、同期結果(必要な遅延)を伝えたりする

    • また RtpStreamsSynchronizerStreamSynchronization というクラスのインスタンスも保持している

      • こちらは実際のストリームとは切り離された、同期処理用の計算部分を担っているクラス(上述の 2 を担当)

    • RtpStreamsSynchronizerRtpStreamsSynchronizer::UpdateDelay() メソッドを定期的(一秒毎)に呼び出し、その中で上の 1 ~ 3 の処理を実施している

以降では、上の三つのパートのそれぞれについて記載していく。

詳細

同期用の情報取得

RtpStreamsSynchronizer クラスは Syncable::GetInfo() メソッドを経由して、同期に必要な各種情報を取得する。

Syncable::GetInfo() メソッドは、以下の定義の Syncable::Info 構造体を返す:

struct Info {
  // 最後に RTP パケットを受信した時のシステム時刻
  int64_t latest_receive_time_ms = 0;

  // 最後に受信した RTP パケットのタイムスタンプ
  uint32_t latest_received_capture_timestamp = 0;

  // RTCP Sender Report パケットから取得した情報
  uint32_t capture_time_ntp_secs = 0;  // NTP 時刻の秒部分
  uint32_t capture_time_ntp_frac = 0;  // NTP 時刻の秒未満部分
  uint32_t capture_time_source_clock = 0;  // RTP タイムスタンプ

  // ストリームの現在の遅延時間
  //
  // 正確ではないが、イメージとしては「`RtpStreamsSynchronizer`が前回に求めた遅延時間(を反映した値)」が近い
  int current_delay_ms = 0;
};

概要部分に記載の通り、映像と音声では Syncable の実装クラスは別々だが、どちらも似たような方法で RTP / RTCP パケット受信時の情報をほぼそのまま使って、この構造体に必要な情報を埋めている。

current_delay_ms だけは若干特殊だが、ここではこの値の求め方の詳細は割愛する。

同期に必要な遅延の計算

映像ストリームと音声ストリームのタイムスタンプの同期は「片方がもう片方に対して、どの程度遅延しているか」ということを求めることで行われる。

この遅延計算を担当しているのは RtpStreamsSynchronizer::UpdateDelay() メソッドで、流れは以下のようになっている:

  1. 前節に記載の Syncable::GetInfo() を使って、映像と音声のそれぞれで Syncable::Info 構造体を取得する

  2. Syncable::Info を使って、映像および音声用の StreamSynchronization::Measurements メンバ変数を更新する

  3. 更新された映像および音声用の StreamSynchronization::Measurements を引数にして StreamSynchronization::ComputeRelativeDelay() メソッドを呼び出す

    • このメソッドは「音声ストリームが映像ストリームに対して、どの程度遅れているか(or 進んでいるか)」を結果として返す

  4. 次に StreamSynchronization::ComputeDelays() を呼び出して「ストリームを同期するために映像と音声のそれぞれをどの程度遅延させるべきか」を計算する

  5. 最後は Syncable::SetMinimumPlayoutDelay() を用いて、計算した同期用の遅延値を映像と音声ストリームのそれぞれに反映する

    • このメソッドの詳細については、次の節を参照

ComputeRelativeDelay()ComputeDelays() の詳細

StreamSynchronization::ComputeRelativeDelay() のシグネチャは以下のようになっている:

bool StreamSynchronization::ComputeRelativeDelay(
    const Measurements& audio_measurement,  // 音声用の StreamSynchronization::Measurements
    const Measurements& video_measurement,  // 映像用の StreamSynchronization::Measurements
    int* relative_delay_ms) // 音声を基準にした、映像の遅延時間(音声の方が遅れている場合には負の値になる)

メソッド内で行われていることは単純で次の通り:

  • RtpToNtpEstimator::Estimate() メソッドを使って、最後に受信した RTP パケットのタイムスタンプを NTP ドメインに変換する(映像と音声のそれぞれ)

  • 次の式で relative_delay_ms の値を計算する:

    • (映像パケット受信システム時刻 - 音声パケット受信システム時刻) - (映像パケット NTP 時刻 - 音声パケット NTP 時刻)

StreamSynchronization::ComputeDelays() の方はもう少し複雑で、シグネチャは以下のようになっている:

bool StreamSynchronization::ComputeDelays(
    int relative_delay_ms,            // 上で求めた`relative_delay_ms`の値(i.e., 音声に対する映像の遅延時間)
    int current_audio_delay_ms,       // 音声の現在の遅延時間 (`Info.current_delay_ms`)
    int* total_audio_delay_target_ms, // 音声用の同期用遅延時間(初期値は 0)
    int* total_video_delay_target_ms) // 映像用の同期用遅延時間(初期値は映像の現在の遅延時間=`Info.current_delay_ms`)

この関数は、大まかには以下のようなことを行なっている:

  1. 映像の現在の遅延 - 音声の現在の遅延 + relative_delay_ms という式で映像と音声の遅延のズレを算出

    • この結果は、過去四回分が保持されており、以降ではその平均値が使用される

  2. 上の結果が 30ms 未満なら、許容範囲ということで、ここで処理は終了

    • 次節に記載の同期用遅延の反映処理もスキップされる

  3. 急激な遅延調整を防ぐために、上で求めた「ズレの平均値」の値を調整する

    • 二で割った後に、±80ms の範囲に収まるようにmin/max を取る

    • 結果は diff_ms という変数に格納する

  4. diff_ms が正の場合:

    • 映像の方が遅れている(遅延時間が長い)ので 映像の遅延を減らす音声の遅延を増やす 必要がある

    • 別途設定された「基準となるターゲット遅延時間」よりも映像の遅延時間が長い場合には、映像の遅延時間を diff_ms 分だけ減らす

      • かつ、音声の遅延時間を「基準となるターゲット遅延時間」にリセットする

      • 注釈:

    • 「基準となるターゲット遅延時間」よりも映像の遅延時間が短い場合には、音声の遅延時間を diff_ms 分だけ増やす

      • かつ、映像の遅延時間を「基準となるターゲット遅延時間」にリセットする

  5. diff_ms が負の場合:

    • 音声の方が遅れている(遅延時間が長い)ので 音声の遅延を減らす映像の遅延を増やす 必要がある

    • 映像と音声が逆転している以外は一つ前のステップで行なっていることと同じなので、詳細は割愛する

  6. ここまでで計算した映像の遅延時間が「基準となるターゲット遅延時間」を超えている場合には、その値を結果に採用する

    • そうではない場合には、前回の映像遅延時間をそのまま採用する

    • なお、遅延時間は 10 秒を超えないように min を取る

    • この値が total_video_delay_target_ms に格納されて呼び出し元に伝えられる

  7. 映像で前回の遅延時間が採用されなかった場合には、音声の方の遅延時間を更新する

    • 一度のメソッド呼び出しで更新されるの映像か音声のどちらか一つの遅延時間のみ

    • それ以外は映像の場合の処理と同様

    • この値が total_audio_delay_target_ms に格納されて呼び出し元に伝えられる

同期用遅延の反映

前節で計算された「映像と音声を同期させるために必要なそれぞれの遅延時間」は Syncable::SetMinimumPlayoutDelay() メソッドを通して、 それぞれのストリームに伝達され処理されることになる。

以降では、映像と音声のそれぞれについて、サブセクションに分けて概要を記載していく。

なお、メソッド名に含まれる "PlayoutDelay" という用語については、以下のドキュメントも参考になる:

映像の場合

  • 映像の Syncable 実装は VideoReceiveStream2 クラスで行われているので VideoReceiveStream2::SetMinimumPlayoutDelay() メソッドがエントリポイントとなる

    • このメソッドの内部では(細々とした調整処理を経て) VCMTiming::set_min_playout_delay() にターゲット遅延値が渡される

    • 映像の遅延関連処理はこの VCMTiming クラスに集約されている

      • 同期用の遅延以外にも jitter, render, composition, etc の遅延がこのクラスで管理されている

  • 上述の VCMTiming インスタンスへの参照は、 VideoReceiveStream2 以外にも複数箇所で共有されている

    • その内の一つが FrameBufferProxy であり、同期遅延の反映処理は、このクラスで行われている

    • FrameBufferProxy は設定によっていくつか別のクラスに処理を委譲することになる

  • VCMTiming に保持された同期用の遅延時間は、以下のメソッドの中で参照されている:

    • VCMTiming::TargetVideoDelay():「同期用遅延時間」と「jitter, decode, render 遅延の合計時間」を比較し、大きい方を返すメソッド

    • VCMTiming::RenderTime(): 「フレームのタイムスタンプ」を入力として受け取り、それを描画すべきタイムスタンプを返すメソッド

      • その際に「現在の遅延時間」を加味するが、その現在値が同期用の遅延時間の範囲内に収まるように min/max を取っている

    • VCMTiming::MaxWaitingTime(): 「デコーダに次のフレームを渡すまでの待機時間」を計算するメソッド

      • 同期用の遅延時間が 0 の場合(かつ、その他の条件が整った場合)には、特別な計算が実施される

    • この中の最初の二つのメソッドは FrameBuffer::GetNextFrame() メソッド、最後の一つは FrameBuffer::FindNextFrame() の中で使用されている

  • FrameBuffer::FindNextFrame() は「次にデコードするフレーム」と「デコードまでの待機時間」を決定するためのメソッド

    • この中で VCMTiming::RenderTime() を使ってフレームの描画時刻が決定される(まだ未定だった場合)

    • その次に VCMTiming::MaxWaitingTime() を呼び出して、そのフレームをデコーダに渡すまでの待機時間が決定される

      • もしその待機時間が負数(正確には -5ms 未満)だった場合には、そのフレームはドロップされる

  • FrameBuffer::GetNextFrame() は上で準備したフレームを、デコード時間になったら、実際に取得するメソッド

    • 対象フレームの描画時刻は FrameHasBadRenderTiming() 関数を使ってチェックされる

      • このメソッドに VCMTiming::TargetVideoDelay() の値も渡される

      • ざっくり言えば、タイムスタンプの値が負数だったり、遅延時間が 10 秒を超えている場合に "Bad" と判定される

      • その場合には VCMTiming::reset() を呼び出して状態をリセットした上で、再度 VCMTiming::RenderTime() を使って描画時刻が計算される

音声の場合

© Copyright 2022, Shiguredo Inc Created using Sphinx 5.0.0b1