初回: 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の際に動画上の現在の位置を取得し、全体の動画の長さとともに表示を行います。
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_position
とquery_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