Python GStreamer チュートリアル

④ パイプライン

チュートリアルの続き。(※ パイプ:前の出力を次の入力にするための仕組み)


Gst.Pipeline は、独自のバスとクロックを持つトップレベルの bin です。プログラムが bin-likeオブジェクトを1つだけ含む場合は、これを利用します。パイプラインオブジェクトは次のように作成します。

my_pipeline = Gst.Pipeline.new("my-pipeline")

パイプラインは、他のオブジェクトを置くことができる “コンテナ” です。 すべてが揃い、再生するファイルが指定されていれば、パイプラインの状態を Gst.State.PLAYING に設定すると、マルチメディアが出てくるはずです。

4.1 オーディオプレーヤのパイプライン

ここでの最初の例は、前節のオーディオプレイヤから始まります。playbin をMP3ストリームを扱える mad デコードパイプラインに切り替えます。Pythonでコーディングする前に、gst-launch を使ってテストできます。

gst-launch-1.0 filesrc location=file.mp3 ! mad ! audioconvert ! alsasink

概念的には、このパイプラインは次のようになります。

Gstreamer オーディオ・パイプライン

※ 筆者の環境では古いmp3デコーダである mad はインストールされていなかった。代わりのデコーダを使った例は以下の通り:

gst-launch-1.0 filesrc location=file.mp3 ! mpegaudioparse ! mpg123audiodec ! audioconvert ! audioresample ! autoaudiosink
Gstreamer オーディオ・パイプライン

Pythonで書くと、このようになります:

# pipeline-example.py

import sys, os
import gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
gi.require_version("Gst", "1.0")
from gi.repository import Gst, GObject

class GTK_Main(object):

    def __init__(self):
        window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
        window.set_title("MP3-Player")
        window.set_default_size(400, 200)
        window.connect("destroy", Gtk.main_quit, "WM destroy")
        vbox = Gtk.VBox()
        window.add(vbox)
        self.entry = Gtk.Entry()
        vbox.pack_start(self.entry, False, True, 0)
        self.button = Gtk.Button("Start")
        self.button.connect("clicked", self.start_stop)
        vbox.add(self.button)
        window.show_all()

        self.player = Gst.Pipeline.new("player")
        # Gst.ElementFactory.make ("エレメントの種類", "任意の名前")
        source = Gst.ElementFactory.make("filesrc", "file-source")
        parser = Gst.ElementFactory.make("mpegaudioparse", "audio-parse")
        decoder = Gst.ElementFactory.make("mpg123audiodec", "mp3-decoder")
        conv = Gst.ElementFactory.make("audioconvert", "converter")
        resample = Gst.ElementFactory.make("audioresample", "resamler")
        sink = Gst.ElementFactory.make("autoaudiosink", "audio-output")

        self.player.add(source)
        self.player.add(parser)
        self.player.add(decoder)
        self.player.add(conv)
        self.player.add(resample)
        self.player.add(sink)
        source.link(parser)
        parser.link(decoder)
        decoder.link(conv)
        conv.link(resample)
        resample.link(sink)

        bus = self.player.get_bus()
        bus.add_signal_watch()
        bus.connect("message", self.on_message)

    def start_stop(self, w):
        if self.button.get_label() == "Start":
            filepath = self.entry.get_text().strip()
            if os.path.isfile(filepath):
                filepath = os.path.realpath(filepath)
                self.button.set_label("Stop")
                self.player.get_by_name("file-source").set_property("location", filepath)
                self.player.set_state(Gst.State.PLAYING)
            else:
                self.player.set_state(Gst.State.NULL)
                self.button.set_label("Start")

    def on_message(self, bus, message):
        t = message.type
        if t == Gst.MessageType.EOS:
            self.player.set_state(Gst.State.NULL)
            self.button.set_label("Start")
        elif t == Gst.MessageType.ERROR:
            self.player.set_state(Gst.State.NULL)
            self.button.set_label("Start")
            err, debug = message.parse_error()
            print("Error: %s" % err, debug)

Gst.init(None)
GTK_Main()
GObject.threads_init()
Gtk.main()

※ Stopボタンが効かず、曲を終わらせるにはウィンドウを閉じるしかないけれども、とりあえずパイプラインが動けばいいということで。


4.2 パイプラインに動画を追加

次の例は、MPEG2 ビデオの再生です。mpegdemux のようないくつかのデマルチプレクサは、実行時に作成されるダイナミックパッドを使用しているため、実行時にパッドが作成される前にデマルチプレクサとパイプライン内の次の要素をリンクすることはできません。以下の demuxer_callback() メソッドに注意してください。

Gstreamer ビデオ・パイプライン

オリジナルのPythonコードは 4.2 Adding Video to the Pipeline を参照


ちゃんとわかってMP4!

Python3でMP4が再生できるようにコードを書き直したいです。ここから下はチュートリアルのオリジナルにはない部分です。

※「GStreamerのエレメントをつないでパイプラインを組み立てるには」を読んで、 パイプラインを組む時の手順をおぼえます。

  • mediainfoで動画の情報を確認する → format: MPEG-4 / video-format: AVC / audio-format: AAC
  • 動画フォーマットに対応したdemuxer(デマルチプレクサ)を調べる → gst-inspect-1.0 | grep mp4 → qtdemux: QuickTime demuxer
  • demuxerに対応するを調べる →
    • gst-inspect-1.0 | grep AVC | grep decoder → avdec_h264
    • gst-inspect-1.0 | grep AAC | grep decoder → avdec_aac
  • デコーダに対応するsinkを調べる ※実際のコーディングに必要?
    • gst-inspect-1.0 avdec_h264 → ケイパビリティがvideo/x-raw → video/x-rawに対応した表示のためのシンクエレメントを探せばよい → xvimagesink
    • gst-inspect-1.0 avdec_aac
gst-launch-1.0 filesrc location="path/to/file.mp4" ! qtdemux name=demux  demux.audio_0 ! queue ! decodebin ! audioconvert ! audioresample ! autoaudiosink   demux.video_0 ! queue ! decodebin ! videoconvert ! videoscale ! autovideosink
Gstreamer ビデオ・パイプライン

Atmark Techno, Inc.に日本語で Gstreamer の説明があったので縋るように読みました。

  • GStreamerは、オープンソースのマルチメディアフレームワークです。小さなコアライブラリに様々な機能をプラグインとして追加できるようになっており、多彩な形式のデータを扱うことができます。

  • GStreamerでは、マルチメディアデータをストリームとして扱います。ストリームを流すパイプラインの中に、エレメントと呼ばれる処理単位を格納し、それらをグラフ構造で繋ぎ合わせることで、デコードやエンコードなどの処理を行います。

  • 各エレメントは、データの入出力の口となる「パッド(pad)」を持っています。実際にエレメント同士を繋いでいるのはパッドです。パッドにはデータを次のエレメントに渡す「ソースパッド(source pad)」とデータを受け取る「シンクパッド(sink pad)」が存在します。

  • パッドは自分が出力可能な、または入力可能なフォーマットを知っています。これを「ケイパビリティー (Capability)」と言います。パッドは、パイプラインが作られる時に繋がる相手パッドが持っているケイパビリティーを確認します。お互いのケイバビリティーが一致しない場合はデータの受け渡しができませんので、エラーとなり、最終的にパイプライン生成自体がエラーとなります。

  • ソースパッドしか持たないエレメントを「ソースエレメント」、シンクパッドしか持たないエレメントを「シンクエレメント」と呼びます。左端にある「filesrc0」がソースエレメントで、右端の「alsasink0」「acmfbdevsink」の2つがシンクエレメントになります。マルチメディアデータは、一番左側のソースエレメントから右端のシンクエレメントに流れることで形を変えていき、最終的に動画や音声として再生されることになります。

  • ビデオ(映像)とオーディオ(音声)データを一つのファイルにまとめる(多重化する)ためのフォーマットをコンテナフォーマットと言います。H.264/AVCとAACを格納可能なコンテナフォーマットには、MP4(MPEG-4 Part 14)、AVI(Audio Video Interface)、Matroskaなどがあります。また、MPEG2 TS(MPEG-2 Transport Stream)やMPEG2 PS(MPEG-2 Program Stream)は、拡張規格でH.264/AVCとAACに対応しています。

  • GStreamerでは、コンテナに格納されたビデオやオーディオのデータを取り出す場合、デマルチプレクサエレメントを使用します。

  • MP4コンテナからビデオやオーディオを取り出す場合、qtdemuxエレメントを使用します。

  • qtdemuxエレメントは、MP4コンテナに格納されているデータストリームごとに動的にソースパッドを作成します。ビデオ用に動的に生成されたパッドにはvideo_Nという名前が、オーディオ用に生成されたパッドにはaudio_Nという名前がつきます。

  • GStreamerでは、エレメント同士を接続(リンク)する際に、お互いのソースパッド(出力)とシンクパッド(入力)が受け渡しできるフォーマット(ケイパビリティ)が一致するかネゴシエーションを行い、ケイパビリティが一致した場合だけパッドがリンクされます。queueエレメントはバッファリングを行うためのエレメントで、どのようなフォーマットのデータも受け渡しでき、シンクパッドとソースパッドのケイパビリティは同じものになります。そのため、qtdemuxが動的に生成したオーディオ用のパッドにはオーディオを扱うacmaacdecにリンクされているqueueエレメントのシンクパッドが、ビデオ用のパッドにはビデオを扱うacmh264decエレメントがリンクされているqueueエレメントのシンクパッドがリンクされます。

構成要素はなんとなくわかったかも… Pythonで書けるかな?

Gst.ElementFactory.makeで必要なエレメントを作ってリンクして、いざスタート…といっても、どうしても “streaming stopped, reason not-linked (-1)“というエラーが消えてくれない。お手上げです。

泣きながらググってみる。

Stack Overflowのここにヒントが出ていました。

The problem is, that demux has no source pads in the NULL state when you are trying to link it. Demux adds output pads in the PAUSED state since at this moment it starts processing the input file. Therefore you cannot simply link it at the beginning and then start. You must connect to the "on-pad-added" event of the demuxer with something like this:

BTW, gst-launch uses some smart machinery that supports such delayed linking. In code you must do it manually.

翻訳:

問題は、Demuxをリンクしようとしたときに、ソースパッドがNULL状態になっていないことです。この時点で入力ファイルの処理を開始するので、DemuxはPAUSED状態で出力パッドを追加します。そのため、単純に最初からリンクして起動することはできません。以下のように、Demuxerの "on-pad-added "イベントに接続する必要があります。

ところで、gst-launch はこのような遅延リンクをサポートする賢い仕組みを持っています。コードでは手動で行う必要があります。
void
on_pad_added (GstElement *element,
              GstPad     *pad,
              gpointer    data)
{
  GstPad *sinkpad;
  GstElement *queue = (GstElement *) data;

  g_print ("Dynamic pad created, linking demuxer/decoder\n");

  sinkpad = gst_element_get_static_pad (queue, "sink");

  gst_pad_link (pad, sinkpad);

  gst_object_unref (sinkpad);
}

cを読んでもすぐにPythonコードを書けない… (;_;)

よくわからないけれども、動いた

これを誰かに説明せよと言われてもできそうにない。とにかくGtkウィンドウの中で動きました。

# pipeline-branch-example.py

import os
import gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
gi.require_version("Gst", "1.0")
from gi.repository import Gst, GObject
# Needed for window.get_xid(), xvimagesink.set_window_handle(), respectively:
gi.require_version("GstVideo", "1.0")
from gi.repository import GdkX11, GstVideo


class GTK_Main(object):

    def __init__(self):

        # 表示物の準備
        # トップレベルのウィンドウ
        window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
        window.set_title("MP4-Player")
        window.set_default_size(500, 400)
        window.connect("destroy", Gtk.main_quit, "WM destroy")

        # 上下に並ぶボックス
        vbox = Gtk.VBox()
        window.add(vbox)

        # 水平に並ぶボックス
        hbox = Gtk.HBox()
        vbox.pack_start(hbox, False, False, 0)  # Vボックスに詰める

        # ファイル名を受け付ける1行入力
        self.entry = Gtk.Entry()
        self.entry.set_text("file.mp4")
        hbox.add(self.entry)
        # 入力欄の右側にボタンを置く
        self.button = Gtk.Button("Start")
        hbox.pack_start(self.button, False, False, 0)
        # クリックされたら start_stopメソッドを呼ぶ
        self.button.connect("clicked", self.start_stop)

        # 動画表示用のエリア
        self.movie_window = Gtk.DrawingArea()
        vbox.add(self.movie_window)

        # ウィジェットと子ウィジェット(ウィジェットがコンテナの場合)を再帰的に表示
        window.show_all()

        # パイプラインの準備
        # テスト用コマンド:gst-launch-1.0 filesrc location=file.mp4 ! qtdemux name=demux  demux.audio_0 ! queue ! decodebin ! audioconvert ! audioresample ! autoaudiosink   demux.video_0 ! queue ! decodebin ! videoconvert ! videoscale ! autovideosink

        # パイプライン新規作成
        self.player = Gst.Pipeline.new("player")
        source = Gst.ElementFactory.make("filesrc", "file-source")

        # デマルチプレクサ
        # https://gstreamer.freedesktop.org/documentation/isomp4/qtdemux.html?gi-language=python
        demuxer = Gst.ElementFactory.make("qtdemux", "demuxer")

        # on pad added
        demuxer.connect("pad-added", self.demuxer_callback)

        # キュー
        self.queuea = Gst.ElementFactory.make("queue", "queuea")
        self.queuev = Gst.ElementFactory.make("queue", "queuev")

        # デコーダ
        self.audio_decoder = Gst.ElementFactory.make("avdec_aac", "audio-decoder")
        self.video_decoder = Gst.ElementFactory.make("avdec_h264", "video-decoder")

        # コンバータ ← なくても動いた
        audioconv = Gst.ElementFactory.make("audioconvert", "audio-converter")
        videoconv = Gst.ElementFactory.make("videoconvert", "video-converter")

        # 追加のエレメント(audio resample, video scale)必要????
        #audioresample = Gst.ElementFactory.make("audioresample", "audio-resample")
        #colorspace = Gst.ElementFactory.make("videoconvert", "colorspace")

        # シンクエレメント
        audiosink = Gst.ElementFactory.make("autoaudiosink", "audio-output")
        videosink = Gst.ElementFactory.make("autovideosink", "video-output")

        # パイプラインにエレメントを追加
        self.player.add(source)
        self.player.add(demuxer)
        self.player.add(self.queuea)
        self.player.add(self.queuev)
        self.player.add(self.audio_decoder)
        self.player.add(self.video_decoder)
#       self.player.add(audioconv)
#       self.player.add(videoconv)
        self.player.add(audiosink)
        self.player.add(videosink)

        # 各エレメントをリンク
        source.link(demuxer)
        self.queuea.link(self.audio_decoder)
        self.queuev.link(self.video_decoder)

#       self.audio_decoder.link(audioconv)
#       self.video_decoder.link(videoconv)
#       audioconv.link(audiosink)
#       videoconv.link(videosink)

        self.audio_decoder.link(audiosink)
        self.video_decoder.link(videosink)

        # バス取得
        bus = self.player.get_bus()
        bus.add_signal_watch()
        bus.enable_sync_message_emission()
        bus.connect("message", self.on_message)
        bus.connect("sync-message::element", self.on_sync_message)

    def on_message(self, bus, message):
        t = message.type
        if t == Gst.MessageType.EOS:
            self.player.set_state(Gst.State.NULL)
            self.button.set_label("Start")
        elif t == Gst.MessageType.ERROR:
            err, debug = message.parse_error()
            print("Error: %s" % err, debug)
            self.player.set_state(Gst.State.NULL)
            self.button.set_label("Start")

    def on_sync_message(self, bus, message):
        if message.get_structure().get_name() == 'prepare-window-handle':
            imagesink = message.src
            imagesink.set_property("force-aspect-ratio", True)
            xid = self.movie_window.get_property('window').get_xid()
            imagesink.set_window_handle(xid)

    def demuxer_callback(self, demuxer, pad):
        if pad.get_property("template").name_template == "video_%u":
            self.qv_pad = self.queuev.get_static_pad("sink")
            pad.link(self.qv_pad)
        elif pad.get_property("template").name_template == "audio_%u":
            self.qa_pad = self.queuea.get_static_pad("sink")
            pad.link(self.qa_pad)

    def start_stop(self, w):
        if self.button.get_label() == "Start":
            filepath = self.entry.get_text().strip()
            if os.path.isfile(filepath):
                filepath = os.path.realpath(filepath)
                self.button.set_label("Stop")
                self.player.get_by_name("file-source").set_property("location", filepath)
                self.player.set_state(Gst.State.PLAYING)
            else:
                self.player.set_state(Gst.State.NULL)
                self.button.set_label("Start")


Gst.init(None)
GTK_Main()
GObject.threads_init()
Gtk.main()


関連記事