【初心者歓迎】動画分割・結合ツールを作ろう(ffmpegで動画の切り出しと結合)

1つの動画から不要な部分を削除したり、複数の動画から一部分を切り出して1つの動画に結合したい場合、あなたならどうしますか?

フリーの動画編集ツールをダウンロードして使うことになると思いますが、一般的な動画編集ツールは機能が豊富なため、操作を覚えるのが大変ではないでしょうか。

そんな時、今回紹介する動画分割・結合ツールが役立ちます。動画を分割・結合する機能だけに特化しており、直感的に操作が行えます。

もちろん、1つの動画の複数個所の切り出しや、複数動画から切り出した動画を1つに結合することも可能です。

高機能な動画編集ツールを使うほどではない分割・結合操作を、サクッと行いたい方は、是非この記事を参考にしてください。

自作ツールを作るためのポイントと今後公開予定の自作ツール一覧を「【実践】Pythonで事務処理向け自作ツールを作ろう!」で紹介していますので、併せてご覧ください。

※2024/09/23 結合対象のファイルが1個の場合は、出力ファイル名でコピーするよう修正。
※2024/09/29 結合対象のファイルが1個の場合、テンポラリ削除でエラーになる不具合を修正。

目次

動画分割・結合ツールの概要

今回紹介する動画分割・結合ツールは、指定した動画の音声を別の音声に入れ替えることが可能です。動画再生には フVLC Media Player、動画の切り出しと結合には ffmpeg を使っています。

  • 動画の再生(再生位置の指定、コマ送り、再生/停止、終了)と音量の調整が可能です。
  • 切り出し範囲の設定(開始位置、終了位置)が行えます。
  • 切り出し範囲や結合手順を登録し、まとめて実行することができます。
  • 登録した手順はテキストデータであるため、自由に追加/編集することができます。
  • 入力フォルダ、出力ファルダに入力した内容は保持され、次回起動時に復元されます

動画分割・結合ツールの使い方

画面項目の説明

画面項目説明
入力動画ファイル編集対象の動画ファイルを指定します。
ここに動画ファイルをドラッグ&ドロップすることも可能です。
動画の再生と音量調整左のスライダー(長い方)を左右に動かすことで、再生位置を移動できます。
右のスライダー(短い方)を使って音量調整が可能です。
各種操作ボタン[現在の再生時刻を切り出し開始位置に設定
<<0.1秒だけコマ戻しする
再生/停止クリックするごとに動画の再生と停止を繰り返す
>>0.1秒だけコマ送りする
]現在の再生時刻を切り出し終了位置に設定
切出 入力動画ファイルに切り出し範囲を指定して、手順登録エリアに追加
追加 入力動画ファイルを丸ごと手順登録エリアに追加
結合 入力動画ファイル名を元に結合ファイル名を作成、手順登録エリアに登録
終了 動画の再生を終了し、スライダーを左端に戻す
実行 手順登録エリアの内容に従って動画の切り出しと結合を実行
出力フォルダ結合済みファイルの保存先フォルダ
手順登録エリア動画の切り出しと結合の手順を登録するためのテキストボックス

手順登録エリアに表示される内容について

「切出」「追加」「結合」ボタンをクリックすると、手順登録エリアに以下のフォーマットで手順が追加されます。

+00:00:00.40,00:00:05.82,O:/TestData/Movie/11098_1280x720.mp4,temp_00000040_00000582.mp4
+00:00:25.15,00:00:30.65,O:/TestData/Movie/11098_1280x720.mp4,temp_00002515_00003065.mp4
+O:/TestData/Movie/440_960x540.mp4
=5253_1280x720.mp4

先頭1文字目が ’+’ で始まるものは、全て結合対象の動画として扱われ、’=’ で始まるものは、結合済みの動画を出力するファイルパスとして扱われます。

切り出し開始時刻 , 切り出し終了時刻 , 入力動画ファイルのパス , 出力動画ファイルのパス  ⇒「切出」
動画ファイルのパス ⇒「追加」
出力動画ファイルのパス ⇒「結合」

’+’ に続けて切り出し開始時刻と終了時刻が続く場合、入力動画ファイルに対して、その区間の切り出しが行われ、出力動画ファイルのパスに従って切り出し動画が出力されます。
尚、出力動画ファイルのパスは、プログラムが自動生成(入力ファイル名+切り出し開始時刻+切り出し終了時刻)します。
一方、切り出し時刻が指定されない場合、入力ファイル名がそのまま結合対象となります。

以上のルールを守っていれば、画面操作を行わず、直接手順登録エリアを編集の上、「実行」ボタンをクリックしても問題ありません。

本ツールの「実行」ボタンがクリックされると、手順登録エリアの内容に応じた引数を指定し、ffmpeg を順次呼び出していきます。[ << ]>> ボタンは、切り出し位置を特定するための道具でしかありません。

テンポラリフォルダについて

切り出した動画は、プログラムが置かれているフォルダ配下の temp フォルダに出力されます。tempフォルダに出力された動画は、動画の結合が完了した後も消されずに残り続けます(自動で削除していません)。

これは、手順登録エリアを手操作で書き換える運用を考えたとき、誤って素材となる動画が削除されるのを防ぐためです。

ファイルの切り出しを繰り返すとディスクを圧迫する恐れがあるため、定期的に手動で削除してください。

プログラムのダウンロードと動作環境の設定

STEP
Pythonのインストール

事前に、Pythonのインストールが必要です。既に構築済みの方は読み飛ばしてください。詳細は下記の個別記事をご確認ください。

自作ツールのためのポータブルPython開発・実行環境を作ろう!プログラム実行時に必要
WinPython環境に ffmpeg を入れよう!プログラム実行時に必要
WinPythonにVSCode Portable版を入れよう!プログラム修正時に必要
STEP
ライブラリのインストール

Python 環境に下記のライブラリをインストールします。 command.bat を実行後、下記のコマンドを実行します。

pip install customtkinter
pip install tkinterdnd2
pip install python-vlc

STEP
ソースコードのダウンロードと展開

動画分割・結合ツールと共通パッケージの2つをダウンロードし、2つのフォルダが同一階層になるように解凍してください。

STEP
VLC Player または libvlcライブラリのインストール

VLC Player をインストールする方法と、libvlc ライブラリをインストール方法の2通りがあります。

方法1.VLC Playerをインストールする方法

下記リンクからインストーラーをダウンロードし、インストールします。この方法が一番簡単です。

https://www.videolan.org/index.ja.html

方法2.libvic をインストールする方法

VLC Player をインストールしたくない場合は、下記リンクから vlc-3.0.9.2-win64.zip をダウンロードしてください。

https://download.videolan.org/pub/videolan/vlc/3.0.9.2/win64/

ZIPファイルをダウンロード&解凍し、2つのDLL(libvlc.dll、libvlccore.dll)と、plugins フォルダを、まるごと movie_clip フォルダにコピーします。

実行方法

次の手順で実行してください。

  • WinPython のインストールフォルダ内にある command.bat を実行
  • ダウンロードしたプログラムファイルをコピーした場所にカレントディレクトリを移動
  • Python movie_clip.py を実行

Python movie_clip.py

しばらくすると下記の画面が表示されます。初回起動時は全ての入力欄が空ですが、2回目以降は直前に入力した値が画面に復元されます。

動画の再生中は vlc の処理状況が、分割・結合処理中は ffmpeg の処理状況がコンソール画面に出力されます。

動画分割・結合ツールの仕様

プログラムの構成

main.py の中で画面の表示と動画音声入れ替えボタンのイベント処理を記述しています。イベント処理の中で直接音声入れ替え処理を記述しても良いのですが、再利用しやすいよう audio_replace.py に処理をまとめています。

モジュール名役割
main.py画面の表示とボタンクリックのイベント処理
videoclipoperator.py動画の音声を、指定した音声ファイル(又は動画ファイルの音声)で書き換える
video_clip.pyUI定義ファイルを読み込んで画面にウィジェットを表示
customtkinterファイル選択やフォルダ選択、ドラッグ&ドロップを実現するための補助
appconfig.py画面に入力された値をJson形式のファイルに保存/読込する

ソースコード

movie_clip.py

VLC Media Player のライブラリと Custon Tkinter を使い、ファイルやフォルダのドラッグ&ドロップにも対応しているため、プログラムがかなり複雑になりました。最初にChatGPTを使ってある程度の大枠を作成し、細々と機能を追加していったため、手を加えるのが難しいかもしれませんが、ご容赦ください。

import os
import sys
os.add_dll_directory(os.getcwd())
sys.path.append(os.path.join(os.path. dirname(__file__), '../libs'))
import customtkinter as ctk  # カスタムウィジェットを提供するライブラリをインポート
import vlc  # VLCメディアプレーヤーライブラリをインポート
from datetime import datetime  # 日時操作のためのdatetimeモジュールをインポート
from customtkintercontrols import App,CenterWindow,FileSelectionControl,TextBoxControl,FolderSelectionControl
from appconfig import AppConfig
from videoclipoperator import VideoClipOperater 

class MediaPlayer:
    """
    VLCメディアプレーヤーをラップして、動画の再生や制御を行うクラス。
    """
    def __init__(self, root):
        # VLCプレーヤーのインスタンスを作成
        self.instance = vlc.Instance("--no-xlib")
        self.player = self.instance.media_player_new()
        self.media = None
        self.is_playing = False  # 再生中かどうかのフラグ
        self.root = root
        self.frame = ctk.CTkFrame(root)  # 動画を表示するためのフレームを作成
        self.frame.pack(fill="both", expand=True)  # フレームをウィンドウに配置
        self.root.after(100, self.set_window_handle)  # 100ms後にウィンドウハンドルを設定

    def set_window_handle(self):
        # VLCプレーヤーにウィンドウハンドルを設定
        self.player.set_hwnd(self.frame.winfo_id())
        self.root.after(1000, self.configure_slider)  # 1秒後にスライダーの設定を行う

    def load(self, filename):
        # メディアファイルを読み込み、VLCプレーヤーに設定
        self.media = self.instance.media_new(filename)
        self.player.set_media(self.media)
        self.root.after(1000, self.configure_slider)  # 1秒後にスライダーの設定を行う

    def toggle_play_pause(self):
        # 再生/一時停止ボタンが押されたときの動作を切り替え
        if self.is_playing:
            self.pause()  # 再生中なら一時停止
        else:
            self.start()  # 停止中なら再生

    def start(self):
        # 再生を開始し、開始時刻を記録
        if not self.is_playing:
            self.player.play()
            self.is_playing = True

    def stop(self):
        # 再生を停止し、スライダーと時刻ラベルをリセット
        self.player.stop()
        self.is_playing = False
        scale.set(0)  # スライダーを左端に戻す
        time_label.configure(text="00:00:00.00")  # 時刻ラベルをリセット
    
    def pause(self):
        # 再生を一時停止し、終了時刻を記録
        if self.is_playing:
            self.player.pause()
            self.is_playing = False

    def start_mark(self):
        # 開始時刻をラベルに表示
        start_time_label.configure(text=time_label.cget("text")) 
        self.pause()

    def end_mark(self):
        # 終了時刻をラベルに表示
        end_time_label.configure(text=time_label.cget("text")) 
        self.pause()

    def seek(self, value):
        # スライダーを移動して再生位置を設定
        self.player.set_time(int(value))

    def get_time(self):
        # 現在の再生位置を取得
        return self.player.get_time()

    def get_length(self):
        # 動画の長さを取得
        length = self.player.get_length()
        return length

    def configure_slider(self):
        """
        動画の長さに応じてスライダーを設定し、再生を自動的に開始する関数。
        動画の長さが取得できなかった場合、再試行を行う。
        """
        video_length = self.get_length()
        if video_length > 0:
            scale.configure(to=video_length)  # スライダーの最大値を動画の長さに設定
            scale.set(0)  # スライダーを左端に設定
            self.start()  # 自動再生開始
        else:
            self.root.after(1000, self.configure_slider)  # 動画長さ取得に失敗したら再試行

def update_slider():
    # スライダーと時刻ラベルを更新する関数
    length = mp.get_length()
    current_time = mp.get_time()
    if length > 0:
        scale_value = int(current_time)
        if scale_value != scale.get():
            scale.set(scale_value)
            update_time_label(scale_value)  # スライダー位置に応じた時刻を更新

    root.after(100, update_slider)  # 100ms後に再度更新

def on_scale_move(event):
    # スライダーが動かされた時の処理
    global scale_dragging
    if scale_dragging:
        scale_value = scale.get()
        mp.seek(scale_value)

def update_time_label(milliseconds):
    # ミリ秒単位の時間を時分秒およびミリ秒に変換してラベルに表示
    seconds = milliseconds // 1000
    minutes = seconds // 60
    hours = minutes // 60
    seconds = seconds % 60
    minutes = minutes % 60
    millis = milliseconds % 1000
    time_label.configure(text=f"{hours:02}:{minutes:02}:{seconds:02}.{millis:02}"[:11])

def on_scale_press(event):
    # スライダーが押された時の処理
    global scale_dragging
    scale_dragging = True

def on_scale_release(event):
    # スライダーが離された時の処理
    global scale_dragging
    scale_dragging = False
    scale_value = scale.get()
    mp.seek(scale_value)  # スライダーの位置に応じて再生位置を変更
    update_time_label(scale_value)  # スライダー位置に応じた時刻を更新

def on_volume_change(value):
    # ボリュームスライダーが変更されたときの処理
    mp.player.audio_set_volume(int(value))

def add_clip():
    input_file = movie_dialog.get()
    action_text.set(action_text.get() + f'+{input_file}\n')

def cut_clip():
    # 追加ボタンクリック時の処理(開始時刻と終了時刻をテキストボックスに記録)
    start_time = start_time_label.cget("text")
    end_time = end_time_label.cget("text")
    if start_time != end_time:
        prefix = f"{start_time}_{end_time}".replace(":","").replace(".","")
        input_file = movie_dialog.get()
        _,ext = os.path.splitext(os.path.basename(input_file))
        text = f'+{start_time},{end_time},{input_file}' + f',temp_{prefix}{ext}\n'
        lines = action_text.get().split("\n")
        if not text.strip() in lines:
            action_text.set(action_text.get() + text)
        start_time_label.configure(text="00:00:00.00")
        end_time_label.configure(text="00:00:00.00")

def join_movie():
    # 結合ボタンクリック時の処理(結合後の出力ファイルパスを生成)
    movie_file = movie_dialog.get()
    base = os.path.basename(movie_file)
    name,ext = os.path.splitext(base)
    sufix = datetime.now().strftime('%H%M%S') # 現在時刻をプレフィックスに設定
    path = os.path.join(output_folder.get(),name)
    action_text.set(action_text.get() + f'={path}_{sufix}{ext}\n')
    
def selected():
    # ファイル選択ダイアログでファイル選択時の処理
    mp.load(movie_dialog.get())

def time_diff_hhmmss(time1_str, time2_str):
  """
  2つの時間文字列(hh:mm:ss形式)の差を計算し、hh:mm:ss形式で返す関数

  Args:
    time1_str (str): 開始時刻を表す文字列
    time2_str (str): 終了時刻を表す文字列

  Returns:
    str: 時間差を表す文字列(hh:mm:ss形式)
  """

  # 時間文字列をdatetimeオブジェクトに変換
  time_format = "%H:%M:%S.%f"
  time1 = datetime.strptime(time1_str, time_format)
  time2 = datetime.strptime(time2_str, time_format)

  # 時間差を計算
  time_diff = time2 - time1

  # 時間差をhh:mm:ss.000 形式の文字列に変換
  return str(time_diff)

def execute():
    # テキストボックスに表示されているファイル分割/結合処理を実行
    os.makedirs(temp_folder,exist_ok=True)
    vco = VideoClipOperater()
    queue = []
    for line in action_text.get().split("\n") :
        if line.strip() == "":
            continue
        items = line.split(",")
        if line[0] == "+":
            if len(items) == 1:
                queue.append(line[1:])
            else:
                input_path = items[2]
                output_path = os.path.join(temp_folder,items[3])
                queue.append(os.path.abspath(output_path)) # 相対パスを絶対パスに変換
                cut_start = items[0][1:]
                cut_end = items[1]
                cut_len =  time_diff_hhmmss(cut_start,cut_end) 
                vco.cut_video(input_path,cut_start,cut_len,output_path)
        if line[0] == "=":
            output_file = line[1:].strip()
            vco.merge_videos(queue,output_file)
            queue.clear()

def skip_backward():
    # 左に0.1秒進める関数
    current_time = mp.get_time()
    new_time = max(0, current_time - 100)  # 0.1秒(100ミリ秒)後退
    mp.seek(new_time)
    update_time_label(new_time)

def skip_forward():
    # 右に0.1秒進める関数
    current_time = mp.get_time()
    length = mp.get_length()
    new_time = min(length, current_time + 100)  # 0.1秒(100ミリ秒)前進
    mp.seek(new_time)
    update_time_label(new_time)

# ウィンドウを閉じる時に呼ばれる関数
def close_window():
    # 画面入力値を保存
    conf.set("movie_dialog",movie_dialog.get())
    conf.set("output_folder",output_folder.get())
    conf.set("action_text",action_text.get())
    conf.write()
    # プログラムの終了
    root.quit()  

if __name__ == "__main__":

    # -----------------------------------------------------
    # 作業フォルダの設定
    # ------------------------------------------------------
    temp_folder = "./temp"

    # -----------------------------------------------------
    # ウィンドウの作成
    # ------------------------------------------------------
    
    # メインウィンドウの設定
    root = App()
    CenterWindow(root,800,600)
    root.title("Video Player")
    
    # ウィンドウを閉じる時に呼びたい関数を登録
    root.protocol("WM_DELETE_WINDOW", close_window)

    # -----------------------------------------------------
    # 画面にウィジェットを配置
    # ------------------------------------------------------

    # ファイル選択ダイアログの配置
    movie_dialog = FileSelectionControl(root,command=selected)
    movie_dialog.pack(fill="x")
    
    # メディアプレイヤーの配置
    mp = MediaPlayer(root)  # MediaPlayerクラスのインスタンスを作成
    scale_dragging = False  # スライダーがドラッグされているかを管理

    # フレームの開始
    fm = ctk.CTkFrame(root)

    # 現在の再生位置(時刻)表示ラベル
    time_label = ctk.CTkLabel(fm, text="00:00:00.00")  
    time_label.grid(row=0, column=0, padx=10, pady=10)

    # スライダーの最大値を仮の値に設定
    scale = ctk.CTkSlider(fm, from_=0, to=100)
    scale.set(0)
    scale.grid(row=0, column=1, padx=10, pady=10, sticky="ew")

    # スライダーにイベントバインド
    scale.bind("<ButtonPress-1>", on_scale_press)
    scale.bind("<B1-Motion>", on_scale_move)
    scale.bind("<ButtonRelease-1>", on_scale_release)

    # 音量スライダーの追加
    volume_slider = ctk.CTkSlider(fm, width=60, from_=0, to=100, command=on_volume_change)
    volume_slider.set(50)  # 初期値を100に設定
    volume_slider.grid(row=0, column=2, padx=10, pady=10)

    #フレームの終了
    fm.grid_columnconfigure(1, weight=1)
    fm.pack(fill="x")

    # フレームの開始
    fm = ctk.CTkFrame(root)

    # 範囲のラベル
    space = ctk.CTkLabel(fm,text = "範囲")
    space.pack(side="left",padx=10)

    # 開始時刻を表示するラベル
    start_time_label = ctk.CTkLabel(fm, text="00:00:00.00")
    start_time_label.pack(side="left", padx=2)
    space = ctk.CTkLabel(fm,text = " ~ ")
    space.pack(side="left",padx=0)

    # 終了時刻を表示するラベル
    end_time_label = ctk.CTkLabel(fm, text="00:00:00.00")
    end_time_label.pack(side="left", padx=2)
    space = ctk.CTkLabel(fm,text = "")
    space.pack(side="left",padx=5)

    #[ 範囲の開始設定ボタン
    start_button = ctk.CTkButton(fm, text="[", width=40,command=lambda: mp.start_mark())
    start_button.pack(side="left", padx=2)

    # 左スキップボタンの配置
    backward_button = ctk.CTkButton(fm, text="<<", width=30, command=skip_backward)
    backward_button.pack(side="left", padx=2)

    # 再生/停止ボタン
    button1 = ctk.CTkButton(fm, text="再生/停止", width=80,command=lambda: mp.toggle_play_pause())
    button1.pack(side="left",padx=2)

    # 右スキップボタンの配置
    forward_button = ctk.CTkButton(fm, text=">>", width=30, command=skip_forward)
    forward_button.pack(side="left", padx=2)

    # ]範囲の終了設定ボタン
    end_button = ctk.CTkButton(fm, text="]", width=40,command=lambda: mp.end_mark())
    end_button.pack(side="left", padx=2)

    # 余白の設定
    space = ctk.CTkLabel(fm,text = "")
    space.pack(side="left",padx=5)


    # 切り出しボタン(ファイル指定区間)
    record_button = ctk.CTkButton(fm, text="切出", width=40,command=cut_clip)
    record_button.pack(side="left", padx=2)

    # 追加ボタン(ファイル丸ごと)
    record_button = ctk.CTkButton(fm, text="追加", width=40,command=add_clip)
    record_button.pack(side="left", padx=2)

    # 結合ボタン
    record_button = ctk.CTkButton(fm, text="結合", width=40,command=join_movie)
    record_button.pack(side="left", padx=2)

    # 再生終了ボタン
    button3 = ctk.CTkButton(fm, text="終了", width=40, command=lambda: mp.stop())
    button3.pack(side="left",padx=2)

    # 分割実行ボタン
    button3 = ctk.CTkButton(fm, text="実行", width=60, command=execute)
    button3.pack(side="left",padx=10)

    # フレームの終了
    fm.pack(pady=10)

    # 保存先フォルダ選択ダイアログ
    output_folder = FolderSelectionControl(root)
    output_folder.pack(padx=10,pady=10,fill="x")

    # 記録を表示するテキストボックス
    action_text = TextBoxControl(root)
    action_text.pack(padx=10,pady=10,fill="x")

    # スライダーと時刻の更新を開始
    root.after(100, update_slider)

    # ------------------------------------------------------
    # 前回の入力内容を復元
    # ------------------------------------------------------

    # 画面の入力項目をファイルに保存する
    conf = AppConfig("./config.json")
    conf.read()
    movie_dialog.set(conf.get("movie_dialog",""))
    output_folder.set(conf.get("output_folder",""))
    action_text.set(conf.get("action_text",""))

    # -----------------------------------------------------
    # CustomTkinter のメインループを開始
    # ------------------------------------------------------
    root.mainloop()  

videoclipoperator.py

このファイルには、VideoClipOperater クラスが含まれています。VideoClipOperater は 、動画の切り出しと結合、動画情報(フレームレートとビットレート)の取得メソッドを持っています。

メソッド説明
__init__(
cut_encode, # 切り出し時の再エンコード指定(初期値=True)
merge_encode, # 再エンコード指定(初期値=True)
)
ともにFalseを指定すると再エンコード無しで実行されます。但し、動画によってノイズが入ったり、不安定になる場合があります。
cut_video(
input_videos, #動画入力ファイルのパス
start_time, # 切り出し開始時刻
time_len, # 切り出す長さ
output_video, # 出力動画ファイルパス
)
start_time で指定した時刻から、time_len で指定した長さだけ、動画を切り出します。
start_time、time_len ともに hh:mm:ss.fff 形式で指定します
例:"00:10:05.010"
merge_videos(
input_videos, #動画入力ファイルのパス
output_video, # 出力動画ファイルパス
)
input_videos にリスト形式でファイルのパスを指定します。
メソッド内部で結合対象のファイルパスを列記したテキストファイルを作成し、ffmpeg の -i オプションに指定しています。
get_video_info(
input_video, #動画入力ファイルのパス
)
ffprobe コマンドを使って、動画のフレームレートとビットレートをタプルで返します。
import subprocess
import os
import shutil
import tempfile

class VideoClipOperater:
    def __init__(self,cut_encode=True,merge_encode=True):
        self.cut_encode = "" if cut_encode else "-c copy"
        self.merge_encode = "" if merge_encode else "-c copy"        
                
    def cut_video(self, input_video, start_time, end_time, output_video):
        """
        動画の指定範囲を切り出す

        Args:
            input_video (str): 入力動画ファイルパス
            start_frame (int): 開始フレーム
            end_frame (int): 終了フレーム
            output_video (str): 出力動画ファイルパス
        """
        # ffmpegコマンドを作成
        command = f'ffmpeg -ss {start_time} -i "{input_video}" -to {end_time} -y {self.cut_encode} "{output_video}"'
        subprocess.call(command, shell=True)
        

    def merge_videos(self, input_videos, output_video):
        """
        複数の動画を結合する

        Args:
            input_videos (list): 入力動画ファイルパスのリスト
            output_video (str): 出力動画ファイルパス
        """
        if len(input_videos) == 1:
            shutil.copy(input_videos[0], output_video)
        else:
            # 結合対象のファイルリストを一時ファイルとして作成
            with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_file:
                temp_file.writelines(f"file '{video}'\n" for video in input_videos)
                temp_file_name = temp_file.name

            # ffmpegコマンドの実行    
            command = f'ffmpeg -f concat -safe 0 -i "{temp_file_name}" {self.merge_encode} -y "{output_video}"'    
            subprocess.call(command, shell=True)
        
            # 一時ファイルの削除
            os.remove(temp_file_name)
    
    def get_video_info(self,input_video):
        """
        動画ファイルのフレームレートとビットレートを取得します。

        Args:
            input_video (str): 入力動画ファイルパス

        Returns:
            tuple: フレームレート (float), ビットレート (int)
        """

        # ffprobeコマンドを作成 (フレームレートとビットレートを取得)
        get_info_cmd = f'ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,bit_rate -of default=noprint_wrappers=1:nokey=1 "{input_video}"'
        res = subprocess.check_output(get_info_cmd, shell=True).decode('utf-8')

        # 出力結果を解析して、フレームレートとビットレートを抽出
        fps, bitrate_str = res.strip().replace("\r","").split("\n")
        fps = eval(fps)
        bitrate = int(bitrate_str)

        return fps, bitrate

本クラスを直接使用する場合は、下記のサンプルを参考にしてください。

from videoclipoperator import VideoClipOperater 

vc = VideoClipOperater()

# 動画から指定範囲を切り出す
vc.cut_video("d:/hoge1.mp4","00:30:00","00:05:00","d:/output1.mp4")
vc.cut_video("d:/hoge2.mp4","00:30:00","00:05:00","d:/output2.mp4")

# 動画の音声を入れ替える
vc .merge_videos(["d:/output1.mp4","d:/output2.mp4"],"d:/join.mp4"):

# 動画から音声を抽出する
framerate,bitrate = vc.get_video_info("d:/join.mp4"):

まとめ

今回は、「動画分割・結合ツール」について、環境構築の手順と使い方、ソースコードを紹介しました。

Pythonで動画再生を行う方法はいくつかありますが、音声も併せて動画再生させたい場合は、VLC Media Player に同梱されいるライブラリを利用するのが一番簡単です。

また、動画の切り出しや結合については、ffmpeg が非常に便利です。

本ツールもこの2つを使っています。UI部分はかなり複雑になりましたが、できるだけ関数化していますので、必要に応じて手を加えて下さい。動画の切り出しと結合はクラス化していますので、再利用可能です。

本記事で紹介するツールはあくまでも簡易版ですので、他のツールと比べてチープな部分は多々ありますが、本記事の内容が皆さんのお役に立てれば幸いです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次