作ったもの
動画内の黒/白のフェードイン/アウトからチャプターを検出して、好きなチャプターだけを選んで結合できるアプリ「ChapterTrimmer」を作りました。
こちらからダウンロードできます(現時点ではWindowsのみ)。
※自分で使うために勢いで作ったので、正常系の簡単なテストしかやってません。ちゃんとしたテストやリファクタリングはあとでやります(フラグ)。
なぜ作ったか
黒や白のフェードイン/アウトで区切られた動画を観ることってよくありますよね。僕はあります。そのなかで必要なチャプターだけ抽出できたらいいなーと思って調べてみたんですが、そのようなことができるアプリが見当たらなかったので作りました。
なぜFletで作ったか
今回はFletという、フロントエンド言語を書かずにPythonだけでFlutterアプリを作れるフレームワークを使いました。選定の理由はこちらです。
- GB単位の動画ファイルを扱う想定なので、サーバーへのアップロード/ダウンロードが必要なWEBアプリよりもデスクトップアプリがいい
- デスクトップアプリを作るにはいろんなやり方があるが、Fletが一番ちょうど良さそう
ざっくり説明
動作の流れは以下の通りです。順にざっくり説明していきます。
- 動画ファイルを読み込む
- 動画のチャプターを検出する
- 各チャプターの動画を表示する
- 選択されたチャプターを結合して保存する
なお、ソースコードはGitHubにあるので詳しくはそちらを見てください。
動画ファイルを読み込む
Fletでファイルを読み込むには以下のように書きます。
def main(page: ft.Page):
def load_video(e: ft.FilePickerResultEvent):
picked_video = ft.VideoMedia(e.files[0].path)
# 何かしらの処理
# たとえば動画を再生するならこう書く
video = ft.Video(
aspect_ratio=16 / 9,
autoplay=False,
filter_quality=ft.FilterQuality.HIGH,
playlist=[picked_video],
playlist_mode=ft.PlaylistMode.NONE,
)
page.add(video)
page.update()
file_picker = ft.FilePicker(on_result=load_video)
page.overlay.append(file_picker)
file_pick_button = ft.Container(
ft.ElevatedButton(
"Pick Video File",
icon=ft.icons.VIDEO_FILE_OUTLINED,
on_click=lambda _: file_picker.pick_files(allow_multiple=False, file_type=ft.FilePickerFileType.VIDEO),
),
)
page.add(file_pick_button)
page.update()
ft.app(target=main)
Fletは公式ドキュメントが充実しているので、それを参考にすればだいたいの処理は書けると思います。
動画のチャプターを検出する
チャプター検出にはPySceneDetectを使いました。
PySceneDetectには以下の3つのDetectorが搭載されています。
-
Content-Aware Detector
- 動画内のフレームがパッと切り替わったタイミングを検出できる
-
Adaptive Content Detector
-
Content-Aware Detector
と同様にパッと切り替わったのを検出できるが、誤検出が少ない
-
-
Threshold Detector
- フレームの輝度が一定以上/以下になったタイミングを検出できる
これらの複数を組み合わせて使うこともできますが、手持ちの動画だと Content-Aware Detector
と Adaptive Content Detector
では意図しないチャプターが大量に検出されてしまったため、今回は Threshold Detector
のみを使いました。
さて、PySceneDetectでチャプター検出をするにはこのように書きます。
def detect_chapter(video_file_path: str) -> list[tuple[FrameTimecode, FrameTimecode]]:
video = open_video(video_file_path)
scene_manager = SceneManager()
scene_manager.auto_downscale = True
# detect White
scene_manager.add_detector(ThresholdDetector(threshold=243, method=ThresholdDetector.Method.CEILING))
# detect Black
scene_manager.add_detector(ThresholdDetector(threshold=12, method=ThresholdDetector.Method.FLOOR))
scene_manager.detect_scenes(frame_source=video, show_progress=True, frame_skip=1)
chapter_list = scene_manager.get_scene_list()
return chapter_list
上記の ThresholdDetector
の2つの引数のうち、threshold
はチャプターの区切りとみなす輝度のしきい値、method
は threshold
で指定したしきい値の 以上(CEILING) or 以下(FLOOR) のどちらを区切りとみなすかを表しています。
上記のように書くことで、黒のフェードと白のフェードをどちらもチャプターの区切りとして検出しています。
各チャプターの動画を表示する
いくつかやり方はあると思いますが、今回は以下のような方法で実現しました。
- Pythonから
ffmpeg
コマンドを叩いて、元の動画ファイルから各チャプターの動画を切り出して保存する- 5つのチャプターが検出されたら5つの動画ファイルができる
- PySceneDetectに
split_video_ffmpeg()
というこれ用の関数があったのですが、動作が遅かったので自分で書きました
- 各チャプターの動画ファイルを読み込んで表示する
- 少し上に書いたやり方と同様
今回は各動画と合わせてチェックボックスも表示させたかったので、以下のように書きました。
splitted_video_grid = ft.GridView(
runs_count=3, spacing=20, run_spacing=20, child_aspect_ratio=16 / 12, padding=ft.padding.only(right=20)
)
check_box_list = [ft.Checkbox(value=False) for _ in range(len(splitted_video_list))]
for check_box, splitted_video in zip(check_box_list, splitted_video_list):
splitted_video_grid.controls.append(
ft.Container(
ft.Column([check_box, splitted_video]),
bgcolor=ft.colors.GREY_200,
border_radius=10,
padding=10,
),
)
ちなみに、デスクトップアプリ上だと動画コンポーネントの見た目がとてもYoutubeっぽいのでクリックして再生/一時停止ができそうですが、現時点だとできません。
選択されたチャプターを結合して保存する
これもいくつかやり方があると思いますが、上記と同様に ffmpeg
を叩くようにしました。具体的には以下のような流れになっています。
-
Merge
ボタンが押されたタイミングで各チェックボックスの状態(True/False)をみる - 一時ファイルを作成し、Trueになっている動画のファイルパスをすべて書き込む
-
ffmpeg concat
コマンドでその一時ファイルを参照し、結合した動画を保存する
たとえば以下のようなテキストファイルを ffmpeg concat
コマンドで読み込ませると、ファイル内の3つのファイルを結合して保存できます。
hoge/chapter_1.mp4
hoge/chapter_2.mp4
hoge/chapter_3.mp4
ffmpeg concat
コマンドの注意点として動画の解像度を揃える必要があるのですが、今回は同じ動画ファイルから切り出してるので解像度はすべて同じです。
まとめ
以上を組み合わせると、好きな動画から好きなチャプターだけ保存できます。やったね!