Openshot Video Editorを手探る

はじめに

この記事は東京大学電気電子工学科・電子情報工学科の実験「大規模ソフトウェアを手探る」のレポートとして書かれています。実際に使われているオープンソフトウェアを改良・機能拡張するという実験で、私たちの班ではOpenshot Video Editorを扱いました。

Openshot Video Editorとは

Openshot Video Editorとは、オープンソフトの動画編集ソフトウェアです。クロスプラットフォーム(WindowsMacLinux)に対応しており、コード量は40万行程度となっています。

f:id:TheSpoon:20201021191833p:plain

Openshot Video Editorにはカットやエフェクトの追加など動画編集としての基本的な機能は備わっています。しかし、テキストのサイズが外部ツールを用いなければ変更できなかったり、動作が重くなることがあったりと一部使いづらい部分もあります。今回は、OpenshotのUIに関する部分を改良してみました。

ビルド

openshotのビルドは下記のサイトの通りに行いました。
github.com

このサイトにも書いてあるのですが、Ubuntuでビルドするのが一番簡単だと思います。一応MacWindowsなどの他のOSのためのビルド方法もあり、自分もそれを参考にMacでビルドしようとしても上手くいきませんでしたが、Ubuntuで試したらすんなりビルドできました。なので、他のOSの場合は仮想マシンUbuntuを立ち上げた方がいいと思います。

あとは上のサイトの通りにレポジトリを3つクローンして必要なツールをインストールした後に、手順に沿ってコマンドを実行していけばビルドできると思います。

Ubuntu18.04の場合apt-getでcmakeをインストールすると3.10.2がインストールされてしまい、バージョンが足りないのでcmakeを直接ダウンロードするといいです。下記のサイトを参考にさせていただきました。 Ubuntu20.04の場合特に気にする必要はありません。
qiita.com

変更・追加した機能

今回手を加えたのは以下の2点です。

  • タイムライン上で動画クリップの再生速度を変更した際の問題点とそのUIの変更
  • プレビューで再生時の再生速度の表示 

以下でその詳細を説明します。

タイムライン上で動画クリップの再生速度を変更した際の問題点とそのUIの変更

openshot video editerではタイムライン上でビデオクリップの長さを変更し再生速度を変更する機能が存在します。動画のコマが半分に間引かれて動画の長さが半分になり、その結果再生速度が2倍になるといった具合です。

この機能は便利なのですが使いづらい点があります。それは再生速度を2倍→1倍などにした場合動画の長さが元に戻らないことです。言葉だとわかりづらいですが下の画像を参考にしてください。reset timeという機能を用いると元に戻すことができますが4倍→2倍にしたい場合4倍→reset time→2倍という手順をふむ必要があり面倒です。

f:id:TheSpoon:20201102182033p:plain
2倍を選択
f:id:TheSpoon:20201102182047p:plain
動画時間が半分に
f:id:TheSpoon:20201102182053p:plain
1倍を選択
f:id:TheSpoon:20201102182059p:plain
動画時間は半分のまま

そしてもう一点この機能のUIについて、再生速度を変更するにはForward, Backwardを選び、Normal, Fast, Slow選び、1x, 2x, ...を選ぶ必要があり、あまり使いやすいとは(個人的に)思いませんでした。そのためこの2点を変更しました。

以下にそれぞれの実装の詳細を説明します。

タイムライン上の再生速度関連の実装部分

まずはタイムライン上の再生速度に関わっているコードを探します。openshot-qtのディレクトリではopenshot-qt/src/に実行ファイルが含まれており、今回はその中のwindows/views/timeline_webview.pyというタイムラインの実装に関わるpyファイルに注目をしました。特に再生速度に関わる部分を探すと455行目からのShowClipMenuという関数の737行目から764行目、そして2181行目からのTIme_Triggeredという関数が見つかりました。ShowCLipMenuはUIに関する実装、Time_Triggeredは再生速度変更に関する実装で、それぞれ変更しました。

再生速度変更関連

 

for clip_id in clip_ids:
    # Get existing clip object
    clip = Clip.get(id=clip_id)

    if not clip:
        # Invalid clip, skip to next item
        continue

    # Keep original 'end' and 'duration'
    if "original_data" not in clip.data.keys():
        clip.data["original_data"] = {
            "end": clip.data["end"],
            "duration": clip.data["duration"],
            "video_length": clip.data["reader"]["video_length"]
        }

こちらがTime_Triggerdという関数の2190行目以降の部分です。
タイムライン上のビデオクリップにはそれぞれclip.dataという辞書型の配列が、そしてその中にclip.data["original_data"]という元動画の長さなどの情報を含む辞書型配列が存在します。こちらのコードから、clip.data["original_data"]が存在しないとき追加し、clip.data["original_data"]が存在するときは何もしないことがわかります。
動画の長さが元に戻らないのはclip.data["original_data"]が存在するときは何もしないことが原因です。そのため再生速度の変更をするたびにclip.dataをclip.data["original_data"]で置き換えることで一度動画の長さを元に戻すよう、以下のコードを上のコードの直後に追加しました。

else:
    clip.data["end"] = clip.data["original_data"]["end"]
    clip.data["duration"] = clip.data["original_data"]["duration"]
    clip.data["reader"]["video_length"] = clip.data["original_data"]["video_length"]

これにより動画の長さが元に戻るようになり、reset time機能を用いずに済むようになりました。

UIの変更

f:id:TheSpoon:20201028234743p:plain

上の画像のようにタイムライン上で再生速度を変更する際、動画クリップを右クリックした後に出でくるメニューからtimeを選び、再生速度の分類をNormal, Fast, Slowから選び、再生の方向をForward, Backwardから選び、再生速度を2x, 4x, ...から選ぶ必要がありました。

# Time Menu
Time_Menu = QMenu(_("Time"), self)
Time_None = Time_Menu.addAction(_("Reset Time"))
Time_None.triggered.connect(partial(self.Time_Triggered, MENU_TIME_NONE, clip_ids, '1X'))
Time_Menu.addSeparator()
for speed, speed_values in [
    (_("Normal"), ['1X']),
    (_("Fast"), ['2X', '4X', '8X', '16X']),
    (_("Slow"), ['1/2X', '1/4X', '1/8X', '1/16X'])
]:
    Speed_Menu = QMenu(speed, self)

    for direction, direction_value in [
        (_("Forward"), MENU_TIME_FORWARD),
        (_("Backward"), MENU_TIME_BACKWARD)
    ]:
        Direction_Menu = QMenu(direction, self)

        for actual_speed in speed_values:
            # Add menu option
            Time_Option = Direction_Menu.addAction(_(actual_speed))
            Time_Option.triggered.connect(
                partial(self.Time_Triggered, direction_value, clip_ids, actual_speed))

            # Add menu to parent
            Speed_Menu.addMenu(Direction_Menu)
        # Add menu to parent
        Time_Menu.addMenu(Speed_Menu)

実装は上のコードになっています。
PyQtのQmenuという機能で実装されており、Time_menuの中にreset timeという項目と再生速度をNormalFastSlowで分類したSpeed_menuが存在し、Speed_menuの中に再生の方向を表すDirection_menuが存在し、Direction_menuに再生速度の分類それぞれ対応した再生速度の項目が存在していました。

# Time Menu
Time_Menu = QMenu(_("Time"), self)
Time_Menu.addSeparator()

for speed, speed_direction, speed_values in [
    (_("Forward"), MENU_TIME_FORWARD, ['16X', '8X', '4X', '2X', '1X', '1/2X', '1/4X', '1/8X', '1/16X']),
    (_("Backward"), MENU_TIME_BACKWARD, ['16X', '8X', '4X', '2X', '1X', '1/2X', '1/4X', '1/8X', '1/16X'])
]:
    Speed_Menu = QMenu(speed, self)
 
    for actual_speed in speed_values:
        # Add menu option
        Time_Option = Speed_Menu.addAction(_(actual_speed))
        Time_Option.triggered.connect(
            partial(self.Time_Triggered, speed_direction, clip_ids, actual_speed))

Time_Menu.addMenu(Speed_Menu)

そこでTime_menuの中のreset timeを削除し、Speed_menuのみにしました。そしてSpeed_menuの中身をForward, Backwardにして、それぞれに再生速度を1/16xから16xまで選択できるように変更しました。変更後のメニューは下の画像です、多少煩わしさが減ったかなと思います。
f:id:TheSpoon:20201028234805p:plain


プレビューで再生時の再生速度の表示

f:id:TheSpoon:20201027191655p:plain

Openshot Video Editorの画面右上にあるのがプレビューで、ここで編集した動画を実際に再生して確認できます。画面の下にあるツールバーの真ん中にあるのが再生/停止ボタン、その右が早送り、左が巻き戻しボタンです。
通常の再生速度を1とすると、早送りボタンが押されるたびに再生速度が+1、巻き戻しボタンが押されるたびに再生速度が-1されるようになっています。しかし上の写真の通り、動画再生中の再生速度が表示されていないのがわかりにくいと感じ、それを表示しようと思いました。

早送り・巻き戻しの実装部分

まず、早送りや巻き戻しが実装されている部分を探します。そこでopenshot-qt/src/windows/main_window.pyの990行目あたりを見てみると、actionFastForward_trigger、actionRewind_triggerという関数があります。

def actionFastForward_trigger(self, event):

    # Get the video player object
    player = self.preview_thread.player

    if player.Speed() + 1 != 0:
        self.SpeedSignal.emit(player.Speed() + 1)
    else:
        self.SpeedSignal.emit(player.Speed() + 2)

    if player.Mode() == openshot.PLAYBACK_PAUSED:
        self.actionPlay.trigger()

def actionRewind_trigger(self, event):

    # Get the video player object
    player = self.preview_thread.player

    if player.Speed() - 1 != 0:
        self.SpeedSignal.emit(player.Speed() - 1)
    else:
        self.SpeedSignal.emit(player.Speed() - 2)

    if player.Mode() == openshot.PLAYBACK_PAUSED:
        self.actionPlay.trigger()

player.Speed()が現在の再生速度の値を保持しており、早送り・巻き戻しボタンが押されるたびにこれらの関数が呼び出されてplayer.Speed()の値が更新されます。
なので、このplayer.Speed()の値を表示すればいいということがわかります。

早送り・巻き戻しでの再生速度表示の実装

タイムラインのツールバーに、タイムライン上の時間幅を表示している部分(下の写真の45秒の部分)があるので、その実装を参考にしてplayer.Speed()の値を表示させます。
f:id:TheSpoon:20201028145625p:plain

 この時間幅の表示は、setup_toolbarsという関数の中で2480行目あたりで実装されています。

self.zoomScaleLabel = QLabel(
     _("{} seconds").format(zoomToSeconds(self.sliderZoom.value()))
)

# add zoom widgets
self.timelineToolbar.addAction(self.actionTimelineZoomIn)
self.timelineToolbar.addWidget(self.sliderZoom)
self.timelineToolbar.addAction(self.actionTimelineZoomOut)
self.timelineToolbar.addWidget(self.zoomScaleLabel)

Openshot Video Editorでは、PyQtというPythonGUIアプリケーションの開発に用いられるフレームワークが主に使われています(既出)。この時間幅の表示も、PyQtのQLabelという関数で定義されたself.zoomScaleLabelで表示する文字列を決め、その下のself.timelineToolbar.addWidget(self.zoomScaleLabel)でその文字列を表示するという実装になっています。
なので、新しくQLabelを用いた変数を定義して、それをプレビューのツールバーに表示させればいいということになります。

そこで2420行目あたりをみると、self.videoToolbarというプレビューのツールバーを表す変数があるので、

self.PlaybackSpeedLabel = QLabel(
            _("  {}X").format(self.preview_thread.player.Speed())
)        

# Playback controls (centered)
self.videoToolbar.addAction(self.actionJumpStart)
self.videoToolbar.addAction(self.actionRewind)
self.videoToolbar.addAction(self.actionPlay)
self.videoToolbar.addAction(self.actionFastForward)
self.videoToolbar.addAction(self.actionJumpEnd)
self.videoToolbar.addWidget(self.PlaybackSpeedLabel)
self.actionPlay.setCheckable(True)

というふうにself.PlaybackSpeedLabelを新しく定義して表示させます。
この状態で一度アプリを立ち上げてみると、

AttributeError: 'MainWindow' object has no attribute 'preview_thread'

というエラーが出ます。これは、2630行目あたりの__init__内でsetup_toolbarsがself.preview_threadの定義よりも前に呼び出されているためです。なので、self.preview_threadに関連する定義をsetup_toolbarsよりも前に持ってきます。

# Create the timeline sync object (used for previewing timeline)
self.timeline_sync = TimelineSync(self)

# Setup video preview QWidget
self.videoPreview = VideoWidget()
        
# Start the preview thread
self.preview_parent = PreviewParent()
self.preview_parent.Init(self, self.timeline_sync.timeline, self.videoPreview)
self.preview_thread = self.preview_parent.worker

# Setup toolbars that aren't on main window, set initial state of items, etc
self.setup_toolbars()

そしてもう一度アプリを立ち上げてみると、
f:id:TheSpoon:20201028185134p:plain

これでツールバーの横に文字列が表示されました...が、再生や早送りをしてもこの数字は変わりません。player.Speed()の値が変わるたびに値を更新してくれるのではと思いましたが、setup_toolbarsは__init__内で一回呼ばれるだけなので、表示を更新してはくれません。

よって、前述した再生や早送りをするたびに呼ばれる2つのaction○○_triggerにおいてこの表示の更新を行います。早送りでの更新は以下のように2文だけ付け加えます。

def actionFastForward_trigger(self, event):

    _ = get_app()._tr    #付け加える

    # Get the video player object
    player = self.preview_thread.player

    if player.Speed() + 1 != 0:
        self.SpeedSignal.emit(player.Speed() + 1)
    else:
        self.SpeedSignal.emit(player.Speed() + 2)

    if player.Mode() == openshot.PLAYBACK_PAUSED:
        self.actionPlay.trigger()

    self.PlaybackSpeedLabel.setText(_("  {}X").format(player.Speed()))      #付け加える

QLabel.setTextで表示の更新ができるのでそれを実装しました。巻き戻しも同じなので省略します。これで早送り・巻き戻しの再生速度の表示はできました。実際動かしても早送り・巻き戻しボタンが押されるたび値が更新されていると思います。

再生開始・停止時の再生速度の表示

これで想定していた実装はだいたいできましたが、これだけでは不十分です。Openshot Video Editorでは、早送り→停止→再生としたときに再生速度は1に戻りますが、表示はそうはなっていません。また、動画を停止した時の再生速度も0になっていません。

これを改善するために910行目あたりのactionPlay_triggerという関数を見てみます。
self.actionPlay.isChecked()がTrueの場合は再生開始、Falseの場合は停止を表していますが、その時のplayer.Speed()の値を更新していないのでそれぞれ1、0と更新します。それから早送りと同じような文を追加すれば実装できます。

def actionPlay_trigger(self, event, force=None):

    _ = get_app()._tr       #付け加える
    player = self.preview_thread.player    #付け加える

    # Determine max frame (based on clips)
    timeline_length = 0.0
    fps = get_app().window.timeline_sync.timeline.info.fps.ToFloat()
    clips = get_app().window.timeline_sync.timeline.Clips()
    for clip in clips:
        clip_last_frame = clip.Position() + clip.Duration()
        if clip_last_frame > timeline_length:
            # Set max length of timeline
            timeline_length = clip_last_frame

    # Convert to int and round
    timeline_length_int = round(timeline_length * fps) + 1

    if force == "pause":
        self.actionPlay.setChecked(False)
    elif force == "play":
        self.actionPlay.setChecked(True)

    if self.actionPlay.isChecked():
        ui_util.setup_icon(self, self.actionPlay, "actionPlay", "media-playback-pause")
        self.PlaySignal.emit(timeline_length_int)
        player.Speed(1)

    else:
        ui_util.setup_icon(self, self.actionPlay, "actionPlay")  # to default
        self.PauseSignal.emit()
        player.Speed(0)

    self.PlaybackSpeedLabel.setText(_("  {}X").format(player.Speed()))         #付け加える

以上で再生/停止・早送り・巻き戻し時の再生速度の表示ができました。

感想

今回はOpenshotに対して、以上の2点の改良を行いました。初めはコードが膨大でどこから手をつけていいのかわかりませんでしたが、関数の名前に注目したり、他の部分で似たようなことをやっていないか調べたりすることで、少しずつコードを理解していくことができました。
この実験を通して、膨大な量のコードに触れることができたのはもちろんですが、ビルド周りの知識であったり、PyQtのようなGUIを作成するツールについての知識も学ぶことができ、様々な経験をすることができました。ここでは実験の内容としてソフトウェアの改良を行いましたが、時間が限られた中での作業となってしまい、なかなかOpenshotの深い部分まで手探ることはできませんでした。しかし、少し使ってみただけでもいくつかの改良点(動作が重い、テロップの大きさを外部ツールでしか変更できない等)があったため、余力があればそのような部分についても改良を施してみたいと思います。