GStreamer でのクロックと同期の仕組み
公開日:2023年1月5日
GStreamer はオーディオやビデオや字幕などを同期して表示することができます。この記事では、同期の仕組みに関連する GStreamer の挙動を説明します。
クロックの説明に入る前に GStreamer の Pipeline 内をメディアデータがどのように移動するかを説明します。メディアデータはバッファ (GstBuffer というオブジェクト) に格納されて Element 間を移動します。また End of Stream (EOS) やシークは、イベント (GstEvent というオブジェクト) を利用して Element 間で伝えられます。アプリケーションの観点では、シークの際に Pipeline へのイベント通知という形でイベントが利用されます。一方でバッファはアプリケーションから直接的に利用されることはあまりありません。
三種類の時刻情報
GStreamer は三種類の時刻情報、クロック running-time と バッファ running-time と バッファ stream-time を利用します。
クロックおよびバッファ running-time は同期のために利用される時刻情報です。クロック running-time はさらに二つの時刻情報 clock-time と base-time を元に running-time = clock-time - base-time のように算出されます。ここで clock-time は単調増加するクロックで、 base-time は Pipeline が再生開始やシーク後の再生再開など GST_STATE_PLAYING に遷移した際の clock-time のスナップショットです。クロック running-time は Pipeline の状態が GST_STATE_PLAYING の間は進行し、状態が GST_STATE_PAUSED になると進行を止めます。各 Element は Pipeline 内で共通のクロックを参照し、同期が必要な Element (通常は sink) は、クロックがバッファ running-time を通過したときにそのバッファを処理します。このようにして、オーディオやビデオや字幕の sink Element は Pipeline 内で共通のクロックを参照して同期を実施します。
バッファ stream-time はコンテンツ中の現在位置を示します。アプリケーションはこの情報に基づいて、プログレスバーの再生位置の更新などを実施します。バッファ stream-time は同期に用いられることはありません。
なぜこのような設計になっているかというと、 running-time と stream-time がどのような状況でも一致するとは限らないためです。両者が異なる状況を理解するため、例えば
- 再生を開始する
- 10 秒間再生する
- コンテンツの先頭から 20 秒の位置にシークする
- 再生を再開する
という操作を行った場合の、各時刻情報の変化を考えます。
シーク時に GST_SEEK_FLAG_FLUSH を指定した場合は、シーク時に、その時点の clock-time を用いて base-time が再設定されます。つまり、各時刻情報は次のように変化します。
操作 | stream-time | running-time | clock-time | base-time |
---|---|---|---|---|
再生開始 | 00:00 | 00:00 | 00:00 | 00:00 |
再生中 (5 秒経過) | 00:05 | 00:05 | 00:05 | 00:00 |
再生中 (10 秒経過) | 00:10 | 00:10 | 00:10 | 00:00 |
20 秒位置へシーク | 00:20 | 00:00 | 00:10 | 00:10 |
再生中 (10 秒経過) | 00:30 | 00:10 | 00:20 | 00:10 |
一方で、シーク時に GST_SEEK_FLAG_FLUSH を指定しなかった場合は、シーク時に base-time が再設定されず、再生開始時の値が維持されます。つまり、各時刻情報は次のように変化します。
操作 | stream-time | running-time | clock-time | base-time |
---|---|---|---|---|
再生開始 | 00:00 | 00:00 | 00:00 | 00:00 |
再生中 (5 秒経過) | 00:05 | 00:05 | 00:05 | 00:00 |
再生中 (10 秒経過) | 00:10 | 00:10 | 00:10 | 00:00 |
20 秒位置へシーク | 00:20 | 00:10 | 00:10 | 00:00 |
再生中 (10 秒経過) | 00:30 | 00:20 | 00:20 | 00:00 |
アプリケーションから現在の再生位置 (stream-time) を取得するには gst_element_query_positionを利用し、コンテンツの全体長を取得するには gst_element_query_durationを利用します。第一引数の element には Pipeline を与えます。
アプリケーションから running-time, clock-time, base-time を取得するには、それぞれ gst_element_get_current_running_time, gst_element_get_current_clock_time,gst_element_get_base_timeを利用します。こちらも第一引数の element には Pipeline を与えます。
アプリケーションからシークを行うには gst_element_seekを利用します。こちらも第一引数の element には Pipeline を与えます。
static gboolean
cb_print_position (GstElement *pipeline)
{
gint64 pos, len;
if (gst_element_query_position (pipeline, GST_FORMAT_TIME, &pos)
&& gst_element_query_duration (pipeline, GST_FORMAT_TIME, &len)) {
GstClockTime clock_time = gst_element_get_current_clock_time (pipeline);
GstClockTime base_time = gst_element_get_base_time (pipeline);
GstClockTime running_time = gst_element_get_current_running_time (pipeline);
g_print ("Time: %" GST_TIME_FORMAT " / %" GST_TIME_FORMAT ", "
"clock_time=%" GST_TIME_FORMAT ", base_time=%" GST_TIME_FORMAT
", running_time=%" GST_TIME_FORMAT ", clock_time-base_time=%" GST_TIME_FORMAT "\r",
GST_TIME_ARGS (pos), GST_TIME_ARGS (len),
GST_TIME_ARGS (clock_time), GST_TIME_ARGS (base_time),
GST_TIME_ARGS (running_time), GST_TIME_ARGS (clock_time - base_time)
);
}
/* call me again */
return TRUE;
}
のようなコールバック関数を用意して g_main_loop_run の呼び出し前に
g_timeout_add (200, (GSourceFunc) cb_print_position, pipeline);
のように設定すると、第一引数で指定した間隔 (この例なら 200 ミリ秒) で、第二引数で与えたコールバック関数 (cb_print_position) が繰り返し呼び出されます。
クロック供給
GStreamer は Pipeline が同期に利用するクロックとして、 Pipeline 内の Element から提供を受けることができます。 Pipeline 内でのクロックの検索は、下流の sink から上流の source へと遡って行われ、最後に提供されたクロックが採用されます。
コンテンツ再生の Pipeline では audio sink のクロックを利用するのが一般的です。 audio sink のクロックを採用することの利点について考えるため、仮に system clock を Pipeline のクロックとして採用したとします。一般に、水晶発振器の精度にはばらつきがあるため system clock と audio sink のクロックは、いつも正確に一致するわけではありません。 system clock の方が速かった場合は audio sink でのオーディオサンプルの消費速度よりも、 audio sink に投入されるオーディオサンプルの供給速度が速くなるため、再生待ちのオーディオサンプルが積み上がり、いずれはバッファが溢れます。逆に system clock の方が遅かった場合は、 audio sink でオーディオサンプルが枯渇し、アンダーランが発生します。このような問題を audio sink で解決するために苦労するよりは、 audio sink がクロックを供給して Pipeline 全体がこのクロックに従ったほうがよいと考えられます。
カメラなどのキャプチャデバイスがソースになる Pipeline では状況が異なり、 source のクロックを利用するのが一般的です。キャプチャデバイスからのストリームの供給速度は、自由に調整できないため、 Pipeline 全体のクロックとキャプチャデバイスのクロックに齟齬が生じると、ストリームの供給速度と消費速度が一致せず、オーバーフローやアンダーランが生じます。
PLAYING に移行した際にクロックが選択されると GST_MESSAGE_NEW_CLOCK が Bus に通知されます。選択されたクロックを調べるには、下記のような処理を Bus のコールバック関数に追加します。
case GST_MESSAGE_NEW_CLOCK:
GstClock *clock = NULL;
gst_message_parse_new_clock (msg, &clock);
if (clock != NULL) {
g_print ("New clock: %s\n", GST_OBJECT_NAME (clock));
}
else {
g_print ("New clock: NULL\n");
}
break;
オーディオファイル再生アプリケーションで試すと GstPulseSinkClock という名前が得られました。
まとめ
この記事では、
- Pipeline 内で共通のクロックに従い、通常は sink で提示処理の同期を行う
- 各バッファには処理すべき時刻情報が含まれる
- Pipeline のクロックは Pipeline 内の Element から提供される
といった内容を紹介しました。下図も参照ください。
実際には、バッファには pts (Presentation Time Stamp) が与えられており、この値から、セグメント (この記事では紹介しませんでした) の情報を加味して、バッファ running-time に変換したのちに、クロック running-time と比較して同期します。
※文中に記載されている各種名称、会社名、商品名などは各社の商標もしくは登録商標です。