Paepoi

Paepoi » PyGObject Tips » GStreamer, ClutterGst で動画再生

GStreamer, ClutterGst で動画再生

# 最終更新日 2019.09.14

ClutterGst を使う方法の追記
古い手段も残すけど非推奨コードの書き換え

端末にて実験

GStreamer: open source multimedia framework
を利用した動画プレイヤーの作り方。
GNOME の人は Nautilus のサムネイルにも使われるのでお馴染みです。

Ubuntu の御仁はインストール時に入るでしょうけど、とにかく GStreamer デコーダーを入れてください。
デコーダーが入っている動画形式であれば Totem で再生可能かつ Nautilus でサムネイルできるはず。

その環境を構築した状態で以下のようなコマンドを打ち込んでみる。
動画ファイルの URI は自分の手持ちファイルに変更してください。
$ gst-launch-1.0 playbin uri=file:///home/sasakima-nao/movie/katana.mp4
DirectShow の IVideoWindow みたいなのが出てきて指定したファイルが再生できたはずです。
GStreamer は GNOME の様々な場面で利用されるため部品として提供されています。
つまり GStreamer で再生できる環境があるなら動画プレイヤーを作成する環境は揃っているということ。

ClutterGst

Clutter Gst 3.0.4 Reference Manual: Clutter Gst 3.0.4 Reference Manual

筆者が Y901x でやっている ClutterGst を使う方法。
多分ソースを見てもわからない人のほうが多いと思うので最小限の抜き出し。
細かい制御は Y901x のソースで、JavaScript だけど Python への変換は結構簡単。
#!/usr/bin/env python3

import sys, re, gi
gi.require_version('Gtk', '3.0')
gi.require_version('Clutter', '1.0')
gi.require_version('GtkClutter', '1.0')
gi.require_version('ClutterGst', '3.0')
from gi.repository import Gtk, Clutter, GtkClutter, ClutterGst

class Win(Gtk.ApplicationWindow):
    '''
        Wayland, X.org 両方で動かすならコレが一番簡単
        GtkClutter.Embed を使って Clutter を上に載せて使います
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py')
        # var
        self.src_width = 0
        self.src_height = 0
        #
        self.player = ClutterGst.Playback()
        self.player.connect('eos', self.on_eos)
        self.player.connect('ready', self.on_ready)
        self.content = ClutterGst.Content()
        self.content.set_player(self.player)
        self.actor = Clutter.Actor()
        self.actor.set_background_color(Clutter.Color.new(0, 0, 0, 255))
        self.actor.set_content(self.content)
        embed = GtkClutter.Embed()
        self.stage = embed.get_stage()
        self.stage.add_child(self.actor)
        self.stage.set_background_color(Clutter.Color.new(0, 0, 0, 255))
        self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.vbox.connect('size-allocate', self.on_box_size)
        self.vbox.pack_start(embed, True, True, 0)
        self.add(self.vbox)
        self.drag_dest_add_uri_targets()
        self.show_all()

    def do_drag_data_received(self, context, x, y, data, info, time):
        uris = data.get_uris()
        self.player.set_uri(uris[0])

    def on_eos(self, player):
        pass

    def on_ready(self, player):
        vsink = player.get_video_sink();
        frame = vsink.get_frame()
        self.src_width = frame.resolution.width
        self.src_height = frame.resolution.height
        allocation = self.vbox.get_allocation()
        self.set_video_size(allocation)
        player.set_playing(True)

    def on_box_size(self, widget, allocation):
        if self.src_width != 0:
            self.set_video_size(allocation)

    def set_video_size(self, allocation):
        aw = allocation.width
        ah = allocation.height
        w = self.src_width
        h = self.src_height
        if aw * h > ah * w:
            width = w * ah / h
            height = ah
            x = (aw - width) / 2
            y = 0
        else:
            width = aw
            height = h * aw / w
            x = 0
            y = (ah - height) / 2
        self.actor.set_position(x, y)
        self.actor.set_size(width, height)

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)
        GtkClutter.init()
        ClutterGst.init()

    def do_startup(self):
        Gtk.Application.do_startup(self)
        Win(self)

    def do_activate(self):
        self.props.active_window.present()

app = App()
app.run(sys.argv)

古い手段

下記コードは Wayland では動きません。
C 言語の Wayland でのサンプルコードを見つけたので貼っておきます。
gst-wayland-gtk-demo.git - Unnamed repository; edit this file 'description' to name the repository.

以下は X.org 専用ですが参考にはなるのでしばらく残しておきます。

Novacut/GStreamer1.0 - Ubuntu Wiki
~jderose/+junk/gst-examples

Gst.ElementFactory.make() で playbin というパイプラインを作成。
DirectShow の IGraphBuilder のように面倒なことはコイツに「おまかせ」できるようだ。

get_bus() でパイプラインの gst.Bus オブジェクトを取得。

add_signal_watch() でメッセージを発行するように指定。

enable_sync_message_emission() はよく解らないんですけど…
映像と音声の同期メッセージを放出するように指示する関数っぽい。

そして GtkDrawingArea に貼り付ける。
一旦表示した所で xid を保存してソレを imagesink に割り当てます。

#!/usr/bin/env python3

import sys, gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gst', '1.0')
gi.require_version('GstAudio', '1.0')
gi.require_version('GstVideo', '1.0')
from gi.repository import Gtk, GObject, Gst, GstAudio, Gdk, GdkX11, GstVideo

class Win(Gtk.ApplicationWindow):
    '''
        動画ファイルをドロップすると再生する最小限のプレイヤー
        X.org 状態でないと動かない
    '''
    def __init__(self, app):
        '''
            DnD で再生するだけの処理
        '''
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py')
        # GStreamer を使う最小限の処理
        self.player = Gst.ElementFactory.make('playbin', None)
        bus = self.player.get_bus()
        bus.add_signal_watch()
        bus.enable_sync_message_emission()
        bus.connect('message', self.gst_on_message)
        bus.connect('sync-message::element', self.gst_on_sync_message)
        # この DrawingArea に動画を貼り付ける
        self.video_window = Gtk.DrawingArea()
        self.add(self.video_window)
        # DnD
        self.drag_dest_add_uri_targets()  
        self.show_all()
        # xid は show() の後に得る、Wayland は X11 ではないので xid が存在しない
        self.xid = self.video_window.props.window.get_xid()

    def set_uri(self, uri):
        '''
            playbin に uri をセット
            一旦 NULL にしないと失敗する
        '''
        self.player.set_state(Gst.State.NULL)
        self.player.props.uri = uri
        self.player.set_state(Gst.State.PLAYING)
        # W バッファリングを無効にする
        #self.video_window.set_double_buffered(False) # 廃止

    def do_delete_event(self, event):
        '''
            [閉じる] ボタンが押された
        '''
        self.player.set_state(Gst.State.NULL)
        return False

    def do_drag_data_received(self, drag_context, x, y, data, info, time):
        '''
            ファイルがドロップされた
        '''
        drop = data.get_uris()[0]
        self.set_uri(drop)

    def gst_on_message(self, bus, message):
        '''
            GStreamer から飛んでくるシグナルのハンドラ
        '''
        t = message.type
        if t == Gst.MessageType.EOS:
            self.player.set_state(Gst.State.NULL)
        elif t == Gst.MessageType.ERROR:
            self.player.set_state(Gst.State.NULL)
	
    def gst_on_sync_message(self, bus, message):
        '''
            動画画面を GtkDrawingArea に貼り付ける
        '''
        if message.get_structure().get_name() == 'prepare-window-handle':
            imagesink = message.src
            imagesink.set_window_handle(self.xid)

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)
        #GObject.threads_init() # 廃止
        Gst.init()

    def do_startup(self):
        Gtk.Application.do_startup(self)
        Win(self)

    def do_activate(self):
        self.props.active_window.present()

app = App()
app.run(sys.argv)
Copyright(C) sasakima-nao All rights reserved 2002 --- 2020.