2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[ストリーミング技術]RustでGStreamerチュートリアル 4 : 時間関連の機能

Last updated at Posted at 2020-06-24

初回: https://qiita.com/kyasbal_1994/items/a1a7d1bd5c4832947a8a

前回:https://qiita.com/kyasbal_1994/items/88a5d0dfa2abdc44296c

次回:https://qiita.com/kyasbal_1994/items/ce7d0d6e75fde1d1aff4

コード: https://github.com/kyasbal-1994/qiita-gstreamer-rust-tutorial

Goal

今回はGStreamerの中でも時間に関連した機能を紹介します。

  • 現在の動画上の位置及び動画の長さなどのパイプラインの情報のクエリ手法
  • 他の動画上の時間にシークする方法

Introduction

GStreamerではクエリの機能を用いて要素やpadから情報を取り出すことができます。
このチュートリアルでは動画中の特定の区間を繰り返すことを行ってみます。

また、以前はEnd of Streamのタイミングか、エラーが起きたタイミングのみでしかメッセージの処理を行っていませんでした。今回は、何も処理しなくても良いNoneの際に動画上の現在の位置を取得し、全体の動画の長さとともに表示を行います。
seek-min.gif

Seeking Example

extern crate gstreamer as gst;

use gst::prelude::*;
use std::io;
use std::io::Write;

// Custom data type representing application state
struct PlayerState {
    playbin: gst::Element,
    playing: bool,
    terminate: bool,
    seek_enabled: bool,
    first_seek_done: bool,
    duration: gst::ClockTime,
}

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

    // Create an element.
    let playbin = gst::ElementFactory::make("playbin", Some("playbin"))
        .expect("Failed to create playbin element");

    // Set the URI to play
    let uri =
        "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm";
    playbin
        .set_property("uri", &uri)
        .expect("Can't set uri property on playbin");

    // Start playing
    playbin
        .set_state(gst::State::Playing)
        .expect("Unable to set the playbin to the playing state");

    // Monitor messages until player_state.terminate became true
    let bus = playbin.get_bus().unwrap();
    let mut player_state = PlayerState {
        playbin,
        playing: false,
        terminate: false,
        seek_enabled: false,
        first_seek_done: false,
        duration: gst::CLOCK_TIME_NONE,
    };
    while !player_state.terminate {
        let msg = bus.timed_pop(100 * gst::MSECOND);
        match msg {
            Some(msg) => handle_message(&mut player_state, &msg),
            None => {
                if player_state.playing {
                    // Update position by query
                    let position = player_state
                        .playbin
                        .query_position::<gst::ClockTime>()
                        .expect("Could not query current position");
                    // Query duration if player_state.duration was default value
                    if player_state.duration == gst::CLOCK_TIME_NONE {
                        player_state.duration = player_state
                            .playbin
                            .query_duration()
                            .expect("Could not query current duration");
                    }
                    // Peform a first seek because it begins from 0. I want to play 30s - 35s
                    if !player_state.first_seek_done && player_state.seek_enabled {
                        player_state
                            .playbin
                            .seek_simple(
                                gst::SeekFlags::FLUSH | gst::SeekFlags::KEY_UNIT,
                                30 * gst::SECOND,
                            )
                            .expect("Failed to seek");
                        player_state.first_seek_done = true;
                    } else {
                        // Printing progress and duration of the video
                        print!("\rPosition {} / {}", position, player_state.duration);
                        io::stdout().flush().unwrap();

                        // Perform a seek if the position was over 30s
                        if player_state.seek_enabled && position > 35 * gst::SECOND {
                            println!("\n Reached 5s performing seek...");
                            player_state
                                .playbin
                                .seek_simple(
                                    gst::SeekFlags::FLUSH | gst::SeekFlags::KEY_UNIT,
                                    30 * gst::SECOND,
                                )
                                .expect("Failed to seek");
                        }
                    }
                }
            }
        }
    }

    // Cleaning up
    player_state
        .playbin
        .set_state(gst::State::Null)
        .expect("Unable to set playbin to the Null state");
}

fn handle_message(player_state: &mut PlayerState, msg: &gst::Message) {
    match msg.view() {
        gst::MessageView::Error(err) => {
            println!(
                "Error received from element {:?}: {} ({:?})",
                err.get_src().map(|s| s.get_path_string()),
                err.get_error(),
                err.get_debug()
            );
            player_state.terminate = true;
        }
        gst::MessageView::Eos(..) => {
            println!("EOS");
            player_state.terminate = true;
        }
        gst::MessageView::DurationChanged(_) => {
            player_state.duration = gst::CLOCK_TIME_NONE;
        }
        gst::MessageView::StateChanged(state_changed) => {
            if state_changed
                .get_src()
                .map(|s| s == player_state.playbin)
                .unwrap_or(false)
            {
                let new_state = state_changed.get_current();
                let old_state = state_changed.get_old();

                println!(
                    "Pipeline state changed from {:?} to {:?}",
                    old_state, new_state
                );

                // If player state became first, we need to check that stream being seekable
                // Some streams can be unseekale.
                player_state.playing = new_state == gst::State::Playing;
                if player_state.playing {
                    let mut seeking = gst::Query::new_seeking(gst::Format::Time);
                    if player_state.playbin.query(&mut seeking) {
                        let (seekable, start, end) = seeking.get_result();
                        player_state.seek_enabled = seekable;
                        if seekable {
                            println!("Seeking is ENABLED from {:?} to {:?}", start, end)
                        } else {
                            println!("Seeking is DISABLED for this stream.")
                        }
                    } else {
                        eprintln!("Seeking query failed.")
                    }
                }
            }
        }
        _ => (),
    }
}

Workthrough

メッセージループ中で用いるデータをすべて含む型をプログラムの戦闘で作成しています。Rustでは自由に関数に引数をクロージャを使うことによって渡すことができますが、これはメッセージループにカスタム型を渡せるというCの仕様に則った名残です。
もし、rust流のやり方で適切に記述ができるようであればこの縛りはありません。

// Custom data type representing application state
struct PlayerState {
    playbin: gst::Element,
    playing: bool,
    terminate: bool,
    seek_enabled: bool,
    first_seek_done: bool,
    duration: gst::ClockTime,
}

次に、我々はplaybin人要素だけを作成しました。このplaybinはそれ自身がパイプラインでもあります。そのため、今回はplaybinを直接扱います。
今回はメッセージの監視には第一回で用いたtimed_pop_filteredではなく、直接timed_popを用いて発生したメッセージをすべてキャプチャしています。timed_pop_filteredの場合は、フィルタするメッセージが来るまで関数は返り値を返しませんが、timed_popは指定された秒数の間に何も値を得なかった場合はNoneを返します。通常はこれを用いてUIなどの更新を行います。

    while !player_state.terminate {
        let msg = bus.timed_pop(100 * gst::MSECOND);
        match msg {
...
        }
   }

User interface refreshing

パイプラインがPLAYINGステートのときに画面を更新します。ほとんどのクエリはそれ以外のステートでは失敗するため、PLAYING以外のときには何もしません。そこで、StateChangedメッセージでstateがplayingになるまで、メッセージがないときには何も行いません。

if player_state.playing {
   // Update position by query

おおよそ1秒に10回更新のタイミングを得ることができ、私達のUIにはちょうどよい更新のタイミングと言えます。ここでは、このタイミングで現在の動画の位置であるpositionとdurationをクエリしています。positionとdurationはとても良く使われるクエリなので、query_positionquery_durationの簡単なクエリが用意されています。

それ以外のクエリに関しては次のサブセクションで解説します。

    // Update position by query
    let position = player_state
        .playbin
        .query_position::<gst::ClockTime>()
        .expect("Could not query current position");
    // Query duration if player_state.duration was default value
    if player_state.duration == gst::CLOCK_TIME_NONE {
        player_state.duration = player_state
            .playbin
            .query_duration()
            .expect("Could not query current duration");
    }

さて、ここでは30秒から35秒までの間の部分を再生したいので、再生直後は0秒から開始するためシークを行ってあげる必要があります。シークをするには、単にseek_simpleをパイプラインに対して呼んであげれば大丈夫です。この中にたくさんの込み入ったことが隠されています。

rustで行う場合は第一引数に様々なシークのオプションを指定します。

  • gst::SeekFlags::FLUSH:このフラグを指定した場合にはシークを行う前にパイプライン中に入ったデータをすべて捨てます。新しいデータがパイプラインの中を通る間少しだけ固まりますが、アプリケーションの応答性能を向上します。このフラグが指定されていない場合は、シークされたあとに残ったデータが少しの間だけ表示され続けることになります。

  • gst::SeekFlags::KEY_UNIT:ほとんどの動画のストリームでは、任意の時間にシークすることはできず、特定のキーフレームと呼ばれるフレームにしかシークすることはできません。このフラグが用いられたときは一番シークする時間位置回キーフレームに移動し再生を開始します。もし、このフラグが指定されなかった場合には、内部的に一番近くのキーフレームまで移動し、指定したシークしたい時間に達するまで表示されず、時間に達したあとに表示されます。これが一番正確な方法ですがより時間がかかります。

  • gst::SeekFlags::ACCULATE: いくつかのメディア形式では正確なインデックス情報が含まれていないため、任意の時間へのシークを行うことはとても時間がかかります。このような場合にはGStreamerはシークをするタイミングを推測し、ほとんどの場合は正常に動作します。もし、この精度があまり良くない場合には、このフラグを与えてください。ただし、その場合にはシーク点の計算により長くの時間がかかります。

そして、第二引数に時間を与えます。gst::SECONDやgst::MSECONDなどの単位を用いることが可能です。

    // Peform a first seek because it begins from 0. I want to play 30s - 35s
    if !player_state.first_seek_done && player_state.seek_enabled {
        player_state
            .playbin
            .seek_simple(
                gst::SeekFlags::FLUSH | gst::SeekFlags::KEY_UNIT,
                30 * gst::SECOND,
            )
            .expect("Failed to seek");
        player_state.first_seek_done = true;
    }

Message Pump

handle_message関数はパイプラインのバスを通じて受信したすべてのメッセージを処理します。ErrorやEOSの場合は以前のチュートリアルと同様ですのでここでは割愛します。

DurationChangedはストリームの総時間が変わった際に送出されます。ここでは、単にステートに無効な値を入れ、次回メッセージがなかった際に更新されることを想定しています。

    gst::MessageView::DurationChanged(_) => {
        player_state.duration = gst::CLOCK_TIME_NONE;
    }

シークや時間に関するクエリは一般的にはPAUSEDもしくはPlayingのときしか有効ではありません。そのため、現状のステートを監視するために、ここでは単にステートにplaying中かどうかをトラックする変数を含め、変更された際に代入しています。

    gst::MessageView::StateChanged(state_changed) => {
        if state_changed
            .get_src()
            .map(|s| s == player_state.playbin)
            .unwrap_or(false)
        {
            let new_state = state_changed.get_current();
            let old_state = state_changed.get_old();

            println!(
                "Pipeline state changed from {:?} to {:?}",
                old_state, new_state
            );

            // If player state became first, we need to check that stream being seekable
            // Some streams can be unseekale.
            player_state.playing = new_state == gst::State::Playing;

また、Playing状態になったとき、このストリームでシークが有効なのかクエリをしています。クエリを行う際には、rustではgst::Query以下のクラスからクエリを表すクエリを生成します。ここでは、gst::Format::Timeを渡しているため、帰ってくる値は開始時間と終了時間、そもそもシーク可能かどうかが戻ってきます。gst::Format::Byteも渡すことができ、特定のバイト単位での動画データ上のオフセットを取得することができますが、ほとんどの場合は不要です。

このクエリオブジェクトは要素に含まれているqueryメソッドに渡されます。結果はこのクエリ自体に格納され、get_result()によって取得することができます。

    if player_state.playing {
        let mut seeking = gst::Query::new_seeking(gst::Format::Time);
        if player_state.playbin.query(&mut seeking) {
            let (seekable, start, end) = seeking.get_result();
            player_state.seek_enabled = seekable;
            if seekable {
                println!("Seeking is ENABLED from {:?} to {:?}", start, end)
            } else {
                println!("Seeking is DISABLED for this stream.")
            }
        } else {
            eprintln!("Seeking query failed.")
        }
    }

Conclusion

このチュートリアルでは以下を示しました。

  • パイプライン上の情報をどうやってクエリするか
  • よく使われるクエリである、positionやduration用の簡易メソッドを使ってどうやってクエリするか
  • どうやって好きな位置にシークを行うか
  • どのステートの際に、すべての操作が可能になるか

次回: https://qiita.com/kyasbal_1994/items/ce7d0d6e75fde1d1aff4

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?