Friday, December 11, 2015

GStreamer でプログラミング 6 (Buffer と Memory)

GStreamer のパイプライン内を流れているのは GstEventGstBuffer ということでしたが、 実際にマルチメディアデーターを運んでいる方の GstBuffer を覗いてみましょう。

GstBuffer の構造体 は、

と定義されています。

APIドキュメントによると

バッファーは、GStreamerのデーターを運ぶ基本ユニットであり、タイミング情報や、オフセット、バッファーが持つ GstMemory ブロックに関係するメタデーターなどが含まれている。

と言うことらしいです。メタデーターはとりあえず置いといて、 GstMemory が気になりますね。だって、 struct GstBuffer の中に GstMemory なんて入っていないし、どうなってんでしょう? 答は GstBufferImpl にありました。

つまり、

こんな風に、Bridge パターンを使って GstBufferImpl の方で GstMemory を持っているんですね。

さて、 GstMemory の方に行ってみましょう。 GstMemoryGstAllocator によって確保されるメモリです。 GstAllocator は、「いろいろな条件に合ったメモリをよしなに確保してくれるアロケーター」とでも今は覚えておいてください。 gst_allocator_alloc() でアロケーターを指定しない、つまり NULL を指定すると、システムのデフォルトアロケーターが使われます。

Sorry, your browser does not support SVG.

確保できたメモリは、 malloc() されたメモリのように、そのまま扱うことができません。どのようなメモリが確保できたかは、利用したアロケーターによって異なります。もしかすると、ぜんぜんメモリとは異なる状態かもしれません。このようなデーターを 「Opaque data type」と言います。

どのような構造のメモリなのか分らないので、そのままではアクセスすることができません。そのため CPUから利用する場合は、かならず「アクセスするよ (map)」「終ったよ (unmap)」と指示する必要があります。それぞれ、 gst_memory_map()gst_memory_unmap() です。 組み込みやカーネルのコードを知っている人であれば、「メモリ空間にマップしたので、触れる」と言えば、感覚として分るでしょうか? または、ファイルを mmap() してメモリとして触るような感じでしょうか。

malloc() されるようなシステムメモリであれば、このような仕組みは不要です。しかし GStreamer はマルチメディアを扱うフレームワークです。ビデオ処理や OpenGL 用のメモリを確保する場合もあります。その時 GPU 側のメモリの方が都合が良いかもしれません。 CUDA 6 の Unified MemoryAMD の HSA の様に CPU から直接触れる GPU メモリも増えそうですが、実際のハードウェアや OS によって状況が異なります。そのため、メモリアクセスの前後で、必要な処理をするタイミングを作ってあげる必要があったのです。

それでは、早速 GstBufferGstMemory を使って実験してみましょう。次の 4つを試してみたいと思います。

  1. GstBuffer を確保
  2. GstMemory を確保して、"abc" と書き、GstBuffer に追加
  3. もう1つ GstMemory を確保して、"12345" と書き、GstBuffer に追加
  4. GstBuffer を確認

まずは、 GstBuffer です。

GstBuffer は、 gst_buffer_new() で取得します。 gst_buffer_new() で取得したときは、サイズは 0、持っている GstMemory ( nr ) も 0 です。

buf:
 size: 0
 maxsize: 0
 nr: 0

次は、 GstMemory を作成してみます。

GstMemory は、 gst_allocator_alloc() で取得します。この実験ではアロケーターとアロケーターへのパラメーターは気にしないので、NULL を指定します。確保するメモリは、3 byte にしてみましょう。これで確保できたメモリは、ちゃんと size が 3 になっています。 maxsize が 10 なのは、後で少し追加可能なように、ちょっと多めにメモリが確保されるようになっているからです。

このメモリに、3文字 "abc" と書き込みます1gst_memory_map()gst_memory_unmap() で括るのを忘れずに。これで最初に確保した GstMemory には "abc" と書かれました。では、このメモリを、先程確保した GstBuffer に追加してみましょう。

mem 1:
 size: 3
 maxsize: 10
buf:
 size: 3
 maxsize: 10
 nr: 1

ちゃんと、メモリブロックの数が 1 になりました。 GstBuffer が表示するサイズは、内包するメモリブロックのままのようです。

もう1つ、 GstMemory を確保します。

今度は、確保する領域を 6 byte にし、"12345" と書き込みます。今度は、NUL 文字も含め 6文字です。 今回の GstMemorymaxsize は、13 になるようです。

mem 2:
 size: 6
 maxsize: 13
buf:
 size: 9
 maxsize: 16
 nr: 2
abc12345

2つ目のメモリブロックが追加された GstBuffer の方は、どうでしょう? size は前回の 3 と 今回の 6 で 9 になり、 nr は 2 になっています。でも、 maxsize は 10 + 13 = 23 を期待していたのに、16となりました。なぜでしょう?

GstBuffer は、内部に持っているメモリブロックを列べ、あたかも連続したメモリ空間のように見せてくれます。その時 maxsize として見えるのは最後のメモリブロックの空き領域分だけのようです。これは、そういう仕様だと理解するしかないようです。

メモリバッファーと言えば、「リニアなメモリ空間」というシンプルな物を想像しますが、GStreamer では、とても複雑なことをしています。この GstMemory の機能は、長年続いた Gstreamer 0.x 系から、1.x系にするときに追加された機能です。たとえば YUV 4:2:0 のように LumaChrominance が空間として分れている場合、ハードウェアがバラバラのメモリ空間にデーターがあることを期待しているかもしれません。しかし CPU から触るときは、まとまったデーターとして触りたい。なので map すると内部にあるメモリが合わさって (mergeされて) 見える作りになっています。GStreamer は、さまざまなデーターを扱うため、どうしてもこの柔軟な機能が必要だったようです。

GStreamer を使っていないくても、不連続なメモリを使いたいことは良くあります。そんなとき使ってみたくなりませんか?

ぜひ、お試しあれ。

Footnotes:

1

実験のため、NUL文字を省いて書き込んでいます。そのため、文字列として扱うのは危険ですので注意してください。

Thursday, December 10, 2015

GStreamer でプログラミング 5 (Pad と Event)

パイプライン構造をしている GStreamer で、各エレメントを繋ぐ大事な役割をしているのが GstPad です。

cat の例 では、 filesrc エレメントと fdsink エレメントが繋がっています。 gst-launch の記法では、「 ! 」を使って、「左側のエレメントと右側のエレメントを繋ぐ」を表現しています。 Unix パイプの「 | 」と同じ使い方です。

linked-elements.png

Figure 1: GStreamer Application Development Manual の Linking elementsから抜粋

パイプラインの図を見ても分るように、各エレメントはいくつかのパッド(Pad)を持っていて、これらのパッドが繋がることでエレメントが繋がることができます。ある瞬間にひとつのパッドが繋がることができるのは、1つだけです。かならず 1対1の関係になります。

マニュアル では、 gst_element_link() を使って2つのエレメントを繋いで いますが、実はこの関数 gst_element_link_pads() のラッパー関数です。本来なら繋ぐのはパッド同士なので、「エレメントA のパッド1 を エレメント B のパッド2に繋げ」となるところですが、 gst_element_link() では、「Pad の名前は指定しないので、よしなに繋いでおいて」という感じでしょうか。

各エレメントがどのようなパッドを持つのかというのは、しっかりとドキュメント化されています。たとえば filesrc であれば、

name
src
direction
source
presence
always
details
ANY

という感じに。 filesrc は、方向(direction) が "source" のパッド1つしか持っていません。source 方向のパッドは下位 (downstream) にデーターを出すためのパッドです。 filesrc が「Source Element」と言われる所以です。名前(name)はそのまま "src"。Presence が "always" となっています。"always" なパッドは、エレメントが生成された後であれば、いつでも存在するという意味です。この様なパッドは、「Static pad」と呼ばれます。逆に必要にならないと存在しないパッドは、 「Dyanmic Pad」と呼ばれます。

次に、フィルターエレメントである tee も見てみましょう。

name
sink
direction
sink
presence
always
details
ANY
name
src_%u
direction
source
presence
request
details
ANY

フィルターエレメントは、上流と下流の両方で他のエレメントと繋がります。そのため、方向(direction)が "sink" と "source" の 2種類のパッドを持ちます。 tee は、上流から来たデーターを複製するエレメントです。そのため、下流に流す Sourceパッドを複数持ちます。それぞれのパッドには、0始まりのインデックス番号が振られます。"src_%u" と書かれているのはそのためです。また、"request" された時、つまり繋がるエレメントの数に合わせてパッドが生成されるので presence には "request" と記載されています。

その他パッドについては、マニュアルの「Pads and capabilities」が詳しいです。

さて、Pad 同士が繋っているなら、辿って行けば対になっているパッドが分るはずです。さらにパッドは自分を保持しているエレメントの事も知っています。それなら、端のエレメントから辿って行くことで、隣りのエレメントまで行けるはずですよね?

filesrc0 → src pad → sink pad → fdsink0

と、たどってみましょう。

Presense が "always" のパッドは、 gst_element_get_static_pad() で取り出します。名前は "src" でした。gst_pad_get_peer() という関数は、指定したパッドに繋っているパッドを返してくれます。取り出したパッドは fdsink0 の sink pad なはず。このパッドを持っているエレメント、つまり親のエレメントを gst_pad_get_parent_element() で取り出せば、ちゃんと名前が "fdsink0" となっているエレメントになりました。

./a.out
name: fdsink0
hello

もう1つパッドには重要な仕事があります。隣のエレメントにデーターを渡すという大事な仕事です。 GStreamer がパイプライン構造をしているのは、何らかの情報を流すためですもんね。パイプラインの中を流れるデーターは GstEventGstBuffer というオジェクトです。

パッドを眺めていると、流れているデーターを見ることができます。医療や検査で、ある場所を調査する道具を プローブ と言いますが、パッドにもプローブ(Probe)を付けて覗き見ることができます。その名も gst_pad_add_probe() 。必要に応じてプローブの種類を選べます。指定は、enum GstPadProbeType で行います。

ここでは例として、以前紹介した End of Stream を拾ってみましょう。End of Stream は Event Type なので、 GST_PAD_PROBE_TYPE_BUFFER_EVENT_DOWNSTREAM を指定します。

イベントが流れたときに、 on_probe() を呼び出してもらいます。 on_probe() には、 GstPadProbeInfo が渡ってくるので、この情報から event を取り出し、 GST_EVENT_TYPE_NAME() で種類を表示します。

ついでに on_message() も修正して、End of Stream のメッセージが来たら表示してみます。これで、パッド上をイベントが流れる方が先か、バスにメッセージが投げられるのが先か分りますね。実行すると、 stream-startsegment というイベントの後に、 eos が流れています。どうやらバスにメッセージが流れる方が後のようです。 on_message() の方が後なのは、 sink エレメントにまで EOS が届いてから、バスにメッセージが投げられるからです。

./a.out
name: fdsink0
event: stream-start
event: segment
hello
event: eos
message: eos

お試しあれ。

Wednesday, December 9, 2015

GStreamer でプログラミング 4 (Bin と Iterator)

もし本当に GStreamer の pipeline に入っているエレメントの名前が分らなかったら?

pipeline は自分が持っているエレメントを知っています。なので、pipeline に問い合わせて、持っているエレメントをイテレーターで貰うことができます。これ、pipeline の親クラスである GstBin の機能なんです。

Ruby のように Internal Iterator (内部イテレータ)かつ、Dynamic Typing (動的型付け) であれば

と書けるのですが、C で実装されている GStreamer では、ちょっと面倒ですね。

  1. Pipelineから外部イテレータを貰い ( gst_bin_iterate_elements() )
  2. 次のエレメントをイテレーターから貰います。( gst_iterator_next() )
  3. 貰ったエレメントは、実は GValue という「なんでも内包できる構造体」なので、これから実際のエレメントを取り出します。 ( g_value_get_object() )

gst_iterator_foreach() もあるのですが Lambda が使えない C では、関数を指定する必要があります。

GValue は、なんでも入れることができるという点で、「void*」のようなものですが、型情報を持っている点が異なります。型が必須なので必ず使う前に G_VALUE_INIT または g_value_init() で初期化してください。

GValue や GstElement は、Dynamic Typing を持っていない C のために実装された Glib Dynamic Type System を使っています。 GObject が基本クラスです。Ruby C API で言うところの、 VALUE ですね。

他のエレメントを内包できるエレメントは、イテレーターが使えることが多いので、一度やり方を覚えると便利です。お試しあれ。

Tuesday, December 8, 2015

GStreamer でプログラミング 3 (Element と Property)

GStreamer の pipeline には複数のエレメントが含まれているようですが、どうやってエレメントを取り出すんでしょうか? 自分でエレメントを作成し、pineline に追加した時は良いのですが、 gst_parse_launch() を使った場合は?

答は、 gst_bin_get_by_name() を使います。でも取り出したいエレメントの名前が分らない? 各エレメントのインスタンス名は、デフォルトで「"エレメント名" + "インデックス番号"」となるので簡単です。インデックスは、0 始まりなので filesrc を取り出すときは、 "filesrc0" を指定してあげれば良いです。パイプライン中で、同じエレメントの複数のインスタンスがある場合、順次番号が増えていきます。また、 queue2 のようにエレメント名が数字で終っている場合は、「"エレメント名" + "-" + "インデックス番号"」と間にハイフンが入ります。

前回のコードの filesrc を取り出すなら、こんな感じです。

gst-launch-1.0 -q \
videotestsrc ! tee name=t \
t. ! queue ! ximagesink \
t. ! queue ! ximagesink \
t. ! queue ! ximagesink \
t. ! queue ! ximagesink

よく、こんな風に name= となっているものを見かけます。 teedemux を使うときに使われることが多いようです。実はこれが、 gst-launch で名前を付ける方法です。 gst-launch の時も、名前を指定しなければ tee0 と指定できます。

gst-launch-1.0 -q \
videotestsrc ! tee \
tee0. ! queue ! ximagesink \
tee0. ! queue ! ximagesink \
tee0. ! queue ! ximagesink \
tee0. ! queue ! ximagesink

なので、この2つのコマンドは同じように動きます。ただし、同じエレメントを複数使った場合は紛らわしかったり、実装が変って違う名前が付くと動かなくなるので、明示的に指定しています。

さて、C に戻ります。

gst-launch-1.0 filesrc name=mysrc location=a.txt ! fdsink

これをそのまま gst_parse_launch() に渡し

pipeline = gst_parse_launch("filesrc name=mysrc location=a.txt ! fdsink", &error);

とすると、

src = gst_bin_get_by_name(GST_BIN(pipeline), "mysrc");

として取れるんですね。取れた filesrc エレメントの名前は、もちろん "mysrc" です。 gst_element_get_name() で確認できます。 GstElement の元になっている GstObjectname というプロパティがあるので、 GstElement から派生しているオジェクトであれば、みんな名前が取れます。 pipeline でも。お試しあれ。

Monday, December 7, 2015

GStreamer でプログラミング 2 (Bus と Message)

GStreamer を使って cat コマンドのようなプログラムを作成するには? gst-launch を使うと、こんな感じでしょうか。

echo hello > a.txt
gst-launch-1.0 filesrc location=a.txt ! fdsink
hello

C で書いても簡単で、こんな感じです。

これを 前回のプログラムと比較してみると、文字列部分を filesrcfdsink に変更しているだけだということが分ります。

しかし、このままでは 「hello」と表示された後、プログラムが終了せずに止ってしまいます。理由は、指定したファイルを最後まで読み出したにもかかわらず、 mainloop から抜けてこないからです。 glib の main loop から抜けるには、 g_main_loop_quit() を使いますが、どうやったら「最後まで読み出した」ということが分るのでしょう?

答は、Bus を見ると分ります。 Bus は、プログラムと pipeline がやりとりを行うためのオブジェクトです。pipeline になにかあったときには、かならず bus にメッセージ(GstMessage) が流れてきます。このメッセージを見てみましょう。

Bus は、 pipeline が持っています。 pipeline から gst_element_get_bus() を使って bus を取り出します。 取り出した bus に、「なにか変化があったらこの関数を呼んで」という gst_bus_add_watch() で、 on_message() を登録しておきます。

on_message() では、届いたメッセージによって適切な処理をさせます。今回のように「ファイルを最後まで読み出した」時、 Bus には End of Stream (EOS) が届くはずですので、EOS が届いたら、 g_main_loop_quit() を呼び mainloop から抜けるように指示します。

これで、hello と表示した後にプログラムが終了するようになります。

本物の cat は、複数のファイルを引数に取って concat するコマンドですが、 filesrc では複数のファイルを扱えません。 multifilesrcsplitfilesrc というエレメントもありますが、若干挙動が異なります。 cat と同じよう動作をさせる方法は、また今度紹介します。

Sunday, December 6, 2015

GStreamer でプログラミング 1 (Pipeline と Parse )

GStreamerの Advent Calendarに誘われたので、ちょっとだけ書いてみます。

GStreamer は、gst-launch というデバッグ用ツールでも簡単にパイプラインを構成できて、とても便利です。しかし、GStreamerの本当の力は、ライブラリーを使ってプログラムを書いたときに見えてくると思います。そこで、このシリーズではマルチメディアデータを使わずに、GStreamer の素晴らしさを紹介してみたいと思います。

まずは、「CoreElements について」でも紹介されている、 videotestsrc を使った例をコードにしてみます。

gst-launch-1.0 videotestsrc ! autovideosink

と同じことをするコードです。

結構簡単に C のコードになりそうです。

  1. gst_init() で、初期化
  2. main loop 作成
  3. pipeline 作成
  4. 再生
  5. main loop入る

で、 gst-launch 版と同じような動作をするプログラムが書けました。