Gtkビデオ再生

私の文字起こしツール:原型

動画の文字起こしをするための自分用ツール、mojiokosher.py。

当初はGtkウィンドウの中に動画再生とテキストエディタのウィジェットを持たせるつもりでしたが、テキスト編集は既成のエディタでよさそうなので、ビデオを再生することにフォーカスします。


そのまま使えるソースを見つけたのでコメント部分を翻訳。

ボタン画像とタイムオーバーレイを追加して再生してみる。

Gtkビデオ再生
# mojiokosher.py
# 私の文字起こしツール
# https://github.com/gkralik/python-gst-tutorial/blob/master/basic-tutorial-5.py
# ↑このソースを若干変更

import sys
import gi
gi.require_version("Gst", "1.0")
gi.require_version("Gtk", "3.0")
gi.require_version("GdkX11", '3.0')
gi.require_version("GstVideo", "1.0")
from gi.repository import Gst, Gtk, GLib, GdkX11, GstVideo

wrkDir = "/path/to/videofile/"
videoFile = "Sintel - Wikipedia.webm"

# パラメータとして"object"を持つ理由は?
# Python2ではクラスをNew-Styleクラスとして宣言するため(Classicクラスと対照して)
# Python3では全てのクラスがNew-Styleなので不要

class Player(object):

    def __init__(self):
        # GTK初期化
        Gtk.init(sys.argv)

        # GStreamer初期化
        Gst.init(sys.argv)

        self.state = Gst.State.NULL
        self.duration = Gst.CLOCK_TIME_NONE
        self.playbin = Gst.ElementFactory.make("playbin", "playbin")
        if not self.playbin:
            print("ERROR: playbinを作成できませんでした。")
            sys.exit(1)

        # URI設定
        self.playbin.set_property(
            "uri", "file://" + wrkDir + videoFile)

        # 対象信号とハンドラの接続
        self.playbin.connect("video-tags-changed", self.on_tags_changed)
        self.playbin.connect("audio-tags-changed", self.on_tags_changed)
        self.playbin.connect("text-tags-changed", self.on_tags_changed)

        # GUI作成
        self.build_ui()

        # 受信したメッセージごとに信号を出すようにバスに指示、対象信号とハンドラの接続
        bus = self.playbin.get_bus()
        bus.add_signal_watch()
        bus.connect("message::error", self.on_error)
        bus.connect("message::eos", self.on_eos)
        bus.connect("message::state-changed", self.on_state_changed)
        bus.connect("message::application", self.on_application_message)


    # playbinの状態をPLAYING(再生開始)に設定、リフレッシュコールバックを登録
    # GTK メインループ開始
    def start(self):
        # 再生開始
        ret = self.playbin.set_state(Gst.State.PLAYING)
        if ret == Gst.StateChangeReturn.FAILURE:
            print("ERROR: パイプラインを再生状態に設定できません")
            sys.exit(1)

        # GLibが1秒ごとに呼び出す関数を登録する
        GLib.timeout_add_seconds(1, self.refresh_ui)

        # GTKメインループ開始
        # 制御は Gtk.main_quit() が呼び出されるまで回復しない
        Gtk.main()

        # リソースを解放
        self.cleanup()

    # playbinの状態をNULLに設定し、参照を削除
    def cleanup(self):
        if self.playbin:
            self.playbin.set_state(Gst.State.NULL)
            self.playbin = None

    def build_ui(self):
        main_window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
        main_window.connect("delete-event", self.on_delete_event)
        main_window.set_default_size(1200, 900)

        video_window = Gtk.DrawingArea.new()
        video_window.set_double_buffered(False)
        video_window.connect("realize", self.on_realize)
        video_window.connect("draw", self.on_draw)

        # ボタン作成
        play_button = Gtk.Button()
        pause_button = Gtk.Button()
        stop_button = Gtk.Button()

        # ボタンの画像 GTK+ライブラリが提供するビルトイン画像を使う
        self.play_image = Gtk.Image.new_from_icon_name( "gtk-media-play", Gtk.IconSize.MENU )
        self.pause_image = Gtk.Image.new_from_icon_name( "gtk-media-pause", Gtk.IconSize.MENU )
        self.stop_image = Gtk.Image.new_from_icon_name( "gtk-media-stop", Gtk.IconSize.MENU )

        # ボタンに画像を設定
        play_button.set_image(self.play_image)
        pause_button.set_image(self.pause_image)
        stop_button.set_image(self.stop_image)

        # それぞれのボタンの"clicked"シグナルにハンドラを接続
        play_button.connect("clicked", self.on_play)
        pause_button.connect("clicked", self.on_pause)
        stop_button.connect("clicked", self.on_stop)

        # スライダ
        self.slider = Gtk.HScale.new_with_range(0, 100, 1)
        self.slider.set_draw_value(False)
        self.slider_update_signal_id = self.slider.connect(
            "value-changed", self.on_slider_changed)

        self.streams_list = Gtk.TextView.new()
        self.streams_list.set_editable(False)

        controls = Gtk.HBox.new(False, 0)
        controls.pack_start(play_button, False, False, 2)
        controls.pack_start(pause_button, False, False, 2)
        controls.pack_start(stop_button, False, False, 2)
        controls.pack_start(self.slider, True, True, 0)

        main_hbox = Gtk.HBox.new(False, 0)
        main_hbox.pack_start(video_window, True, True, 0)
        main_hbox.pack_start(self.streams_list, False, False, 2)

        main_box = Gtk.VBox.new(False, 0)
        main_box.pack_start(main_hbox, True, True, 0)
        main_box.pack_start(controls, False, False, 0)

        main_window.add(main_box)
        main_window.set_default_size(640, 480)
        main_window.show_all()

    # GUIツールキットが動画を保持する物理ウィンドウを作成したときに呼び出される
    # この時点でハンドラを取得し、XOverlayインターフェイスを介してGStreamerに渡す
    def on_realize(self, widget):
        window = widget.get_window()
        window_handle = window.get_xid()

        # XOverlayを実装しているplaybinにwindow_handleを渡し、ビデオシンクに転送
        self.playbin.set_window_handle(window_handle)

        # タイムオーバーレイ
        bin = Gst.Bin.new("my-bin")
        timeoverlay = Gst.ElementFactory.make("timeoverlay")
        bin.add(timeoverlay)
        pad = timeoverlay.get_static_pad("video_sink")
        ghostpad = Gst.GhostPad.new("sink", pad)
        bin.add_pad(ghostpad)
        videosink = Gst.ElementFactory.make("autovideosink")
        bin.add(videosink)
        timeoverlay.link(videosink)
        self.playbin.set_property("video-sink", bin)

    # PLAYボタンのクリックで呼び出される
    def on_play(self, button):
        self.playbin.set_state(Gst.State.PLAYING)
        pass

    # PAUSEボタンのクリックで呼び出される
    def on_pause(self, button):
        self.playbin.set_state(Gst.State.PAUSED)
        pass

    # STOPボタンのクリックで呼び出される
    def on_stop(self, button):
        self.playbin.set_state(Gst.State.READY)
        pass

    # メインウィンドウが閉じた時に呼び出される
    def on_delete_event(self, widget, event):
        self.on_stop(None)
        Gtk.main_quit()

    # ビデオウィンドウを再描画する必要があるたびに呼び出される
    # GStreamerは「PAUSED」「PLAYING」の状態でこの処理を行う
    # 他の状態では、ゴミが表示されないように黒い四角形を描く
    def on_draw(self, widget, cr):
        if self.state < Gst.State.PAUSED:
            allocation = widget.get_allocation()

            cr.set_source_rgb(0, 0, 0)
            cr.rectangle(0, 0, allocation.width, allocation.height)
            cr.fill()

        return False

    # スライダーの位置が変わったときに呼び出される
    # 新しい位置へのシークを行う
    def on_slider_changed(self, range):
        value = self.slider.get_value()
        self.playbin.seek_simple(Gst.Format.TIME,
                                 Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                                 value * Gst.SECOND)

    # GUIをリフレッシュするために定期的に呼び出される
    def refresh_ui(self):
        current = -1

        # 「PAUSED」または「PLAYING」の状態になっていない限り、何も更新しない
        if self.state < Gst.State.PAUSED:
            return True

        # まだわからない場合はストリームの持続時間を問い合わせる
        if self.duration == Gst.CLOCK_TIME_NONE:
            ret, self.duration = self.playbin.query_duration(Gst.Format.TIME)
            if not ret:
                print("ERROR: 現在の持続時間を照会できませんでした")
            else:
                # スライダーの範囲をクリップの持続時間に設定する
                self.slider.set_range(0, self.duration / Gst.SECOND)

        ret, current = self.playbin.query_position(Gst.Format.TIME)
        if ret:
            # value-changedシグナルをブロックして、on_slider_changedコールバックを
            # 呼び出さないようにする(この関数はユーザがリクエストしていないシークの
            # 引き金になるので)
            self.slider.handler_block(self.slider_update_signal_id)

            # スライダをパイプライン内の現在位置に設定(秒単位)
            self.slider.set_value(current / Gst.SECOND)

            # シグナルを有効にしなおす
            self.slider.handler_unblock(self.slider_update_signal_id)

        return True

    # この関数はストリーム内に新しいメタデータが発見されたときに呼び出される
    def on_tags_changed(self, playbin, stream):
        # GStreamerの作業スレッドにいる可能性があるので
        # バス内のメッセージでこのイベントをメインスレッドに通知する
        self.playbin.post_message(
            Gst.Message.new_application(
                self.playbin,
                Gst.Structure.new_empty("tags-changed")))

    # この関数はバスにエラーメッセージが表示されたときに呼び出される
    def on_error(self, bus, msg):
        err, dbg = msg.parse_error()
        print("ERROR:", msg.src.get_name(), ":", err.message)
        if dbg:
            print("Debug info:", dbg)

    # この関数はバス上にEnd-Of-Streamメッセージがポストされた時に呼び出される
    # パイプラインを単純にREADYに設定するだけ (再生を停止する)
    def on_eos(self, bus, msg):
        print("ストリームの終わりに達しました")
        self.playbin.set_state(Gst.State.READY)

    # この関数はパイプラインの状態が変化したときに呼び出される
    # 現在の状態を追跡するために使用
    def on_state_changed(self, bus, msg):
        old, new, pending = msg.parse_state_changed()
        if not msg.src == self.playbin:
            # playbinからのメッセージではないので無視
            return

        self.state = new
        print("状態が変化:from {0} to {1}".format(
            Gst.Element.state_get_name(old), Gst.Element.state_get_name(new)))

        if old == Gst.State.READY and new == Gst.State.PAUSED:
            # 応答性を高めるために、PAUSED状態になったら
            # すぐにGUIをリフレッシュする
            self.refresh_ui()

    # すべてのストリームからメタデータを抽出し、GUIのテキストウィジェットに
    # 書き込む
    def analyze_streams(self):
        # ウィジェットの現在のコンテンツをクリア
        buffer = self.streams_list.get_buffer()
        buffer.set_text("")

        # プロパティを読み取る
        nr_video = self.playbin.get_property("n-video")
        nr_audio = self.playbin.get_property("n-audio")
        nr_text = self.playbin.get_property("n-text")

        for i in range(nr_video):
            tags = None
            # ストリームのビデオタグを取得
            tags = self.playbin.emit("get-video-tags", i)
            if tags:
                buffer.insert_at_cursor("video stream {0}\n".format(i))
                _, str = tags.get_string(Gst.TAG_VIDEO_CODEC)
                buffer.insert_at_cursor(
                    "  codec: {0}\n".format(
                        str or "unknown"))

        for i in range(nr_audio):
            tags = None
            # ストリームのオーディオタグを取得
            tags = self.playbin.emit("get-audio-tags", i)
            if tags:
                buffer.insert_at_cursor("\naudio stream {0}\n".format(i))
                ret, str = tags.get_string(Gst.TAG_AUDIO_CODEC)
                if ret:
                    buffer.insert_at_cursor(
                        "  codec: {0}\n".format(
                            str or "unknown"))

                ret, str = tags.get_string(Gst.TAG_LANGUAGE_CODE)
                if ret:
                    buffer.insert_at_cursor(
                        "  language: {0}\n".format(
                            str or "unknown"))

                ret, str = tags.get_uint(Gst.TAG_BITRATE)
                if ret:
                    buffer.insert_at_cursor(
                        "  bitrate: {0}\n".format(
                            str or "unknown"))

        for i in range(nr_text):
            tags = None
            # ストリームの字幕タグを取得
            tags = self.playbin.emit("get-text-tags", i)
            if tags:
                buffer.insert_at_cursor("\nsubtitle stream {0}\n".format(i))
                ret, str = tags.get_string(Gst.TAG_LANGUAGE_CODE)
                if ret:
                    buffer.insert_at_cursor(
                        "  language: {0}\n".format(
                            str or "unknown"))

    # この関数は "application "メッセージがバスにポストされた時に呼び出される
    # ここではon_tags_changedコールバックによってポストされたメッセージを取得する
    def on_application_message(self, bus, msg):
        if msg.get_structure().get_name() == "tags-changed":
            # メッセージが "tags-changed "の場合は、GUIのストリーム情報を更新
            self.analyze_streams()

if __name__ == '__main__':
    p = Player()
    p.start()

Python 

関連記事