GTK+でメディアプレーヤーを作る①

作って使う文字起こしツール

動画から文字起こしをするためのプログラムを Python GTK+ で作りたいです。そのプログラムに必須の機能は以下の2つです。

  • ビデオクリップを再生できること
  • テキスト・エディタを兼ね備えていること

まず、動画が再生できるプログラムを作る(というか、世の中の達人のコードを拝借する)ところから始めましょう。


ちょうどぴったりのチュートリアルがあったので、これを真似します。

How to Build a Python Media Player using LibVLC and GTK+

これから書くプログラムに名前をつけます。「文字起こしをする者」+「Publisherのシャーの部分」で モジオコシャー と呼ぶことにします。スクリプト名は mojiokosher.py です。


① アプリケーション・ウィンドウ

GTK+ で入れ物を作り、VLCライブラリで動画を再生します。関係のある基礎知識をざっくりおさらいしておきます。

イベントとシグナルについて


  • GTK+はイベントドリブンのシステムです。全てのGUIアプリケーションはイベントドリブンです。
  • アプリケーションは、新しく生成されたイベントを継続的にチェックするメインループで開始します。イベントがない場合、アプリケーションは何もせずに待機します。
  • GTK+では、イベントとはXサーバーからのメッセージです。イベントがウィジェットに到達すると、ウィジェットはシグナルを発することでこのイベントに反応します。
  • GTK+プログラマは、特定のコールバックをシグナルに接続することができます。コールバックとはシグナルに反応するハンドラ関数です。
  • ハンドラ関数は2つのパラメータを受け取ります。最初のパラメータは信号を発したオブジェクトで、例えば再生/一時停止ボタンです。2番目のパラメータはオプションで、シグナルが発射されたときに生成されるイベントやデータを送信することができます。例えばデータタイプとして Gdk.EventTypeも可能です。

メディアのレンダリング面について

画面上に描画するには、DrawingArea ウィジェットを使用します。描画エリアウィジェットは基本的にはXウィンドウであり、それ以上のものではありません。これは何もないキャンバスで、好きなものを描くことができます。描画エリアは以下の呼び出しで作成されます。

すべてのウィジェットに当てはまることですが、デフォルトのサイズは set_size_request_を呼び出すことで上書きできます。また、ユーザが手動で描画領域を含むウィンドウのサイズを変更した場合も、サイズは上書きされます。

self.draw_area = Gtk.DrawingArea()
self.draw_area.set_size_request(300,300)

注意: このチュートリアルでは、GtkDrawingArea におけるユーザからの入力シグナルは扱いません。マウスのような入力デバイスからのイベントをキャプチャするためには、これらのウィジェットとして EventBox ウィジェットを使う必要があります。

レンダリング領域をそれの持つ"realize"シグナルに接続します。"realize"シグナルとは、ウィジェットが特定のディスプレイ上でインスタンス化されたときに必要なアクションを取るためのものです。 このチュートリアルで後ほど、_realizeハンドラについて説明します。

self.draw_area.connect("realize",self._realized)

Gtk.Boxウィジェットについて

LibVLC について説明する前に、ウィジェットを Gtk Containers と呼ばれる Gtk.Box ウィジェットに入れてみましょう。

ボックスとは何でしょうか? ボックスは目に見えないコンテナで、ウィジェットを詰め込むことができます。そして、ウィジェットを横長のボックスに詰める際には、Gtk.Box.pack_start() と Gtk.Box.pack_end() のどちらが使われているかによって、オブジェクトは左から右へ、または右から左へと水平に挿入されます。

縦長のボックスでは、ウィジェットは上から下に、またはその逆に詰められています。他のボックスの内側や横にあるボックスを自由に組み合わせて、目的の効果を生み出すことができます。

水平方向に配置されたボックスコンテナを作成し、子の間を6ピクセル空けます。ここでは、子は2つのメディアコントロールボタンです。

ボックスの左端からパックしていきます。パックスタートには4つのパラメータが必要です。

  1. ボックスに追加する GtkWidget。
  2. 拡張の有無の論理値:新しい子ボックスに拡張されたスペースを割り当てる場合に True を指定します。
  3. フィルの有無の論理値:拡張オプションで子に与えられたスペースが、単なるパディングではなく実際に子に割り当てられているかどうかを True で指定します。
  4. パディング:"spacing" プロパティで指定されたグローバルな量以上の、この子とその隣の子の間に置くピクセル単位の余分なスペース。

self.hbox = Gtk.Box(spacing=6)
self.hbox.pack_start(self.playback_button, True, True, 0)
self.hbox.pack_start(self.stop_button, True, True, 0)

同様に、トップレベルのウィンドウの子として垂直方向のボックスを作成します。次に、まずレンダリングサーフェスを、それから水平方向のボックスをパックします。

self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.add(self.vbox)
self.vbox.pack_start(self.draw_area, True, True, 0)
self.vbox.pack_start(self.hbox, False, False, 0)

ウィンドウと任意の子ウィジェットを再帰的に表示します。

self.show_all()

VLC周りはわからないままにサンプルコードを実行してみます。

# mojiokosher.py
# 作って使う文字起こしツール

import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
gi.require_version('GdkX11', '3.0')
from gi.repository import GdkX11

import vlc

MRL = ""

# 他のウィジェットを包含するためのトップレベルのウィンドウ、ApplicationWindowクラスを
# Gtk.Windowのサブクラスとして定義する
class ApplicationWindow(Gtk.Window):

    # クラスのコンストラクタで、スーパークラスのコンストラクタを呼び出し
    # プロパティタイトルの値を"モジオコシャー:私の文字起こしツール"に設定する
    def __init__(self):
        Gtk.Window.__init__(self, title="モジオコシャー:私の文字起こしツール")
        self.player_paused=False
        self.is_player_active = False
        self.connect("destroy",Gtk.main_quit)

    def show(self):
        self.show_all()

    def setup_objects_and_events(self):
        # コントロールボタン
        self.playback_button = Gtk.Button()
        self.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
            )

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

        # ボタンを"clicked"シグナルに接続する
        self.playback_button.connect("clicked", self.toggle_player_playback)
        self.stop_button.connect("clicked", self.stop_player)

        # 描画エリアを設定
        self.draw_area = Gtk.DrawingArea()
        self.draw_area.set_size_request(300,300)

        # 描画エリアと"realize"シグナルを接続
        self.draw_area.connect("realize",self._realized)

        # 水平方向のボックスを加え、再生・停止のボタンをパックする
        self.hbox = Gtk.Box(spacing=6)
        self.hbox.pack_start(self.playback_button, True, True, 0)
        self.hbox.pack_start(self.stop_button, True, True, 0)

        # 垂直方向のボックスを加え、描画エリアとボタンが水平にならぶボックスをパックする
        self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.add(self.vbox)
        self.vbox.pack_start(self.draw_area, True, True, 0)
        self.vbox.pack_start(self.hbox, False, False, 0)

    # 関数 stop_player は、メディアの再生を停止し、
    # 再生ボタンの画像を「再生」に設定する
    def stop_player(self, widget, data=None):
         self.player.stop()
         self.is_player_active = False
         self.playback_button.set_image(self.play_image)

    # 関数 toggle_player_playback は、再生/一時停止オプションの切り替えを処理する
    # playback_button の画像も適宜設定
    def toggle_player_playback(self, widget, data=None):
        """
        プレイヤーの再生ボタン(再生/一時停止)のハンドラです。
        """
        if self.is_player_active == False and self.player_paused == False:
            self.player.play()
            self.playback_button.set_image(self.pause_image)
            self.is_player_active = True
        elif self.is_player_active == True and self.player_paused == True:
            self.player.play()
            self.playback_button.set_image(self.pause_image)
            self.player_paused = False
        elif self.is_player_active == True and self.player_paused == False:
            self.player.pause()
            self.playback_button.set_image(self.play_image)
            self.player_paused = True
        else:
            pass

    # 描画エリアが実現された時に実行される関数
    def _realized(self, widget, data=None):
        self.vlcInstance = vlc.Instance("--no-xlib")
        self.player = self.vlcInstance.media_player_new()
        win_id = widget.get_window().get_xid()
        self.player.set_xwindow(win_id)
        self.player.set_mrl(MRL)
        self.player.play()
        self.playback_button.set_image(self.pause_image)
        self.is_player_active = True

if __name__ == "__main__":
    if not sys.argv[1:]:
        print("動画ファイル名が指定されていないので終了します…")
        sys.exit(1)
    if len(sys.argv[1:]) == 1:
        MRL = sys.argv[1]
        window = ApplicationWindow()
        window.setup_objects_and_events()
        window.show()
        Gtk.main()
        window.player.stop()
        window.vlcInstance.release()

上のコードを実行してみました。動くことは動いたけれども、VLCでエラーが出ています。VLCについては次回詳しく調べます。

動作テスト - モジオコシャー

Python 

関連記事