動画圧縮ツールwithffmpeg

ffmpegで動画をほぼ無劣化で圧縮する簡易的なツールを作った

まじのアバター

(どこのものかに限らず)
コードは自己責任で利用してくださいね!

環境構築

テキストエディタで作業場所に .bat を作成し、実行してください。

@echo off
setlocal enabledelayedexpansion

echo "かんたん動画圧縮ソフト セットアップツール"
echo "=========================================="

:: 作業ディレクトリの作成
set WORK_DIR=%USERPROFILE%\video_compressor
if not exist "%WORK_DIR%" mkdir "%WORK_DIR%"
cd /d "%WORK_DIR%"

:: Pythonのインストール確認とダウンロード
python --version > nul 2>&1
if errorlevel 1 (
    echo Pythonをダウンロードしています...
    curl -o python_installer.exe https://www.python.org/ftp/python/3.11.5/python-3.11.5-amd64.exe
    if errorlevel 1 (
        echo Pythonのダウンロードに失敗しました
        goto :error
    )
    echo Pythonをインストールしています...
    python_installer.exe /quiet InstallAllUsers=1 PrependPath=1
    if errorlevel 1 (
        echo Pythonのインストールに失敗しました
        goto :error
    )
)

:: FFMPEGのダウンロードと設定
if not exist "%WORK_DIR%\ffmpeg" (
    echo FFMPEGをダウンロードしています...
    curl -L -o ffmpeg.zip https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
    if errorlevel 1 (
        echo FFMPEGのダウンロードに失敗しました
        goto :error
    )
    powershell Expand-Archive ffmpeg.zip .
    move ffmpeg-master-latest-win64-gpl ffmpeg
    setx PATH "%PATH%;%WORK_DIR%\ffmpeg\bin"
)

:: 必要なライブラリのインストール
echo Pythonライブラリをインストールしています...
python -m pip install --upgrade pip
python -m pip install customtkinter
if errorlevel 1 (
    echo ライブラリのインストールに失敗しました
    goto :error
)

:: Pythonスクリプトの作成
echo Pythonスクリプトを作成しています...
if not exist "%WORK_DIR%\compressor.py" (
    copy nul compressor.py
)

echo "セットアップが完了しました!"
echo "video_compressor フォルダで python compressor.py にスクリプトを書き込んで実行してください"
pause
exit /b 0

:error
echo エラーが発生しました
echo エラーコード: %errorlevel%
pause
exit /b 1
@echo off
setlocal enabledelayedexpansion

echo "かんたん動画圧縮ソフト セットアップツール"
echo "=========================================="

:: 作業ディレクトリの作成
set WORK_DIR=%USERPROFILE%\video_compressor
if not exist "%WORK_DIR%" mkdir "%WORK_DIR%"
cd /d "%WORK_DIR%"

:: Pythonのインストール確認とダウンロード
python --version > nul 2>&1
if errorlevel 1 (
    echo Pythonをダウンロードしています...
    curl -o python_installer.exe https://www.python.org/ftp/python/3.11.5/python-3.11.5-amd64.exe
    if errorlevel 1 (
        echo Pythonのダウンロードに失敗しました
        goto :error
    )
    echo Pythonをインストールしています...
    python_installer.exe /quiet InstallAllUsers=1 PrependPath=1
    if errorlevel 1 (
        echo Pythonのインストールに失敗しました
        goto :error
    )
)

:: FFMPEGのダウンロードと設定
if not exist "%WORK_DIR%\ffmpeg" (
    echo FFMPEGをダウンロードしています...
    curl -L -o ffmpeg.zip https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
    if errorlevel 1 (
        echo FFMPEGのダウンロードに失敗しました
        goto :error
    )
    powershell Expand-Archive ffmpeg.zip .
    move ffmpeg-master-latest-win64-gpl ffmpeg
    setx PATH "%PATH%;%WORK_DIR%\ffmpeg\bin"
)

:: 必要なライブラリのインストール
echo Pythonライブラリをインストールしています...
python -m pip install --upgrade pip
python -m pip install customtkinter
if errorlevel 1 (
    echo ライブラリのインストールに失敗しました
    goto :error
)

:: Pythonコードを書き込むファイルの作成
set CODE_FILE=%WORK_DIR%\compressor.py
echo # -*- coding: utf-8 -*- > "%CODE_FILE%"

:: 必要なインポート文を追加
(
echo import customtkinter as ctk
echo import subprocess
echo import os
echo from pathlib import Path
echo import threading
echo import tkinter.filedialog as filedialog
echo import re
echo import queue
echo import time
echo class VideoCompressorApp:
echo     def __init__(self):
echo         self.window = ctk.CTk()
echo         self.window.title("かんたん動画圧縮ソフト")
echo         self.window.geometry("600x700")  # ウィンドウサイズを調整
echo         self.main_frame = ctk.CTkFrame(self.window)
echo         self.main_frame.pack(pady=20, padx=20, fill="both", expand=True)
echo         self.title_label = ctk.CTkLabel(
echo             self.main_frame, text="動画圧縮ツール", font=("Helvetica", 24)
echo         )
echo         self.title_label.pack(pady=(20, 10))  # 下側のpadyを小さくして詰める
echo         # 設定フレームをタイトルの直後に配置
echo         self.settings_frame = ctk.CTkFrame(self.main_frame)
echo         self.settings_frame.pack(pady=(0, 10))  # タイトルとの間隔を調整
echo         # エンコーダー選択の変数
echo         self.encoder_mode_var = ctk.StringVar(value="cpu_h264")
echo         self.crf_value_var = ctk.StringVar(value="28")
echo         self.output_path_var = ctk.StringVar(value="")
echo         # エンコーダー選択用ラジオボタン
echo         encoders_frame = ctk.CTkFrame(self.settings_frame)
echo         encoders_frame.pack(pady=5, padx=10, fill="x")
echo         ctk.CTkLabel(encoders_frame, text="エンコーダー選択:").pack(pady=2)
echo         # ラジオボタンを横並びに変更
echo         radio_frame = ctk.CTkFrame(encoders_frame)
echo         radio_frame.pack(pady=2)
echo         self.cpu_h264_radio = ctk.CTkRadioButton(
echo             radio_frame,
echo             text="CPU (h.264)",
echo             variable=self.encoder_mode_var,
echo             value="cpu_h264",
echo         )
echo         self.cpu_h265_radio = ctk.CTkRadioButton(
echo             radio_frame,
echo             text="CPU (h.265)",
echo             variable=self.encoder_mode_var,
echo             value="cpu_h265",
echo         )
echo         self.gpu_hevc_radio = ctk.CTkRadioButton(
echo             radio_frame,
echo             text="GPU (HEVC)",
echo             variable=self.encoder_mode_var,
echo             value="hevc_nvenc",
echo         )
echo         self.cpu_h264_radio.pack(side="left", padx=10)
echo         self.cpu_h265_radio.pack(side="left", padx=10)
echo         self.gpu_hevc_radio.pack(side="left", padx=10)
echo         # CRF値の入力とツールチップ
echo         crf_frame = ctk.CTkFrame(self.settings_frame)
echo         crf_frame.pack(pady=5, padx=10, fill="x")
echo         crf_label = ctk.CTkLabel(crf_frame, text="CRF値 (0-51):")
echo         crf_label.pack(side="left", padx=5)
echo         self.crf_entry = ctk.CTkEntry(
echo             crf_frame, textvariable=self.crf_value_var, width=50
echo         )
echo         self.crf_entry.pack(side="left", padx=5)
echo         crf_help = ctk.CTkLabel(crf_frame, text="?", width=20)
echo         crf_help.pack(side="left", padx=5)
echo         # 出力先ディレクトリ選択
echo         output_frame = ctk.CTkFrame(self.settings_frame)
echo         output_frame.pack(pady=5, padx=10, fill="x")
echo         ctk.CTkLabel(output_frame, text="出力先:").pack(side="left", padx=5)
echo         self.output_path_entry = ctk.CTkEntry(
echo             output_frame, textvariable=self.output_path_var, width=300
echo         )
echo         self.output_path_entry.pack(side="left", padx=5)
echo         ctk.CTkButton(
echo             output_frame, text="参照", width=50, command=self.select_output_directory
echo         ).pack(side="left", padx=5)
echo         # ツールチップの設定
echo         self.tooltip_window = None
echo         crf_help.bind("<Enter>", self.show_crf_tooltip)
echo         crf_help.bind("<Leave>", self.hide_crf_tooltip)
echo         # ファイル選択ボタンを設定の下に配置
echo         self.select_button = ctk.CTkButton(
echo             self.main_frame, text="動画ファイルを選択", command=self.select_files
echo         )
echo         self.select_button.pack(pady=10)
echo         # ファイルリストを表示するテキストボックス
echo         self.file_list = ctk.CTkTextbox(self.main_frame, height=100, width=500)
echo         self.file_list.pack(pady=10)
echo         # 全体のプログレスバー
echo         self.total_progress_label = ctk.CTkLabel(self.main_frame, text="全体の進捗:")
echo         self.total_progress_label.pack(pady=(10, 0))
echo         self.total_progress_bar = ctk.CTkProgressBar(self.main_frame, width=400)
echo         self.total_progress_bar.pack(pady=(0, 10))
echo         self.total_progress_bar.set(0)
echo         # 現在のファイルのプログレスバー
echo         self.current_progress_label = ctk.CTkLabel(
echo             self.main_frame, text="現在のファイルの進捗:"
echo         )
echo         self.current_progress_label.pack(pady=(10, 0))
echo         self.current_progress_bar = ctk.CTkProgressBar(self.main_frame, width=400)
echo         self.current_progress_bar.pack(pady=(0, 10))
echo         self.current_progress_bar.set(0)
echo         # FFMPEGの出力表示用テキストボックス
echo         self.output_label = ctk.CTkLabel(self.main_frame, text="FFMPEG出力:")
echo         self.output_label.pack(pady=(10, 0))
echo         self.output_text = ctk.CTkTextbox(self.main_frame, height=100, width=500)
echo         self.output_text.pack(pady=10)
echo         # 状態表示ラベル
echo         self.status_label = ctk.CTkLabel(self.main_frame, text="")
echo         self.status_label.pack(pady=10)
echo         self.output_directory = None  # この変数は引き続き使用
echo         self.selected_files = []
echo         # キューの初期化
echo         self.message_queue = queue.Queue()
echo         self.processing = False
echo         # 定期的なGUI更新用のメソッド呼び出し
echo         self.window.after(100, self.update_gui)
echo     def select_files(self):
echo         # 複数ファイル選択に対応
echo         files = filedialog.askopenfilenames(filetypes=[("動画ファイル", "*.mp4 *.mov")])
echo         if files:
echo             self.selected_files = files
echo             self.file_list.delete("1.0", "end")
echo             for file in files:
echo                 self.file_list.insert("end", f"{file}\n")
echo             # ファイル選択後すぐに圧縮開始
echo             self.start_compression()
echo     def select_output_directory(self):
echo         directory = filedialog.askdirectory()
echo         if directory:
echo             self.output_directory = directory
echo             self.status_label.configure(text=f"出力先: {directory}")
echo     def start_compression(self):
echo         # 圧縮処理を別スレッドで実行
echo         self.select_button.configure(state="disabled")
echo         self.output_text.delete("1.0", "end")  # 出力テキストをクリア
echo         self.settings_frame.pack_forget()  # 動画圧縮開始時に設定を隠す
echo         threading.Thread(target=self.compress_videos, daemon=True).start()
echo     def update_output(self, text):
echo         self.output_text.insert("end", text + "\n")
echo         self.output_text.see("end")  # 最新の出力が見えるようにスクロール
echo     def update_gui(self):
echo         """GUIの更新を担当するメソッド"""
echo         try:
echo             # キューからメッセージを取り出して処理
echo             while not self.message_queue.empty():
echo                 msg_type, msg_data = self.message_queue.get_nowait()
echo                 if msg_type == "output":
echo                     self.output_text.insert("end", msg_data + "\n")
echo                     self.output_text.see("end")
echo                 elif msg_type == "progress":
echo                     file_index, progress = msg_data
echo                     self.total_progress_bar.set(file_index / len(self.selected_files))
echo                     self.current_progress_bar.set(progress)
echo                 elif msg_type == "status":
echo                     self.status_label.configure(text=msg_data)
echo                 elif msg_type == "error":
echo                     self.status_label.configure(text=f"エラー: {msg_data}")
echo                     self.output_text.insert("end", f"エラー: {msg_data}\n")
echo                     self.output_text.see("end")
echo         except queue.Empty:
echo             pass
echo         # GUI更新を継続
echo         self.window.after(100, self.update_gui)
echo     def compress_videos(self):
echo         """FFMPEGの処理を担当するメソッド"""
echo         self.processing = True
echo         total_files = len(self.selected_files)
echo         for index, file_path in enumerate(self.selected_files):
echo             try:
echo                 input_path = Path(file_path)
echo                 if self.output_directory:
echo                     output_path = (
echo                         Path(self.output_directory) / f"Compressed_{input_path.name}"
echo                     )
echo                 else:
echo                     downloads_path = Path.home() / "Downloads"
echo                     output_path = downloads_path / f"Compressed_{input_path.name}"
echo                 # 動画の長さを取得
echo                 duration_cmd = [
echo                     "ffprobe",
echo                     "-v",
echo                     "error",
echo                     "-show_entries",
echo                     "format=duration",
echo                     "-of",
echo                     "default=noprint_wrappers=1:nokey=1",
echo                     str(file_path),
echo                 ]
echo                 duration = float(subprocess.check_output(duration_cmd).decode().strip())
echo                 # FFMPEGコマンドの実行
echo                 command = ["ffmpeg", "-i", str(file_path), "-map_metadata", "0"]
echo                 # エンコーダーの設定
echo                 encoder_mode = self.encoder_mode_var.get()
echo                 if (encoder_mode == "cpu_h264"):
echo                     command += ["-c:v", "libx264"]
echo                 elif (encoder_mode == "cpu_h265"):
echo                     command += ["-c:v", "libx265"]
echo                 else:  # hevc_nvenc
echo                     command += ["-c:v", "hevc_nvenc"]
echo                 # CRF値の設定
echo                 command += ["-crf", self.crf_value_var.get()]
echo                 if self.output_path_var.get():
echo                     # 出力名が指定されていればファイル名に使う
echo                     output_name = self.output_path_var.get()
echo                     if "." not in output_name:
echo                         output_name += f"_{input_path.name}"
echo                     output_path = (
echo                         Path(self.output_directory or downloads_path) / output_name
echo                     )
echo                 command += [
echo                     "-progress",
echo                     "pipe:1",
echo                     str(output_path),
echo                 ]
echo                 process = subprocess.Popen(
echo                     command,
echo                     stdout=subprocess.PIPE,
echo                     stderr=subprocess.PIPE,
echo                     universal_newlines=True,
echo                     bufsize=1,
echo                     encoding="utf-8",
echo                     errors="replace",
echo                 )
echo                 # 標準エラー出力の監視
echo                 def monitor_stderr():
echo                     for line in process.stderr:
echo                         self.message_queue.put(("output", line.strip()))
echo                 stderr_thread = threading.Thread(target=monitor_stderr, daemon=True)
echo                 stderr_thread.start()
echo                 # 進捗の監視
echo                 last_progress = 0
echo                 while True:
echo                     line = process.stdout.readline()
echo                     if not line:
echo                         break
echo                     if "out_time_ms=" in line:
echo                         try:
echo                             time_str = line.split("=")[1].strip()
echo                             if time_str != "N/A":
echo                                 time_ms = int(time_str) / 1000000
echo                                 progress = min(time_ms / duration, 1.0)
echo                                 if (
echo                                     abs(progress - last_progress) >= 0.01
echo                                 ):  # 1%以上の変化がある場合のみ更新
echo                                     self.message_queue.put(
echo                                         ("progress", (index, progress))
echo                                     )
echo                                     last_progress = progress
echo                         except (ValueError, IndexError):
echo                             continue
echo                 process.wait()
echo                 stderr_thread.join()
echo                 if process.returncode != 0:
echo                     self.message_queue.put(
echo                         ("error", f"{input_path.name}の処理中にエラーが発生しました")
echo                     )
echo                 # 現在のファイルの完了を通知
echo                 self.message_queue.put(("progress", (index + 1, 0)))
echo                 self.message_queue.put(("status", f"処理中: {index + 1}/{total_files}"))
echo             except Exception as e:
echo                 self.message_queue.put(("error", str(e)))
echo         # 処理完了の通知
echo         self.message_queue.put(("status", "すべての圧縮が完了しました!"))
echo         self.message_queue.put(("output", "処理が完了しました!"))
echo         self.processing = False
echo         self.window.after(0, self.compression_completed)
echo     def show_crf_tooltip(self, event):
echo         tooltip_text = (
echo             "CRF (Constant Rate Factor)は動画の品質を制御します。\n"
echo             "値が小さいほど高品質、大きいほど低品質になります。\n"
echo             "推奨値:\n"
echo             "- h.264: 18-28\n"
echo             "- h.265: 22-32\n"
echo             "※GPUエンコードの場合は効果が異なる場合があります"
echo         )
echo         x = event.widget.winfo_rootx() + event.widget.winfo_width()
echo         y = event.widget.winfo_rooty()
echo         self.tooltip_window = ctk.CTkToplevel()
echo         self.tooltip_window.wm_overrideredirect(True)
echo         self.tooltip_window.geometry(f"+{x}+{y}")
echo         label = ctk.CTkLabel(self.tooltip_window, text=tooltip_text, justify="left")
echo         label.pack(padx=5, pady=5)
echo     def hide_crf_tooltip(self, event):
echo         if self.tooltip_window:
echo             self.tooltip_window.destroy()
echo             self.tooltip_window = None
echo     def compression_completed(self):
echo         self.total_progress_bar.set(1)
echo         self.current_progress_bar.set(0)
echo         self.status_label.configure(text="すべての圧縮が完了しました!")
echo         self.select_button.configure(state="normal")
echo         self.update_output("処理が完了しました!")
echo         self.settings_frame.pack(pady=5)  # 処理完了時に再表示
echo     def run(self):
echo         self.window.mainloop()
echo if __name__ == "__main__":
echo     app = VideoCompressorApp()
echo     app.run()
) > video_compressor.py

echo "Pythonファイルの生成が完了しました"

echo "セットアップが完了しました!"
echo "video_compressor フォルダで python compressor.py を実行してください"
pause
exit /b 0

:error
echo エラーが発生しました
echo エラーコード: %errorlevel%
pause
exit /b 1

コード(自己責任で利用してくださいね!)

import customtkinter as ctk
import subprocess
import os
from pathlib import Path
import threading
import tkinter.filedialog as filedialog
import re
import queue
import time


class VideoCompressorApp:
    def __init__(self):
        self.window = ctk.CTk()
        self.window.title("かんたん動画圧縮ソフト")
        self.window.geometry("600x700")  # ウィンドウサイズを調整

        self.main_frame = ctk.CTkFrame(self.window)
        self.main_frame.pack(pady=20, padx=20, fill="both", expand=True)

        self.title_label = ctk.CTkLabel(
            self.main_frame, text="動画圧縮ツール", font=("Helvetica", 24)
        )
        self.title_label.pack(pady=(20, 10))  # 下側のpadyを小さくして詰める

        # 設定フレームをタイトルの直後に配置
        self.settings_frame = ctk.CTkFrame(self.main_frame)
        self.settings_frame.pack(pady=(0, 10))  # タイトルとの間隔を調整

        # エンコーダー選択の変数
        self.encoder_mode_var = ctk.StringVar(value="cpu_h264")
        self.crf_value_var = ctk.StringVar(value="28")
        self.output_path_var = ctk.StringVar(value="")

        # エンコーダー選択用ラジオボタン
        encoders_frame = ctk.CTkFrame(self.settings_frame)
        encoders_frame.pack(pady=5, padx=10, fill="x")

        ctk.CTkLabel(encoders_frame, text="エンコーダー選択:").pack(pady=2)

        # ラジオボタンを横並びに変更
        radio_frame = ctk.CTkFrame(encoders_frame)
        radio_frame.pack(pady=2)

        self.cpu_h264_radio = ctk.CTkRadioButton(
            radio_frame,
            text="CPU (h.264)",
            variable=self.encoder_mode_var,
            value="cpu_h264",
        )
        self.cpu_h265_radio = ctk.CTkRadioButton(
            radio_frame,
            text="CPU (h.265)",
            variable=self.encoder_mode_var,
            value="cpu_h265",
        )
        self.gpu_hevc_radio = ctk.CTkRadioButton(
            radio_frame,
            text="GPU (HEVC)",
            variable=self.encoder_mode_var,
            value="hevc_nvenc",
        )

        self.cpu_h264_radio.pack(side="left", padx=10)
        self.cpu_h265_radio.pack(side="left", padx=10)
        self.gpu_hevc_radio.pack(side="left", padx=10)

        # CRF値の入力とツールチップ
        crf_frame = ctk.CTkFrame(self.settings_frame)
        crf_frame.pack(pady=5, padx=10, fill="x")

        crf_label = ctk.CTkLabel(crf_frame, text="CRF値 (0-51):")
        crf_label.pack(side="left", padx=5)

        self.crf_entry = ctk.CTkEntry(
            crf_frame, textvariable=self.crf_value_var, width=50
        )
        self.crf_entry.pack(side="left", padx=5)

        crf_help = ctk.CTkLabel(crf_frame, text="?", width=20)
        crf_help.pack(side="left", padx=5)

        # 出力先ディレクトリ選択
        output_frame = ctk.CTkFrame(self.settings_frame)
        output_frame.pack(pady=5, padx=10, fill="x")

        ctk.CTkLabel(output_frame, text="出力先:").pack(side="left", padx=5)
        self.output_path_entry = ctk.CTkEntry(
            output_frame, textvariable=self.output_path_var, width=300
        )
        self.output_path_entry.pack(side="left", padx=5)

        ctk.CTkButton(
            output_frame, text="参照", width=50, command=self.select_output_directory
        ).pack(side="left", padx=5)

        # ツールチップの設定
        self.tooltip_window = None
        crf_help.bind("<Enter>", self.show_crf_tooltip)
        crf_help.bind("<Leave>", self.hide_crf_tooltip)

        # ファイル選択ボタンを設定の下に配置
        self.select_button = ctk.CTkButton(
            self.main_frame, text="動画ファイルを選択", command=self.select_files
        )
        self.select_button.pack(pady=10)

        # ファイルリストを表示するテキストボックス
        self.file_list = ctk.CTkTextbox(self.main_frame, height=100, width=500)
        self.file_list.pack(pady=10)

        # 全体のプログレスバー
        self.total_progress_label = ctk.CTkLabel(self.main_frame, text="全体の進捗:")
        self.total_progress_label.pack(pady=(10, 0))
        self.total_progress_bar = ctk.CTkProgressBar(self.main_frame, width=400)
        self.total_progress_bar.pack(pady=(0, 10))
        self.total_progress_bar.set(0)

        # 現在のファイルのプログレスバー
        self.current_progress_label = ctk.CTkLabel(
            self.main_frame, text="現在のファイルの進捗:"
        )
        self.current_progress_label.pack(pady=(10, 0))
        self.current_progress_bar = ctk.CTkProgressBar(self.main_frame, width=400)
        self.current_progress_bar.pack(pady=(0, 10))
        self.current_progress_bar.set(0)

        # FFMPEGの出力表示用テキストボックス
        self.output_label = ctk.CTkLabel(self.main_frame, text="FFMPEG出力:")
        self.output_label.pack(pady=(10, 0))
        self.output_text = ctk.CTkTextbox(self.main_frame, height=100, width=500)
        self.output_text.pack(pady=10)

        # 状態表示ラベル
        self.status_label = ctk.CTkLabel(self.main_frame, text="")
        self.status_label.pack(pady=10)

        self.output_directory = None  # この変数は引き続き使用

        self.selected_files = []

        # キューの初期化
        self.message_queue = queue.Queue()
        self.processing = False

        # 定期的なGUI更新用のメソッド呼び出し
        self.window.after(100, self.update_gui)

    def select_files(self):
        # 複数ファイル選択に対応
        files = filedialog.askopenfilenames(filetypes=[("動画ファイル", "*.mp4 *.mov")])
        if files:
            self.selected_files = files
            self.file_list.delete("1.0", "end")
            for file in files:
                self.file_list.insert("end", f"{file}\n")
            # ファイル選択後すぐに圧縮開始
            self.start_compression()

    def select_output_directory(self):
        directory = filedialog.askdirectory()
        if directory:
            self.output_directory = directory
            self.status_label.configure(text=f"出力先: {directory}")

    def start_compression(self):
        # 圧縮処理を別スレッドで実行
        self.select_button.configure(state="disabled")
        self.output_text.delete("1.0", "end")  # 出力テキストをクリア
        self.settings_frame.pack_forget()  # 動画圧縮開始時に設定を隠す
        threading.Thread(target=self.compress_videos, daemon=True).start()

    def update_output(self, text):
        self.output_text.insert("end", text + "\n")
        self.output_text.see("end")  # 最新の出力が見えるようにスクロール

    def update_gui(self):
        """GUIの更新を担当するメソッド"""
        try:
            # キューからメッセージを取り出して処理
            while not self.message_queue.empty():
                msg_type, msg_data = self.message_queue.get_nowait()

                if msg_type == "output":
                    self.output_text.insert("end", msg_data + "\n")
                    self.output_text.see("end")
                elif msg_type == "progress":
                    file_index, progress = msg_data
                    self.total_progress_bar.set(file_index / len(self.selected_files))
                    self.current_progress_bar.set(progress)
                elif msg_type == "status":
                    self.status_label.configure(text=msg_data)
                elif msg_type == "error":
                    self.status_label.configure(text=f"エラー: {msg_data}")
                    self.output_text.insert("end", f"エラー: {msg_data}\n")
                    self.output_text.see("end")

        except queue.Empty:
            pass

        # GUI更新を継続
        self.window.after(100, self.update_gui)

    def compress_videos(self):
        """FFMPEGの処理を担当するメソッド"""
        self.processing = True
        total_files = len(self.selected_files)

        for index, file_path in enumerate(self.selected_files):
            try:
                input_path = Path(file_path)
                if self.output_directory:
                    output_path = (
                        Path(self.output_directory) / f"Compressed_{input_path.name}"
                    )
                else:
                    downloads_path = Path.home() / "Downloads"
                    output_path = downloads_path / f"Compressed_{input_path.name}"

                # 動画の長さを取得
                duration_cmd = [
                    "ffprobe",
                    "-v",
                    "error",
                    "-show_entries",
                    "format=duration",
                    "-of",
                    "default=noprint_wrappers=1:nokey=1",
                    str(file_path),
                ]
                duration = float(subprocess.check_output(duration_cmd).decode().strip())

                # FFMPEGコマンドの実行
                command = ["ffmpeg", "-i", str(file_path), "-map_metadata", "0"]

                # エンコーダーの設定
                encoder_mode = self.encoder_mode_var.get()
                if (encoder_mode == "cpu_h264"):
                    command += ["-c:v", "libx264"]
                elif (encoder_mode == "cpu_h265"):
                    command += ["-c:v", "libx265"]
                else:  # hevc_nvenc
                    command += ["-c:v", "hevc_nvenc"]

                # CRF値の設定
                command += ["-crf", self.crf_value_var.get()]

                if self.output_path_var.get():
                    # 出力名が指定されていればファイル名に使う
                    output_name = self.output_path_var.get()
                    if "." not in output_name:
                        output_name += f"_{input_path.name}"
                    output_path = (
                        Path(self.output_directory or downloads_path) / output_name
                    )

                command += [
                    "-progress",
                    "pipe:1",
                    str(output_path),
                ]

                process = subprocess.Popen(
                    command,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    universal_newlines=True,
                    bufsize=1,
                    encoding="utf-8",
                    errors="replace",
                )

                # 標準エラー出力の監視
                def monitor_stderr():
                    for line in process.stderr:
                        self.message_queue.put(("output", line.strip()))

                stderr_thread = threading.Thread(target=monitor_stderr, daemon=True)
                stderr_thread.start()

                # 進捗の監視
                last_progress = 0
                while True:
                    line = process.stdout.readline()
                    if not line:
                        break

                    if "out_time_ms=" in line:
                        try:
                            time_str = line.split("=")[1].strip()
                            if time_str != "N/A":
                                time_ms = int(time_str) / 1000000
                                progress = min(time_ms / duration, 1.0)
                                if (
                                    abs(progress - last_progress) >= 0.01
                                ):  # 1%以上の変化がある場合のみ更新
                                    self.message_queue.put(
                                        ("progress", (index, progress))
                                    )
                                    last_progress = progress
                        except (ValueError, IndexError):
                            continue

                process.wait()
                stderr_thread.join()

                if process.returncode != 0:
                    self.message_queue.put(
                        ("error", f"{input_path.name}の処理中にエラーが発生しました")
                    )

                # 現在のファイルの完了を通知
                self.message_queue.put(("progress", (index + 1, 0)))
                self.message_queue.put(("status", f"処理中: {index + 1}/{total_files}"))

            except Exception as e:
                self.message_queue.put(("error", str(e)))

        # 処理完了の通知
        self.message_queue.put(("status", "すべての圧縮が完了しました!"))
        self.message_queue.put(("output", "処理が完了しました!"))
        self.processing = False
        self.window.after(0, self.compression_completed)

    def show_crf_tooltip(self, event):
        tooltip_text = (
            "CRF (Constant Rate Factor)は動画の品質を制御します。\n"
            "値が小さいほど高品質、大きいほど低品質になります。\n"
            "推奨値:\n"
            "- h.264: 18-28\n"
            "- h.265: 22-32\n"
            "※GPUエンコードの場合は効果が異なる場合があります"
        )

        x = event.widget.winfo_rootx() + event.widget.winfo_width()
        y = event.widget.winfo_rooty()

        self.tooltip_window = ctk.CTkToplevel()
        self.tooltip_window.wm_overrideredirect(True)
        self.tooltip_window.geometry(f"+{x}+{y}")

        label = ctk.CTkLabel(self.tooltip_window, text=tooltip_text, justify="left")
        label.pack(padx=5, pady=5)

    def hide_crf_tooltip(self, event):
        if self.tooltip_window:
            self.tooltip_window.destroy()
            self.tooltip_window = None

    def compression_completed(self):
        self.total_progress_bar.set(1)
        self.current_progress_bar.set(0)
        self.status_label.configure(text="すべての圧縮が完了しました!")
        self.select_button.configure(state="normal")
        self.update_output("処理が完了しました!")
        self.settings_frame.pack(pady=5)  # 処理完了時に再表示

    def run(self):
        self.window.mainloop()


if __name__ == "__main__":
    app = VideoCompressorApp()
    app.run()

経緯

  • 契約しているクラウドストレージの容量が足りなくなってきた。
  • 確認すると、大量の動画があることに気付いた。
  • でも消したくないし…と思った。
  • 圧縮してローカルにおいておくっていうのも、画質落ちそうで嫌だし、いつでも確認できない。
  • そういえばffmpegラッパーのソフトを売ってちょっと燃えた人いたなと思い出す。
  • 無料で圧縮できそうだなと思い、ChatGPTやパープレと壁打ち。
  • 色々調整してもらいつつ、CustomTkinterで実装してもらった。コードは一ミリも書いていない。ははは。
    • 設定変更(ffmpegコマンド生成)
    • 複数ファイル逐次実行(さすがに並列だと重すぎて意味ないんじゃないかと思って。)
    • なぜかメタデータをコピーできない。なんで。


コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA