はじめに
動画ファイルを特定のサイズに圧縮したいとき、「だいたいこのくらい」ではなく「正確に50MBにしたい」というケースがあります。例えば:
- メール添付の容量制限に収めたい
- クラウドストレージの容量を節約したい
- SNSのアップロード制限に合わせたい
既存のツールでは目標サイズを指定しても実際のサイズがずれることが多く、試行錯誤が必要でした。そこで、目標サイズを正確に実現し、音質を優先する動画圧縮CLIツールを作成しました。
このツールの特徴
1. 目標サイズを正確に実現
動画の長さから必要なビットレートを逆算し、目標サイズに対して±5%以内の精度で圧縮します。
目標サイズ: 50.00 MB
実際のサイズ: 49.85 MB
差分: 0.15 MB
2. 音質優先の設計
音声ビットレートを192kbps(AAC)に固定し、ビデオビットレートを調整することで音質を優先します。
3. ドライランモード
実際の圧縮前に結果をプレビューできます。予想画質や必要な時間を事前に確認可能です。
./compress_video.py --dry-run
4. バッチ処理対応
ディレクトリを指定するだけで、配下の全動画ファイルを一括処理できます。
5. 2パスエンコーディング
高品質な圧縮を実現するため、2パスエンコーディングを採用しています。
使い方
インストール
# ffmpegのインストール
brew install ffmpeg
# リポジトリのクローン
git clone https://github.com/hiroki-abe-58/video-compressor.git
cd video-compressor
chmod +x compress_video.py
基本的な使い方
./compress_video.py
対話形式で以下を入力します:
- 動画ファイルのパス(またはディレクトリ)
- 目標サイズ(MB)
- 拡張子変換の有無
ドライラン
実際の圧縮前に結果を確認できます:
./compress_video.py --dry-run
出力例:
ドライラン結果
============================================================
入力ファイル: video.mp4
現在のサイズ: 150.50 MB
目標サイズ: 50.00 MB
圧縮率: 66.8%
【エンコード設定】
ビデオビットレート: 1145 kbps
音声ビットレート: 192 kbps (AAC)
【予想画質】
高画質 (軽微な劣化)
============================================================
バッチ処理
ディレクトリ内の全動画を一括処理:
./compress_video.py
# ディレクトリパスを入力
> /path/to/videos/
# 一括設定または個別設定を選択
技術的なポイント
1. ビットレート計算アルゴリズム
目標ファイルサイズから必要なビットレートを逆算します:
def calculate_bitrate(self, target_size_mb: float, duration: float,
audio_bitrate: int = 192) -> int:
# 目標サイズ(MB) -> bits
target_size_bits = target_size_mb * 8 * 1024 * 1024
# 音声ビットレート(kbps) -> bits/sec
audio_bitrate_bps = audio_bitrate * 1000
# 音声トータルサイズ
audio_total_bits = audio_bitrate_bps * duration
# ビデオに割り当て可能なサイズ
video_total_bits = target_size_bits - audio_total_bits
if video_total_bits <= 0:
raise ValueError("目標サイズが小さすぎる")
# ビデオビットレート(bps) -> kbps(余裕を持たせて95%)
video_bitrate_bps = video_total_bits / duration
return int(video_bitrate_bps / 1000 * 0.95)
ポイント:
- 音声サイズを先に確保し、残りをビデオに割り当てる
- 95%の係数でマージンを持たせ、目標サイズを超えないようにする
2. 画質レベルの推定
解像度とビットレートから画質を推定します:
def estimate_quality_level(self, video_bitrate: int, video_info: dict) -> str:
# 動画ストリームから解像度取得
video_stream = next(
(s for s in video_info['streams'] if s['codec_type'] == 'video'),
None
)
height = video_stream.get('height', 0)
# 解像度ベースの推奨ビットレート(YouTube基準)
if height >= 1080: # Full HD
excellent = 8000
good = 5000
acceptable = 3000
elif height >= 720: # HD
excellent = 5000
good = 2500
acceptable = 1500
# ...
# 判定
if video_bitrate >= excellent:
return "最高画質 (ほぼ劣化なし)"
elif video_bitrate >= good:
return "高画質 (軽微な劣化)"
elif video_bitrate >= acceptable:
return "標準画質 (許容範囲)"
else:
return "低画質 (明らかに劣化)"
YouTubeの推奨ビットレートを参考に、4K/2K/FHD/HD/SDごとに異なる基準で判定しています。
3. 2パスエンコーディング
高品質な圧縮のため、2パスエンコーディングを採用:
# 1パス目: ビットレート配分を解析
pass1_cmd = [
'ffmpeg',
'-i', str(input_path),
'-c:v', 'libx264',
'-b:v', f'{video_bitrate}k',
'-pass', '1',
'-an', # 音声なし
'-f', 'null',
'/dev/null'
]
# 2パス目: 最適化されたエンコーディング
pass2_cmd = [
'ffmpeg',
'-i', str(input_path),
'-c:v', 'libx264',
'-b:v', f'{video_bitrate}k',
'-pass', '2',
'-c:a', 'aac',
'-b:a', f'{audio_bitrate}k',
'-y',
str(output_path)
]
2パスエンコーディングの利点:
- 1パス目でビットレート配分を分析
- 2パス目で最適化されたエンコーディング
- シングルパスより高品質だが時間がかかる
4. プログレスバー表示
ffmpegの出力をパースしてリアルタイムに進捗を表示:
def _run_ffmpeg_with_progress(self, cmd: list, phase: str, video_info: dict):
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True
)
duration = float(video_info['format']['duration'])
while True:
line = process.stderr.readline()
if not line and process.poll() is not None:
break
# timeパラメータから進捗を取得
time_match = re.search(r'time=(\d{2}):(\d{2}):(\d{2}\.\d{2})', line)
if time_match:
hours, minutes, seconds = time_match.groups()
current_time = int(hours) * 3600 + int(minutes) * 60 + float(seconds)
progress = min(100, (current_time / duration) * 100)
# プログレスバー生成
bar_length = 40
filled = int(bar_length * progress / 100)
bar = '█' * filled + '░' * (bar_length - filled)
# 残り時間計算
if progress > 0:
elapsed = current_time
total_estimated = (elapsed / progress) * 100
remaining = total_estimated - elapsed
remaining_str = self._format_time(remaining)
print(f'\r{phase}: [{bar}] {progress:5.1f}% | 残り時間: {remaining_str}',
end='', flush=True)
5. エラーハンドリング
想定されるエラーを全て検出し、わかりやすいメッセージを表示:
# ファイル存在チェック
if not path.exists():
print(f"エラー: 存在しないパスです。正しいパスを入力してください。")
continue
# サイズの妥当性チェック
if target_size >= current_size:
print(f"エラー: 目標サイズ({target_size:.2f}MB)が"
f"現在のサイズ({current_size:.2f}MB)以上です。")
continue
# 音声サイズチェック
audio_size_mb = (192 * 1000 * duration) / (8 * 1024 * 1024)
if target_size < audio_size_mb * 1.1:
print(f"警告: 目標サイズが小さすぎる可能性があります。")
print(f"音声ビットレート192kbpsだけで約{audio_size_mb:.2f}MBになります。")
confirm = input("それでも続けますか? (y/n): ").strip().lower()
if confirm != 'y':
continue
設計の工夫
DRY原則の徹底
圧縮処理を_compress_and_report()
メソッドに共通化し、単体モードとバッチモードで重複を排除:
def _run_single_mode(self):
"""単体ファイルモード"""
input_path = self.input_files[0]
video_info = self.get_video_info(input_path)
target_size_mb = self._phase2_get_target_size(input_path, video_info)
output_format = self._phase3_convert_format(input_path)
# 共通化されたメソッドを使用
self._compress_and_report(input_path, target_size_mb,
output_format, video_info)
def _batch_mode_uniform(self):
"""一括設定モード"""
# ... 設定取得 ...
for i, input_path in enumerate(self.input_files, 1):
video_info = self.get_video_info(input_path)
# 同じメソッドを使用
self._compress_and_report(input_path, target_size_mb,
output_format, video_info, i, total)
単一責任の原則
各メソッドは単一の責任を持つように設計:
メソッド | 責任 |
---|---|
get_video_info() |
動画情報取得 |
calculate_bitrate() |
ビットレート計算 |
compress_video() |
圧縮実行 |
estimate_quality_level() |
画質推定 |
_dry_run_report() |
ドライラン結果表示 |
型ヒントの活用
Python 3.8以上の型ヒントを活用し、コードの可読性と保守性を向上:
from typing import Optional, List, Tuple
from pathlib import Path
def calculate_bitrate(
self,
target_size_mb: float,
duration: float,
audio_bitrate: int = 192
) -> int:
# ...
def get_video_files_from_directory(self, directory: Path) -> List[Path]:
# ...
パフォーマンス
処理時間
2パスエンコーディングのため、シングルパスより時間がかかります:
動画の長さ | 解像度 | 処理時間(目安) |
---|---|---|
5分 | 1080p | 約10-15分 |
15分 | 1080p | 約30-40分 |
30分 | 1080p | 約1時間 |
プログレスバーで進捗と残り時間を確認できます。
精度
目標サイズに対する精度:
ファイルサイズ | 精度 |
---|---|
50MB以上 | ±2-5% |
10-50MB | ±3-7% |
10MB未満 | ±5-10% |
95%の係数により、目標サイズを超えることはほぼありません。
今後の改善予定
- ログ出力機能(処理履歴の保存)
- プリセット機能(高画質/標準/低容量モード)
- 設定ファイル対応(デフォルト設定の保存)
- GPU加速対応(エンコード高速化)
- pytestによるテストコード追加
- CI/CD構築(GitHub Actions)
まとめ
動画を正確なサイズに圧縮するCLIツールを作成しました。主な特徴は:
- 目標サイズを±5%以内の精度で実現
- 音質優先(192kbps AAC)
- ドライランモードで事前確認
- バッチ処理対応
- 2パスエンコーディングによる高品質圧縮
ffmpegをラップしたシンプルなツールですが、以下の工夫により実用的なツールになりました:
- ビットレート逆算アルゴリズム
- 解像度ベースの画質推定
- 包括的なエラーハンドリング
- DRY原則に基づく設計
- 型ヒントによる保守性向上
GitHubで公開していますので、興味がある方はぜひ試してみてください。