これはRust Advent Calendar 2022カレンダー2、23日目の記事です。
GStreamerとは
GStreamerはオープンソースのマルチメディアフレームワークです。
特に動画や音声データを扱うときに便利なフレームワークでカメラやマイクのデバイスから入力を受け、デコード、加工して再生、またはエンコードして記録、配信などを既存のコンポーネントを再利用することで容易に実現できます。
記事によくあるのはエッジとして置いているRaspberryPiで録画あるいは配信などもありますが、エッジデバイスの性能向上して推論用のエレメントも公開されており、ますます動画データの活用がやりやすくなっています。
具体的にはNVIDIAのDeepstream、intelのDL StreamerやHailoのTAPPASなどがあります。
GStreamerでできること
詳細はGStreamerのチュートリアルに任せるとして、動作例を挙げていきます。
筆者の環境はUbuntu20.04
なので他の環境の方は適宜読み替えてください。
# gstreamerのinstall
sudo apt install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
# 動画の再生 音がでます
gst-launch-1.0 playbin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm
playbinというのがエレメントです。
この場合はuriで指定した動画ファイルをいい感じにとってきて、実行環境の画面と音声出力をいい感じに確保して動画を再生します
いい感じにとは
GStreamerは環境変数から様々な設定が出来てデバッグ関係のものもあります
GST_DEBUG_DUMP_DOT_DIR
を指定すると実行しているパイプラインを出力します。
GST_DEBUG_DUMP_DOT_DIR=. gst-launch-1.0 playbin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm
横に長くて大変見づらいですが大まかには次のようになっています
- GstURIDecodeBin: URIのデータを見てメディアを判別し、映像や音声をデコードする
- GstPlaySink: 適切な出力に繋ぐ
- 映像はximagesinkへ
- 音声はpulsesinkへ
メディアの判別やデコード、出力デバイスの確保、データのスケールやリサンプルなど個別に実装したエレメントがあり、それらを適切に繋げることで動画データを再生しているのがわかります。
gst-launch
GStreamerのプログラムはエレメントを適切に繋いでパイプラインを構築し、パイプラインやエレメントが発するイベントを元に駆動します。
gst-launch-1.0は専用の構文でそれらを構築してくれるプログラムです。
playbinがしていることの一部を切り出してみます。
gst-launch-1.0 uridecodebin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm ! autovideosink
uridecodebinがメディアの判別とデコード、その先に映像出力のみを行うautovideosinkを繋げると音声はなく映像のみが出力されます。
グラフでも音声出力が繋がらずximagesinkだけに出力があるのがわかります。
GstApp
gst-launch-1.0はあくまでパイプライン構築実験のためでアプリケーションに組み込むことは推奨されていません。
コードで書くのは専用の構文と比べると煩雑になりますが、アプリケーションとやり取りするのに便利なGstAppを使うことが出来ます。
主題ではないのでコードはgstreamer-rs/exmaplesのappsrc.rs, appsink.rsを参照してください。
GstAppはビデオ入力を柔軟にでき、プログラムに繋ぐことが容易になります。例えばOpenCVのVideoCaptureではappsinkを使ってGstreamerから入力を受けています。
AppSrcはデータを生成して下流に渡し、AppSinkは上流からデータを受けて何らかの処理が出来ます。
これはとても便利ですが、SrcとSinkでは受け取ったデータを何らかの加工を施して下流に流すことは出来ません。
また、これらの実装はアプリケーション毎にする必要があり再利用性も悪いです。
もっと柔軟に使いたい。そうなればpluginを書くしかありません。
PluginをRustで書く
GStreamerにはrust-bindingが存在し、さらにgst-plugin-rsにチュートリアルも存在します。
ここではどのようにTransformエレメントを作るか、最小限の実装について解説します。
SimpleTransエレメント
バッファが来たらログを表示してsinkに流すだけのエレメントを作ります。
動くコードを用意したので全体はGithubのコードを見てください。
git clone -b example/simple https://github.com/uzuna/gst-example-rs.git
Cargo.toml
使う依存関係は3つあります。
- gstはGStremaerのrust bindingsでgstremaerの依存関係や関数を呼ぶのに使います。
- gst-baseはGStreamerのエレメントを書く場合は基本となる実装のライブラリで、プラグインはベースクラスを拡張する形で実装するのが楽です。Base and Utility classesにあるBase classとなっているのがその対象です。
- once_cellはログのための構造体などGStreamerが起動後に宣言しstaticに扱いたいもののために使います。
gst-plugin-version-helperはプラグイン登録の補助に使います。
最後にGStreamerのプラグインはSharedObjectとして配布、配置する必要があるのでライブラリ名とcrate-typeにcdylib
を指定します。
[dependencies]
# gstremaerはcrates.ioでの公開ではない。
# master参照すると進んでしまうのでbranchとバージョンで固定する
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19.1" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "0.19", version = "0.19" }
once_cell = "1.0"
[build-dependencies]
gst-plugin-version-helper = { git = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"}
[lib]
name = "gstrsexample"
crate-type = ["cdylib"]
path = "src/lib.rs"
lib.rs
ここではGStreamerに対してプラグインの登録のフックを用意します。
version helperを使ってパッケージの情報はCargo.toml
に書かれたものを渡します。
GStreamerが初期化時にこのライブラリからplugin_initを探して呼び出します。
プラグインを登録するためsimpletrans::register(plugin)?;
を記述しておきます
use gst::glib;
mod simpletrans;
gst::plugin_define!(
// これはマクロなので文字列ではない。ライブラリ名と同じ文字列を指定する必要がある
rsexample,
env!("CARGO_PKG_DESCRIPTION"),
plugin_init,
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
// The licence parameter must be one of: LGPL, GPL, QPL, GPL/QPL, MPL, BSD, MIT/X11, Proprietary, unknown.
// refer: https://api.gtkd.org/gstreamer.c.types.GstPluginDesc.html
"MIT/X11",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_REPOSITORY"),
env!("BUILD_REL_DATE")
);
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
simpletrans::register(plugin)?;
Ok(())
}
mod.rs
ここではGStreamerにプラグインを登録します。
エレメントの特性を宣言をするのにgst::glib::wrapper
で定義します。
use gst::glib;
use gst::prelude::*;
const ELEMENT_NAME: &str = "simpletrans";
const CLASS_NAME: &str = "SimpleTrans";
mod imp;
gst::glib::wrapper! {
// 継承するクラスをここで宣言する
// 少なくとも gst::Element, gst::Object が必要でObjectSubclassでParentTypeに指定するクラスをここに書く
// 今回はgst_base::BaseTransform
pub struct SimpleTrans(ObjectSubclass<imp::SimpleTrans>) @extends gst_base::BaseTransform, gst::Element, gst::Object;
}
// gstreamerにこのエレメントを登録する
// gstremaerは起動時にpluginのあるディレクトリからダイナミックリンクライブラリを読み
// エレメント以外にもメタデータやgstreamer内で一意になるように情報を割り当てるため、
// 何らかの構造体を使い時には登録が必要
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
gst::Element::register(
Some(plugin),
ELEMENT_NAME,
gst::Rank::None,
SimpleTrans::static_type(),
)
}
imp.rs
ここがプラグインの実装です。重要な部分だけを抜粋します。
ObjectSubclass
でmod.rs
と同じように親クラスの宣言を行います。
今回はTransformエレメントを作るのでBaseTransform
です。
#[glib::object_subclass]
impl ObjectSubclass for SimpleTrans {
const NAME: &'static str = CLASS_NAME;
type Type = super::SimpleTrans;
// どのクラスを継承するか。
// 時前で全て実装する場合はgst::Elementを使うが、パターンが明らかなら
// gst_baseのBaseSrc,BaseSink,BaseTransform,GstAggregatorなどを選ぶのがよい
type ParentType = gst_base::BaseTransform;
}
BaseTransform
が親クラスとなっているので、持つメソッドの内このエレメント特有の処理を実装します。
今回は何もせずに流すのでGstBufferを受けるところだけ実装します。
TransformImplには3つのconstがあり、この宣言次第で3つの関数何れかが呼ばれます。
今回はAlwaysInPlace
かつPASSTHROUGH_ON_SAME_CAPS=true
なのでtransform_ip_passthrough
が呼ばれます。
gst::info!
マクロでgstのログに出力します。
impl BaseTransformImpl for SimpleTrans {
// AlwaysInPlaceなので入力されたGstBufferを使いそのままsinkに流す
const MODE: gst_base::subclass::BaseTransformMode =
gst_base::subclass::BaseTransformMode::AlwaysInPlace;
// 文字通りPassThroughするかどうか
const PASSTHROUGH_ON_SAME_CAPS: bool = true;
const TRANSFORM_IP_ON_PASSTHROUGH: bool = true;
// AlwaysInPlace以外の場合に実装が必要
// inbufの内容を加工してoutbufに書き込みOkを返すとsinkに送られる
fn transform(
&self,
_inbuf: &gst::Buffer,
_outbuf: &mut gst::BufferRef,
) -> Result<gst::FlowSuccess, gst::FlowError> {
gst::info!(CAT, "transform");
Ok(gst::FlowSuccess::Ok)
}
// transformの入力されたGstBufferに手を加える、あるいは何もせずにsinkに流す
// 例えばカラー変更などバッファのサイズが変わらないなどの場合に使う
fn transform_ip(&self, _buf: &mut gst::BufferRef) -> Result<gst::FlowSuccess, gst::FlowError> {
gst::info!(CAT, "transform_ip");
Ok(gst::FlowSuccess::Ok)
}
// PassThroughは基本的にはGstBufferを無駄に処理せずにただsinkに流すためにある
// 例えばカラー変更でsrcとsinkのCapabilityが同じ場合は処理の必要がない
// このような処理を分けたい場合にPASSTHROUGH_ON_SAME_CAPS=trueにして利用する
fn transform_ip_passthrough(
&self,
_buf: &gst::Buffer,
) -> Result<gst::FlowSuccess, gst::FlowError> {
gst::info!(CAT, "transform_ip_passthrough");
Ok(gst::FlowSuccess::Ok)
}
}
動作確認
動作確認してみます。
まずはgst-inspect-1.0で情報が出ることを確認します。
# リポジトリをcloneした方は make inspectで
gst-inspect-1.0 --gst-plugin-path=./target/debug/ ./target/debug/libgstrsexample.so
Plugin Details:
Name rsexample
Description Rust Example Plugin
Filename ./target/debug/libgstrsexample.so
Version 0.0.1-707f7fa
License MIT/X11
Source module gst-example-plugin
Source release date 2022-12-23
Binary package gst-example-plugin
Origin URL https://github.com/uzuna/gst-example-rs
simpletrans: SimpleTrans
1 features:
+-- 1 elements
更に詳細を見てみましょう。エレメント名も指定してみます。
以下のようにどのクラスを実装しているか、PadTemplate、Propertyを持っているかが表示されます。
Propertyを追加実装していませんが親のクラスがname, parent, qos
を実装しているのがわかります。
# リポジトリをcloneした方は make inspect ELEM=simpletransで
gst-inspect-1.0 --gst-plugin-path=./target/debug/ ./target/debug/libgstrsexample.so simpletrans
Factory Details:
Rank none (0)
Long-name SimpleTrans
Klass Generic
Description simple transform
Author FUJINAKA Fumiya <uzuna.kf@gmail.com>
Plugin Details:
Name rsexample
Description Rust Example Plugin
Filename ./target/debug/libgstrsexample.so
Version 0.0.1-707f7fa
License MIT/X11
Source module gst-example-plugin
Source release date 2022-12-23
Binary package gst-example-plugin
Origin URL https://github.com/uzuna/gst-example-rs
GObject
+----GInitiallyUnowned
+----GstObject
+----GstElement
+----GstBaseTransform
+----SimpleTrans
Pad Templates:
SINK template: 'sink'
Availability: Always
Capabilities:
ANY
SRC template: 'src'
Availability: Always
Capabilities:
ANY
Element has no clocking capabilities.
Element has no URI handling capabilities.
Pads:
SINK: 'sink'
Pad Template: 'sink'
SRC: 'src'
Pad Template: 'src'
Element Properties:
name : The name of the object
flags: readable, writable
String. Default: "simpletrans0"
parent : The parent of the object
flags: readable, writable
Object of type "GstObject"
qos : Handle Quality-of-Service events
flags: readable, writable
Boolean. Default: false
最後に実行してみます。
30フレームのデータが出て終了します。
transform_ip_passthrough
が呼ばれているのがわかりますね。
MODEやPASSTHROUGH_ON_SAME_CAPSを変更すると他のメソッドが呼ばれるのが確認できます。
# リポジトリをcloneした方は make launchで
GST_DEBUG=1,simpletrans:7 gst-launch-1.0 --gst-plugin-path=./target/debug/ videotestsrc num-buffers=30 ! simpletrans ! autovideosink
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
0:00:00.066875058 127524 0x55a3d8f84d20 INFO simpletrans plugin/src/simpletrans/imp.rs:117:gstrsexample::simpletrans::imp: transform_ip_passthrough
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
New clock: GstSystemClock
0:00:00.068375734 127524 0x55a3d8f84d20 INFO simpletrans plugin/src/simpletrans/imp.rs:117:gstrsexample::simpletrans::imp: transform_ip_passthrough
0:00:00.102002014 127524 0x55a3d8f84d20 INFO simpletrans plugin/src/simpletrans/imp.rs:117:gstrsexample::simpletrans::imp: transform_ip_passthrough
0:00:00.135238432 127524 0x55a3d8f84d20 INFO simpletrans plugin/src/simpletrans/imp.rs:117:gstrsexample::simpletrans::imp: transform_ip_passthrough
0:00:00.173129599 127524 0x55a3d8f84d20 INFO simpletrans plugin/src/simpletrans/imp.rs:117:gstrsexample::simpletrans::imp: transform_ip_passthrough
0:00:00.202333850 127524 0x55a3d8f84d20 INFO simpletrans plugin/src/simpletrans/imp.rs:117:gstrsexample::simpletrans::imp: transform_ip_passthrough
0:00:00.235573379 127524 0x55a3d8f84d20 INFO simpletrans plugin/src/simpletrans/imp.rs:117:gstrsexample::simpletrans::imp: transform_ip_passthrough
...
Got EOS from element "pipeline0".
Execution ended after 0:00:01.000032106
Setting pipeline to NULL ...
Freeing pipeline ...
まとめ
RustでGStreamer Pluginが書けることがわかりました。
マクロやトレイトの実装が充実しておりCでかくよりもずっと快適で安全に書くことが出来ます。
GStreamerはGlibの上に構築されておりエレメントやバッファはMutexで保護、ReferenceCountで生存管理されているため至るとイコロでLockとFree、RefとUnrefが必要ですがRustではRAIIによって自動解放されるので注意を払う必要が少なくとてもありがたいです。
ここでは紹介しませんでしたがGstBufferのデータを加工する以外にもメタデータの読み書きができ、GStreamerで動画を受け取るだけではなく各種メタデータと統合して扱えるとアプリケーションの用途が広がりそうです。
みなさんも動画処理にGStreamer試してみてください。