Inside of LOVOT

GROOVE X 技術ブログ

RustでGStreamerを使うのがめっちゃ楽な件

この記事はGROOVE X Advent Calendar 2024の23日目の記事です。

こんにちは、あずこと斎藤@aznhe21です。スチーム式の加湿器を買ったらカルキの溜まり方がエグくて笑いました。

さて、LOVOT 3.0では画像認識にDeepStreamを採用しています。 DeepStreamはNVIDIA製画像・音声認識ツールキットで、GStreamer上に構築されています。

画像認識部分にはRust言語を使う方針であったためRustからGStreamerを使う必要があったわけですが、 GStreamerはなんと公式でRust用バインディングとしてgstreamer-rsを提供してくれています。 このgstreamer-rsがかなり使いやすかったので、今回はその使い方を簡単にご紹介したいと思います。

なおgstreamer-rsを使うにはセットアップが必要ですが、この手順は公式ドキュメントにあるのでそちらをご参照ください。 また記事に記載しているサンプルは一部の使用例を示したものに過ぎず、かつ最新の仕様に基づいているとは限りません。 みなさんが使う際には公式サンプルをご覧ください。

実行が簡単

パイプライン記述(gst-launch-1.0コマンドに渡すやつ)をそのまま渡すので良ければ30行ほどで実行できます。

// [dependencies]
// gst = { package = "gstreamer", version = "0.23.3" }
use gst::prelude::*;

fn main() {
    gst::init().unwrap();

    // パイプライン構築
    let pipeline = gst::parse::launch("videotestsrc ! autovideosink").unwrap();

    // パイプライン開始
    pipeline.set_state(gst::State::Playing).unwrap();

    // パイプラインからのイベントを処理
    let bus = pipeline.bus().unwrap();
    for message in bus.iter_timed(gst::ClockTime::NONE) {
        use gst::MessageView;
        match message.view() {
            MessageView::Eos(_) => break,
            MessageView::Error(err) => {
                println!(
                    "Error from {:?}: {} ({:?})",
                    message.src().map(|s| s.path_string()),
                    err.error(),
                    err.debug()
                );
                break;
            }
            _ => {}
        }
    }

    // パイプライン終了
    pipeline.set_state(gst::State::Null).unwrap();
}

簡単ですね!

パイプラインの構築が簡単

パイプライン記述はテスト用に使うのは簡単ですが、これは文字列です。 実際のプログラムではプロパティを定数なり変数なりで指定する必要があるため、 文字列でパイプラインを構築するのは面倒です。

そこでパイプライン内のエレメントを個々に手動で構築することになるわけですが、 エレメントはビルダーパターンで構築できるため非常に簡単です。

    // 最初のサンプルの`let pipeline = ...`を書き換える
    let pipeline = gst::Pipeline::new();

    // エレメントをビルダーパターンで構築できる
    let src = gst::ElementFactory::make("videotestsrc")
        .property_from_str("pattern", "ball")
        .property("flip", true)
        .property("num-buffers", 100_i32)
        .build()
        .unwrap();
    let sink = gst::ElementFactory::make("autovideosink").build().unwrap();

    // 構築したエレメントをパイプラインに追加・リンク
    pipeline.add_many([&src, &sink]).unwrap();
    gst::Element::link_many([&src, &sink]).unwrap();

ただしサンプルの100_i32100_u32にしたときなど、プロパティの型が一致していない場合は実行時にパニックしてしまうため、 gst-inspect-1.0で事前に確認しておくなど注意が必要です(sqlxみたいなコンパイル時に型チェックできる仕組みが欲しい)。

非同期とも相性が良い

多くの機能が非同期プログラミングに対応しており、非同期ランタイム下でも処理をブロックせずにパイプラインの処理が可能です。

// [dependencies]
// gst = { package = "gstreamer", version = "0.23.3" }
// tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread"] }
// tokio-stream = "0.1.17"

use gst::prelude::*;
use tokio_stream::StreamExt;

#[tokio::main]
async fn main() {
    // ...

    // メッセージループを非同期化
    let mut bus_stream = pipeline.bus().unwrap().stream();
    while let Some(message) = bus_stream.next().await {
        // ...
    }

    // ...
}

他にもgstreamer-appクレートのAppSrc/AppSinkではプログラムからデータを入出力することが可能ですが、 こちらも非同期に対応しており、LOVOT内部ではデバッグ用の映像配信に使っていたりします。

なおpipeline.set_state(...)はブロックしてるはずなんですがこちらには非同期用の機能はないようでした。 仕方が無いのでtokio::task::spawn_blockingの中で使うようにしています。

エレメントを簡単に自作できる

GStreamer標準のエレメントやAppSrc/AppSinkだけでもかなりの表現力があるのですが、 パイプラインが複雑になってくるとエレメントを自作したいことがあります。

gstreamer-rsでは自作エレメントを作ることも出来ます。 よく見るプラグインとして提供する方法はもちろん、プログラム内部用に提供することもできます。

これを記述すると長くなってしまうためサンプルは割愛します。

自作したエレメントを簡単にテストできる

せっかくエレメントを作れても、動作を単体テストできなければ意味がありません。 動作を単体テストできなければ意味がありません(大事なことなので2回言いました)。

GStreamerにはエレメントをテストする機能が備わっており、gstreamer-rsでもこれを利用できます。

// [dependencies]
// gst-check = { package = "gstreamer-check", version = "0.23.2" }

// 複数のテストがある際に多重初期化をさせない
fn init() {
    use std::sync::Once;
    static INIT: Once = Once::new();

    INIT.call_once(|| {
        gst::init().unwrap();
        plugin_module::plugin_register_static().unwrap();
    });
}

#[test]
fn test_vts() {
    init();

    let e = gst::ElementFactory::make("videotestsrc").build().unwrap();

    let mut h = gst_check::Harness::with_element(&e, None, Some("src"));
    h.set_sink_caps(
        gst::Caps::builder("video/x-raw")
            .field("format", gst::List::new(["RGB"]))
            .field("width", 320_i32)
            .field("height", 240_i32)
            .build(),
    );
    h.play();

    // 生成されたフレームの大きさを確認
    let buffer = h.pull().unwrap();
    assert_eq!(buffer.size(), 320 * 240 * 3);
}

さいごに

gstreamer-rsのおかげでRustによるDeepStreamアプリケーションを楽に開発できています。 やはりRustは楽しくて良い言語です。

GROOVE XではRustで画像認識したい方を募集しています。