LoginSignup
27
16

More than 1 year has passed since last update.

RustでもGStreamer理解がしたい! その0 〜まずはGStreamerパイプラインを書こう 〜

Last updated at Posted at 2022-08-28

きっかけは、"カメラ配信でも遅延を1秒未満にしたい!"

さて、前回はWebTransportというものを使い、サーバーからMP4の動画を0.5秒程度の遅延で配信するというのをやりました。
これはこれで所謂オンデマンドストリーミング配信なので良いのですが、
実際にはカメラなどでライブ配信し、その遅延も1秒未満にしたい、というところにありました。

構成図を書くとこんな感じですが、遅延は5秒でした。これでは超低遅延とは申せません。

ffmpegではパラメータ調整に限界がある

ffmpegはとても良くできていて公式マニュアルにこれでもかと情報が載っていますし、
ニコラボさんをはじめ、日本語の情報もたくさんあります。ググれば大体何とかなるのではないでしょうか?

さて今回調べていたところ、打ち上げ時に -f mpegts とすると5秒だったのに対し -f flv とすると3秒程度になることがわかりました。
とはいえ SRTで配信するのでOBS Studioなどでは mpegts で配信されることになります。
そのためどうもmuxするところのバッファを短くすれば良さそうです。早速オプションを調べてみますが。。なさそうです。

あとはソースコードを読んでいくしかないのですが、それだったら最初からプログラムすることを前提として開発されているGStreamerを使った方が良いのでは思いました。

そこでGStreamerの出番

前置きが少し長くなりましたが、GStreamerとはプログラムすることを前提としたメディアフレームワークです。
ffmpegもPythonでOpenCVと組み合わせて使えたりしますが、主な用途はコマンドラインツールのようにたくさんのオプションをつけて実行する使われ方が多いと思います。
対してGStreamerは予め決められた規則に従ってプラグインを組み合わせ、一つのパイプラインとしてプログラミングさせるように作れているようです。

名前にストリームと入っている通り、Unixのコマンドラインのようにパイプでデータを渡して処理する設計思想です。
GoogleのMeetで仮想背景を処理するのに使われているMediaPipeや、メディアサーバーのKurentoなど、影響を受けたものも多いようです。

これを使えばバッファや遅延の調整はできそうですし、各要素がプラグインとなっているので必要な箇所だけ修正するか作るかすれば良さそうです。

日本語の情報が少なく、学習コストが高い

公式マニュアルには色々と概念からチュートリアルまで一通り書いてあるのですが、イマイチよくわかりません。
チュートリアルをやって、マニュアルを見て、またチュートリアルをやって、と何度か繰り返しているとわかってくるのかもしれません。

ffmpegはたくさん情報があるのにGStreamerになると途端に情報が少なくなります。

日本語の情報

私が参考にしたサイトはこの辺りでしょうか

特に上記の方のGStreamerでテキストを処理する話については仕組みが理解できそうなのでRustでもやってみたいと思います。

海外の情報

公式マニュアルとチュートリアルはもちろん、GStreamer + Rust でメディアサーバーを作るというドンピシャなライブコーディングがありました。

(↓RTMP Switcherを作る話ですが全部で数十時間くらいあります😱)

環境構築

環境構築についてはとても簡単で、様々なディストリービューションのパッケージマネージャーにて管理されているようです。
むしろソースコードコンパイルの情報が少ないのが不思議ですが単にその必要が少ないだけなのかもしれません。

Rustでの開発用にはDockerfileを書きましたが、コンテナで動かすとGUIでの確認ができなかったりするので一旦はローカルマシンにインストールして触ってみるのが良いと思います。

Mac
$ brew install gstreamer \
  gst-libav \
  gst-plugins-bad \
  gst-plugins-base \
  gst-plugins-good \
  gst-plugins-ugly

# みんな大好きBigBuckBummyをダウンロードする
$ wget https://mirror.clarkson.edu/blender/demo/movies/BBB/bbb_sunflower_1080p_30fps_normal.mp4

プログラミングの前に gst-launch-1.0 を理解する

GStreamerの本来の目的はプログラムを組むことにあるわけですが、そもそも概念を理解していないといけません。
ffmpegと同じようにコマンドラインツールとして使える gst-launch-1.0 というバイナリが標準で入っています。
ただ、公式としてはこれはデバッグ用ツールとのことです。

gst-launch-1.0の情報はググれば日本語でもわりと出てきはしますが、
videoの処理だけでaudioの処理が入ってないとか、とかく応用が効かないことが多いのです。

そんなわけでまず gst-launch-1.0 のコマンドがさっと頭に思い浮かぶようにならないとプログラムを書くどころではなさそうです。
参考までに、次の例はSRTで受信した動画・音声をDASHにするというだけのコマンドですが、これを書くだけで2日かかったような気がします。

gst-launch-1.0 -v srtserversrc uri="srt://:4201?mode=listener" ! tsdemux name=demux \
    demux. ! queue ! h264parse ! dash.video_0 \
    demux. ! queue ! audio/mpeg ! aacparse ! dash.audio_0 \
    dashsink name=dash mpd-root-path=/media muxer=ts dynamic=true target-duration=1

# なお、target-duration=1 (guint) は秒数を指定するのでdashsinkのソースを書き換えないと遅延を1秒未満にできない

h264parse とか audio/mpegなどこれらを指定しないと動きません。(一応、自動判別するためのプラグインも用意はされています)

GStramer基本概念

ここでは概念を一つずつ洗い出していきたいと思います。

パイプライン(pipeline)

GStreamerの実行単位をパイプラインと言います。

hst-launch-1.0 --help
usage:
  gst-launch-1.0 [option] PIPELINE_DESCRIPTION

パイプラインには状態(ステート)があります

プレイヤーを作る場合などは理解が必要と思われますが、ストリーミングでは流しっぱなしなのであまり意識する必要はありません。
動いているかそうでないかくらいです。真面目にエラー処理やる場合は必要かもしれません。
マニュアル

gst-launch-1.0 適当なパイプライン
$ gst-launch-1.0 fakesrc ! fakesink
パイプラインを一時停止 (PAUSED) にしています...
Pipeline is PREROLLING ...
Pipeline is PREROLLED ...
パイプラインを再生中 (PLAYING) にしています...
Redistribute latency...
New clock: GstSystemClock
^Chandling interrupt.        <- Ctrc + C で終了
割り込み: パイプラインを停止しています...
Execution ended after 0:00:04.069181000
Setting pipeline to NULL ...
Freeing pipeline ...

エレメント(element)

パイプラインは複数のエレメントからできています。

gst-launch-1.0 ではエレメントを ! で繋いて記載します。

$ gst-launch-1.0 element1 ! element2 ! ... ! elementN

Elementにはいくつか種類があり、srcで始まりsinkで終わります。

参考例
# 最小限の何もしないパイプライン
$ gst-launch-1.0 fakesrc ! fakesink

# テスト映像を表示する
$ gst-launch-1.0 videotestsrc ! autovideosink

# カメラの映像を表示する
$ gst-launch-1.0 avfvideosrc ! autovideosin

# テスト音声を再生する? (↓これは動かないんです 正しいコマンドは次の項で)
$ gst-launch-1.0 audiotestsrc ! autoaudiosink

テスト映像についてはわかりやすくて良いですね。色々パラメータがあるので試してみると面白いです

テスト音声については早速GStreamerらしいところが出てきました。一見動きそうですが動きません。

Elementのsrcとsinkの間はfilterで繋ぎます

参考例
# テスト映像に現在時刻を描画する
$ gst-launch-1.0 videotestsrc ! clockoverlay auto-resize=false time-format="%Y-%m-%d %H:%M:%S" ! autovideosink

# 1280 x 720 にする
$ gst-launch-1.0 avfvideosrc ! video/x-raw,width=1280,height=720 ! autovideosink

# テスト音声を再生する(音が鳴ります audioconvertについては後述)
$ gst-launch-1.0 audiotestsrc ! audioconvert ! autoaudiosink

# マイクの音声を再生する
$ gst-launch-1.0 autoaudiosrc ! audioconvert ! audioresample ! autoaudiosink

現在時刻表示については典型的なフィルタですね。
音声についてはフォーマット(capsとかcapabilityとかと呼ばれるもの)を合わせるためのものです。これを押さえておかないとパイプラインが組めません。詳しくは後述

(少し寄り道)GStreamerはオブジェクト指向です

上記の clockoverlayauto-resize=false というプロパティを設定しました。
ところがマニュアルをみてみるとプロパティは time-format しかありません。
なぜでしょう? フォントサイズやら表示位置やら色々なプロパティがあっても良いはずです。

答えはHierarchyのセクションにあります。

GObject
    ╰──GInitiallyUnowned
        ╰──GstObject
            ╰──GstElement
                ╰──GstBaseTextOverlay
                    ╰──clockoverlay

これはGstBaseTextOverlayを継承しているため、それが持つパラメータはそのまま使えるのでした。
このオブジェクト思考であるというところは、今後プラグインを自分で作るにあたって重要になってきます。

さらにfilterには mux / demux / tee / queue / capsfilter などがあります!

demuxはコンテナからデータを分離します

通常はファイルや受信したデータを映像と音声に分けて処理をします。
それぞれ別のスレッドになるため queue を挟む必要があります。(入れないとパイプラインが動きません)

# BigBuckBummyを再生する
$  gst-launch-1.0 filesrc location="./bbb_sunflower_1080p_30fps_normal.mp4" ! qtdemux name=demux \
    demux. ! queue ! avdec_h264 ! autovideosink \
    demux. ! queue ! mpegaudioparse ! avdec_mp3  ! audioconvert ! audioresample ! autoaudiosink

# SRTで受信したデータをDASHで書き出す
$ gst-launch-1.0 -v srtserversrc uri="srt://:4201?mode=listener" ! tsdemux name=demux \
    demux. ! queue ! h264parse ! dash.video_0 \
    demux. ! queue ! audio/mpeg ! aacparse ! dash.audio_0 \
    dashsink name=dash mpd-root-path=/media muxer=ts dynamic=true target-duration=1

ちなみにRTMPなら flvdemux を使うことでしょう。
(ちゃんと音声付きで再生される)

さて、エンコード・デコードが絡んでくると *convert だの *parse だの色々出てきます。
ひとまずfilterを先に一通り書いときます。

muxはデータをコンテナ化します

demuxの反対でデータをコンテナ化します。映像・音声を組み合わせことも多いですが、単純に映像だけ・音声だけを個別にコンテナ化してファイル書き出すことも可能です。

# カメラとマイクを一つのMP4ファイルに録画する
$ gst-launch-1.0 -e autovideosrc ! videoconvert ! x264enc tune=zerolatency speed-preset=ultrafast bitrate=500 ! h264parse ! queue ! mux. \
    autoaudiosrc ! audioconvert ! audioresample ! faac bitrate=192000 ! queue ! mux. \
    qtmux name=mux ! filesink location="./cam_mic.mp4"

カメラの映像だけを保存するコマンド例は結構見かけるのですが、マイクも合わせて保存する例がありそうでないんです。
しかも -e オプションをつけないとファイルが破損します。

# -e オプションは終了時にストリームの終端であることをプラグインに通知し、その結果ファイルのヘッダーがきちんと書き込まれます。
  -e, --eos-on-shutdown             Force EOS on sources before shutting the pipeline down

teeは映像や音声スレッドを複製します

すみません。あまり使ったことないのと、こちらの記事がよく纏まっています。

capsfilterはcapabilitiesを設定します

はい、なんのこちゃよくわからないので次で詳しく説明します。
audio/mpeg とか video/x-raw,width=1280,height=720``h264parse とかそういうやつです。

今回の山場、PadとCapabilitiesはどこにどういうデータが流れるか指定します

まず、映像や音声など一つのストリームが流れる出入り口を Pad と言います。
エレメントは複数のPadを持つことがあり、入口がsink 出口がsrc となります。
(sinkは凹んだ形、データの流れを受け止めるところ。srcは源、データの流れが湧き出すところ。とイメージすると覚えやすいかもしれません)

なぜ、GStreamerのコマンドを適当に書くと動かないのでしょうか。
それは各エレメントがPadCapabilitiesと流れるデータが一致するように求めるためです。

基本的なエレメントはpadを一つ持ちます

例えば、filesrcfilesink、ダミーデータを流すだけの fakesrcfakesinkにはどんなデータが流れても良いわけです。
データが破損してようがランダム文字列であろうが、ただ読み書きさえすれば良いわけです。
ただし、複数のストリームを扱うことは想定していないのでPadは一つしか持ちません。

filesrcの例
$  gst-inspect-1.0 filesink
...

Pad Templates:
  SINK template: 'sink'
    Availability: Always
    Capabilities:
      ANY

padはsink padが一つあるだけです。
映像と音声の二つの入力を渡されても書き込み順番はわからないのでそういう処理はmuxに任せることになります。
流れるデータは何でも良いので Capabilities: ANYとなっています。

filterエレメントの多くもやはり受け取ったデータを加工して次に渡すだけなので sink pad一つとsrc pad一つを持ちます。

mux / demux は複数のpadを持ちます

demuxは一つのコンテナから複数の映像や音声を分離するため、複数のsrc padを持ちます。
反対にmuxは複数の映像や音声から一つのコンテナ化されたデータを出力するので複数のsink padを持ちます。

capabilitiesとは受け入れ可能なフォーマットのこと

先ほど例にあげた filesrcfilesinkについてはどんなデータがでも良いわけですが、
clockoverlayや再生するためのautovideosinkなどは予めpadを流れるデータがどういうフォーマットか知っていなければなりません。
また、muxdemuxは形式さえ分かっていればよく、詳細なフォーマットまでは知る必要はありません。
それらの情報はプラグインに書いてあります。

例えば、qtmuxであればMP4に準じたデータである必要がありますが、分離したデータについては何が入っていても良いというわけです。

qtmuxの例
$ gst-inspect-1.0 qtdemux

  SINK template: 'sink'
    Availability: Always
    Capabilities:
      video/quicktime
      video/mj2
      audio/x-m4a
      application/x-3gp

# srcについてはどんなデータでも良い
  SRC template: 'video_%u'
    Availability: Sometimes
    Capabilities:
      ANY
...

反対にclocloverlayではデコードされたデータである程度のフォーマットである必要があります。

$ gst-inspect-1.0 clockoverlay
...
  SINK template: 'video_sink'
    Availability: Always
    Capabilities:
      video/x-raw
                 format: { (string)ABGR64_LE, (string)BGRA64_LE, (string)AYUV64, (string)ARGB64_LE, (string)ARGB64, (string)RGBA64_LE, (string)ABGR64_BE, (string)BGRA64_BE, (string)ARGB64_BE, (string)RGBA64_BE, (string)GBRA_12LE, (string)GBRA_12BE, (string)Y412_LE, (string)Y412_BE, (string)A444_10LE, (string)GBRA_10LE, (string)A444_10BE, (string)GBRA_10BE, (string)A422_10LE, (string)A422_10BE, (string)A420_10LE, (string)A420_10BE, (string)RGB10A2_LE, (string)BGR10A2_LE, (string)Y410, (string)GBRA, (string)ABGR, (string)VUYA, (string)BGRA, (string)AYUV, (string)ARGB, (string)RGBA, (string)A420, (string)AV12, (string)Y444_16LE, (string)Y444_16BE, (string)v216, (string)P016_LE, (string)P016_BE, (string)Y444_12LE, (string)GBR_12LE, (string)Y444_12BE, (string)GBR_12BE, (string)I422_12LE, (string)I422_12BE, (string)Y212_LE, (string)Y212_BE, (string)I420_12LE, (string)I420_12BE, (string)P012_LE, (string)P012_BE, (string)Y444_10LE, (string)GBR_10LE, (string)Y444_10BE, (string)GBR_10BE, (string)r210, (string)I422_10LE, (string)I422_10BE, (string)NV16_10LE32, (string)Y210, (string)v210, (string)UYVP, (string)I420_10LE, (string)I420_10BE, (string)P010_10LE, (string)NV12_10LE32, (string)NV12_10LE40, (string)P010_10BE, (string)Y444, (string)RGBP, (string)GBR, (string)BGRP, (string)NV24, (string)xBGR, (string)BGRx, (string)xRGB, (string)RGBx, (string)BGR, (string)IYU2, (string)v308, (string)RGB, (string)Y42B, (string)NV61, (string)NV16, (string)VYUY, (string)UYVY, (string)YVYU, (string)YUY2, (string)I420, (string)YV12, (string)NV21, (string)NV12, (string)NV12_64Z32, (string)NV12_4L4, (string)NV12_32L32, (string)Y41B, (string)IYU1, (string)YVU9, (string)YUV9, (string)RGB16, (string)BGR16, (string)RGB15, (string)BGR15, (string)RGB8P, (string)GRAY16_LE, (string)GRAY16_BE, (string)GRAY10_LE32, (string)GRAY8 }
                  width: [ 1, 2147483647 ]
                 height: [ 1, 2147483647 ]
              framerate: [ 0/1, 2147483647/1 ]
...

convert resample parse はフォーマット変換エレメント

時折出てきた videoconvertaudioresampleなどは細かいフォーマット変換のためのエレメントです。
各エレメントの間で、同じ生データだけれども画像のフォーマットやサンプリングレートが異なるときに変換してくれるものです。

まとめ

このほかにもbinやらバッファやら色々あるのですが、ひとまずcapsがわかればパイプラインは書けるかなと思います。
本当はRustのプラグイン書いたところまでまとめようと思いましたがまだまだ時間がかかりそうです。
それでは次回はRustでシンプルなスケルトンからチュートリアルをなぞる感じでまとめていこうと思います。

27
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
16