libwebrtc リップシンク¶
この資料は libwebrtc M99 (4844) 時点の資料です。
ここでは libwebrtc でのリップシンク処理(映像および音声ストリームのタイムスタンプ同期処理)について記載しています。
概要¶
- 同期処理は、主に以下の 3 つのパートで構成される:
- 映像・音声ストリームから同期用の情報(主にパケットのタイムスタンプ)を取得部分
- 映像・音声ストリームを同期するために必要な遅延時間の計算部分
- 片方が遅れている場合には、進んでいる方のストリームに対して遅延を課してタイミングを合わせるイメージ
- 上で求めた同期用の遅延時間のストリームへの反映部分
- RtpStreamsSynchronizer がこのような同期処理のハブとなるクラス
- 上述の 1 と 3 については、映像・音声ストリームと情報のやり取りが必要になるが、それは Syncable という抽象クラスを通して行われる
- 映像では VideoReceiveStream2 が、音声では AudioReceiveStream が、 Syncable の実装クラスとなる
- RtpStreamsSynchronizer はこれらのインスタンスをメンバ変数として保持しており、必要に応じてストリームの情報を取得したり、同期結果(必要な遅延)を伝えたりする
- また RtpStreamsSynchronizer は StreamSynchronization というクラスのインスタンスも保持している
- こちらは実際のストリームとは切り離された、同期処理用の計算部分を担っているクラス(上述の 2 を担当)
- RtpStreamsSynchronizer は RtpStreamsSynchronizer::UpdateDelay() メソッドを定期的(一秒毎)に呼び出し、その中で上の 1..3 の処理を実施している
- 上述の 1 と 3 については、映像・音声ストリームと情報のやり取りが必要になるが、それは Syncable という抽象クラスを通して行われる
以降では、上の三つのパートのそれぞれについて記載していく。
詳細¶
同期用の情報取得¶
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() メソッドで、流れは以下のようになっている:
- 前節に記載の Syncable::GetInfo() を使って、映像と音声のそれぞれで Syncable::Info 構造体を取得する
- Syncable::Info を使って、映像および音声用の StreamSynchronization::Measurements メンバ変数を更新する
- StreamSynchronization::Measurements は「RTP タイムスタンプの NTP ドメインへの変換」を行うためのクラス
- このクラスは RtpToNtpEstimator というクラスのインスタンスを保持している
- RtpToNtpEstimator::UpdateMeasurements() は Syncable::Info の RTCP Sender Report (SR) 関連の情報を受け取り、それらを 20 個分保持している
- RtpToNtpEstimator::Estimate() では、その履歴情報を使って(線形回帰で)求めたパラメーターを用いて、 RTP タイムスタンプから NTP タイムスタンプへの変換を行う
- 更新された映像および音声用の StreamSynchronization::Measurements を引数にして StreamSynchronization::ComputeRelativeDelay() メソッドを呼び出す
- このメソッドは「音声ストリームが映像ストリームに対して、どの程度遅れているか(or 進んでいるか)」を結果として返す
- 次に StreamSynchronization::ComputeDelays() を呼び出して「ストリームを同期するために映像と音声のそれぞれをどの程度遅延させるべきか」を計算する
- 最後は 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`)
この関数は、大まかには以下のようなことを行なっている:
映像の現在の遅延 - 音声の現在の遅延 + relative_delay_msという式で映像と音声の遅延のズレを算出- この結果は、過去四回分が保持されており、以降ではその平均値が使用される
- 上の結果が 30 ms 未満なら、許容範囲ということで、ここで処理は終了
- 次節に記載の同期用遅延の反映処理もスキップされる
- 急激な遅延調整を防ぐために、上で求めた「ズレの平均値」の値を調整する
- 二で割った後に、±80 ms の範囲に収まるようにmin/max を取る
- 結果は
diff_msという変数に格納する
diff_msが正の場合:- 映像の方が遅れている(遅延時間が長い)ので 映像の遅延を減らす か 音声の遅延を増やす 必要がある
- 別途設定された「基準となるターゲット遅延時間」よりも映像の遅延時間が長い場合には、映像の遅延時間を
diff_ms分だけ減らす- かつ、音声の遅延時間を「基準となるターゲット遅延時間」にリセットする
- 注釈:
- 「基準となるターゲット遅延時間」は StreamSynchronization::SetTargetBufferingDelay() メソッドによって設定される
- ただし通常はデフォルト値である 0 が使用されるように見える
- 「基準となるターゲット遅延時間」よりも映像の遅延時間が短い場合には、音声の遅延時間を
diff_ms分だけ増やす- かつ、映像の遅延時間を「基準となるターゲット遅延時間」にリセットする
diff_msが負の場合:- 音声の方が遅れている(遅延時間が長い)ので 音声の遅延を減らす か 映像の遅延を増やす 必要がある
- 映像と音声が逆転している以外はひとつ前のステップで行なっていることと同じなので、詳細は割愛する
- ここまでで計算した映像の遅延時間が「基準となるターゲット遅延時間」を超えている場合には、その値を結果に採用する
- そうではない場合には、前回の映像遅延時間をそのまま採用する
- なお、遅延時間は 10 秒を超えないように min を取る
- この値が
total_video_delay_target_msに格納されて呼び出し元に伝えられる
- 映像で前回の遅延時間が採用されなかった場合には、音声の方の遅延時間を更新する
- 一度のメソッド呼び出しで更新されるの映像か音声のどちらか一つの遅延時間のみ
- それ以外は映像の場合の処理と同様
- この値が
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 は設定によっていくつか別のクラスに処理を委譲することになる
- 例えば FrameBuffer2Proxy が使われる場合には、 VCMTiming への参照は、さらに FrameBuffer クラスに渡されて、処理されることになる
- 以降では、この FrameBuffer での挙動を見ていく
- 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() を使って描画時刻が計算される
- 対象フレームの描画時刻は FrameHasBadRenderTiming() 関数を使ってチェックされる
音声の場合¶
- 音声の Syncable 実装は AudioReceiveStream クラスで行われているので AudioReceiveStream::SetMinimumPlayoutDelay() メソッドがエントリポイントとなる
- その後、以下のメソッド群の呼び出し通じて値が(ほぼそのまま)伝播する:
- ChannelReceiveInterface::SetMinimumPlayoutDelay()
- AcmReceiver::SetMinimumDelay()
- NetEq::SetMinimumDelay()
- NetEqController::SetMinimumDelay()
- DelayManager::SetMinimumDelay()
- その後、以下のメソッド群の呼び出し通じて値が(ほぼそのまま)伝播する:
- DelayManager::SetMinimumDelay() に渡された遅延時間は DelayManager::Update() メソッドの中で参照される
- これは「RTP パケットのタイムスタンプ」を入力に受け取り、内部の統計情報等を更新した上で、パケットの遅延時間を返すメソッド
- このメソッド呼び出し時には、他のクラスから参照される「ターゲット遅延時間」の値も更新される
- このターゲット遅延時間の値は DelayManager::TargetDelayMs() 経由で取得できる
- DelayManager::TargetDelayMs() の返り値と DelayManager::SetMinimumDelay() に渡された値は、必ずしも一致する訳ではない
- ただし、大枠を理解する上では「おおむね同じもの」と考えてしまってもそこまで問題はない
- DelayManager::TargetDelayMs() は DecisionLogic クラスの色々な箇所で呼び出されている
- その中でも DecisionLogic::GetDecision() がおそらく一番重要
- DecisionLogic::GetDecision() は最後に受け取ったパケットやジッタバッファの情報を使って、次に行うべき操作を指示する NetEq::Operation 列挙型を返す
- 操作決定の際には DelayManager::TargetDelayMs() の値も考慮し、それに応じて
Operation::kAccelerate(再生時間を早める) やOperation::kExpand(再生時間を遅らせる)を指示してペース調整が行われる - なお NetEq クラスについては以下のリンクも参考になる: