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

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

https://qiita.com/alivelime/items/dd96d7c91c7d5f3991ef

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

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

```plantuml

actor 配信者 as publisher
actor 見る人たち as audience

[OBS Studio] as obs

node server {
  () SRT as srt
  [ffmpeg]
  [HDD] as hdd
  () inotify
  [quic_go]
}

node browser {
  () WebTransport as wt
  [MP4パーサー] as parser
  [デコーダー\nWebCodecs] as dec
  [レンダラー\nMediaStream] as renderer
}

publisher -> obs
obs -> srt
srt -> ffmpeg
ffmpeg -> hdd
hdd <- inotify
inotify -> quic_go

quic_go --> wt

audience <-- renderer
```

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

ffmpegはとても良くできていて[公式マニュアル](https://ffmpeg.org/ffmpeg.html#Options)にこれでもかと情報が載っていますし、
[ニコラボ](https://nico-lab.net/category/ffmpeg/)さんをはじめ、日本語の情報もたくさんあります。ググれば大体何とかなるのではないでしょうか?

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

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

# そこでGStreamerの出番

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

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

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

```plantuml
title :GStreamerの処理イメージ:動画・音声に加工する場合
left to right direction

[入力元\nsrc] as src
[demuxer\n分離する] as demux
[video queue] as v_que
[audio queue] as a_que
[decoder] as v_dec
[decoder] as a_dec
[filter] as v_filter
[filter] as a_filter
[encoder] as v_enc
[encoder] as a_enc
[muxer\n結合する] as mux
[出力先\nsink] as sink

src --> demux
demux --> v_que
demux --> a_que
v_que --> v_dec
v_dec --> v_filter
v_filter --> v_enc
v_enc --> mux

a_que --> a_dec
a_dec --> a_filter
a_filter --> a_enc
a_enc --> mux

mux --> sink
 ```

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

[公式マニュアル](https://gstreamer.freedesktop.org/documentation/application-development/index.html?gi-language=c)には色々と概念からチュートリアルまで一通り書いてあるのですが、イマイチよくわかりません。
チュートリアルをやって、マニュアルを見て、またチュートリアルをやって、と何度か繰り返しているとわかってくるのかもしれません。

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

### 日本語の情報

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

https://qiita.com/advent-calendar/2015/gstreamer

https://qiita.com/advent-calendar/2016/gstreamer

http://hellolibraryworld.blogspot.com/2015/12/gstreamer-1-pipeline-parse.html

特に上記の方の[GStreamerでテキストを処理する話](http://hellolibraryworld.blogspot.com/2016/12/gstreamer-1-core-cat-cp-dd-tee.html)については仕組みが理解できそうなのでRustでもやってみたいと思います。

### 海外の情報

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

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

https://www.youtube.com/watch?v=mwuCMDSZdiQ

# 環境構築

https://gstreamer.freedesktop.org/documentation/installing/index.html?gi-language=c

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

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

```shell: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日かかったような気がします。

```shell
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の実行単位をパイプラインと言います。

```plantuml
[input]
package gstreamer {
  [pipeline]
}
[output]

input -> pipeline
pipeline -> output
```

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

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

プレイヤーを作る場合などは理解が必要と思われますが、ストリーミングでは流しっぱなしなのであまり意識する必要はありません。
動いているかそうでないかくらいです。真面目にエラー処理やる場合は必要かもしれません。
[マニュアル](https://gstreamer.freedesktop.org/documentation/additional/design/states.html?gi-language=c)

```plantuml
[*] --> NULL
NULL -> Ready
NULL <- Ready
Ready -> Paused
Ready <- Paused
Paused -> Playing
Paused <- Playing
NULL --> [*]
```

```shell: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)

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

```plantuml

package gstreamer {
  package pipeline {
    [element1]
    [element2]
    [...]
    [elementN]
  }
}

element1 -> element2
element2 -> ...
... -> elementN

```

gst-launch-1.0 ではエレメントを `!` で繋いて記載します。
```shell
$ gst-launch-1.0 element1 ! element2 ! ... ! elementN
```

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

```plantuml
[input]
package gstreamer {
  package pipeline {
    [src]
    [sink]
  }
}
[output]

src --> input
src -> sink
sink --> output
```


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

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

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

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

テスト映像についてはわかりやすくて良いですね。色々[パラメータ](https://gstreamer.freedesktop.org/documentation/videotestsrc/index.html?gi-language=c)があるので試してみると面白いです
<img width=320 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/c66469f2-1fe6-4278-e2bc-ba8fb6659781.png" />

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

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

```plantuml
[input]
package gstreamer {
  package pipeline {
    [src]
    [filter]
    [sink]
  }
}
[output]

src --> input
src -> filter
filter -> sink
sink --> output
```

```shell:参考例
# テスト映像に現在時刻を描画する
$ 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`とかと呼ばれるもの)を合わせるためのものです。これを押さえておかないとパイプラインが組めません。詳しくは後述
<img width=320 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/51dd2761-5cf9-97f2-cd4c-3ab5e40a29d0.png" />

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

上記の `clockoverlay` で `auto-resize=false` というプロパティを設定しました。
ところが[マニュアル](https://gstreamer.freedesktop.org/documentation/pango/clockoverlay.html?gi-language=c)をみてみるとプロパティは `time-format` しかありません。
なぜでしょう? フォントサイズやら表示位置やら色々なプロパティがあっても良いはずです。

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

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

これは[GstBaseTextOverlay](https://gstreamer.freedesktop.org/documentation/pango/GstBaseTextOverlay.html?gi-language=c#GstBaseTextOverlay)を継承しているため、それが持つパラメータはそのまま使えるのでした。
このオブジェクト思考であるというところは、今後プラグインを自分で作るにあたって重要になってきます。

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

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


```plantuml
left to right direction

[MP4\nMPEG2TS\nなど] as input
package gstreamer {
  [src]
  [demux]
  [video queue] as v_que
  [audio queue] as a_que
  [video sink] as v_sink
  [audio sink] as a_sink
}
[output] as v_output
[output] as a_output

src -le-> input
src --> demux
demux --> v_que
demux --> a_que
v_que --> v_sink
a_que --> a_sink
v_sink --> v_output
a_sink --> a_output
```

```shell
# 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` を使うことでしょう。
(ちゃんと音声付きで再生される)
<img width=480 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/b89165be-016e-7ec4-4f03-901158848106.png" />

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

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

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

```plantuml
left to right direction
[video input] as v_input
[audio_input] as a_input

package gstreamer {
  [video src] as v_src
  [audio src] as a_src
  [muxer] as mux
}
[output]

v_input --> v_src
a_input --> a_src
v_src --> mux
a_src --> mux
mux --> output
```

```shell
# カメラとマイクを一つの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` オプションをつけないとファイルが破損します。

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

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

すみません。あまり使ったことないのと、[こちらの記事](https://dev.classmethod.jp/articles/kinesis-video-streams-gstreamer-tee-queue/)がよく纏まっています。

### capsfilterはcapabilitiesを設定します

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

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

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

```plantuml
left to right direction

package gstreamer {
  package "src element" {
    [src pad] as src_src
  }
  () "受け入れ可能フォーマット\ncapabilities" as filter_caps
  package "filter" {
    [sink pad] as filter_sink
    [src pad] as filter_src
  }
  () "capabilities" as sink_caps
  package "sink element" {
    [sink pad] as sink_sink
  }
}
src_src --> filter_caps
filter_caps --> filter_sink
filter_sink --> filter_src
filter_src --> sink_caps
sink_caps --> sink_sink
```

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

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

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

```shell: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`を持ちます。

```plantuml
left to right direction
package gstreamer {
  [src]
  package demux {
    [sinkpad] as demux_sink
    [video0 srcpad] as v0_demux_src
    [audio0 srcpad] as a0_demux_src
    [audio1 srcpad] as a1_demux_src
  }
  () "video capabilities" as v_caps
  () "audio capabilities" as a_caps
  [sink] as v_sink
  [sink] as a_sink
}
src --> demux_sink
demux_sink --> v0_demux_src
demux_sink --> a0_demux_src
demux_sink --> a1_demux_src

v0_demux_src --> v_caps
v_caps --> v_sink
a0_demux_src --> a_caps
a_caps --> a_sink
```

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

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

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

```shell: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`ではデコードされたデータである程度のフォーマットである必要があります。

```shell
$ 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 はフォーマット変換エレメント

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

# まとめ

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