LoginSignup
7
3

More than 1 year has passed since last update.

RustでGStreamer Pluginを書く

Last updated at Posted at 2022-12-23

これは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

gst-launch.PAUSED_PLAYING.png

横に長くて大変見づらいですが大まかには次のようになっています

  1. GstURIDecodeBin: URIのデータを見てメディアを判別し、映像や音声をデコードする
  2. GstPlaySink: 適切な出力に繋ぐ
    1. 映像はximagesinkへ
    2. 音声は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だけに出力があるのがわかります。
gst-launch.PAUSED_PLAYING.png

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つあります。

  1. gstはGStremaerのrust bindingsでgstremaerの依存関係や関数を呼ぶのに使います。
  2. gst-baseはGStreamerのエレメントを書く場合は基本となる実装のライブラリで、プラグインはベースクラスを拡張する形で実装するのが楽です。Base and Utility classesにあるBase classとなっているのがその対象です。
  3. 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

ここがプラグインの実装です。重要な部分だけを抜粋します。

ObjectSubclassmod.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試してみてください。

7
3
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
7
3