FFmpegで一括処理したい

まえがき

前前回 WebM作りました。
前回 画像キャプチャしました。
複数ファイルを自動で処理させましょう。

手順

  1. 同じビットレートのWebMを作る
  2. 固定サイズの格子状サムネイルを作る

同じビットレートのWebM

ffprobeの結果より、
VideoおよびAudioのビットレート(xxx kb/s)を取得できるので、 数字を切り出し変換への入力とする。

固定サイズの格子状サムネイル

動画の長さによらず固定サイズ、固定枚数のキャプチャを取得する必要がある。
ffprobeの結果より、
動画の長さ(duration)を取得できるのでキャプチャ回数(枚数)で割ればキャプチャ間隔が得られる。
出力サイズは、フィルタ設定(-vfオプション)で指定できる。

ソースコード

#!/usr/bin/env python3
# coding: utf-8

import os
import pathlib
import subprocess


def ffprobe(filepath):
    """ffprobe 実行結果を返す"""
    command = ['ffprobe', str(filepath)]
    cp = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if cp.returncode != 0:
        print('ffprobe error: ')
        print(cp.stdout.decode('utf-8'))
        print(cp.stderr.decode('utf-8'))
        return None

    # stderrに出力されている。改行で分割
    return cp.stderr.decode('utf-8').split('\n')    


def ffmpeg_webm(filepath, fileinfo):
    """ffmpeg で WebM に変換する"""
    # 出力先
    dstpath = pathlib.Path(os.path.abspath(__file__)).with_name(filepath.stem + '.webm')

    # ビットレートを得る
    for line in fileinfo:
        line = line.rstrip().lstrip()
        if line.startswith('Stream'):
            _, index, stream, info = line.split(':')
            for item in info.split(','):
                if item.endswith('kb/s'):
                    bitrate = int(item.split()[0])

            if stream.endswith('Audio'):
                # 96 kbps で十分らしいが
                #if bitrate > 96:
                #    bitrate = 96
                bitrate_audio = bitrate

            elif stream.endswith('Video'):
                bitrate_video = bitrate

    # 変換コマンド
    command_pass1 = ['ffmpeg',
                     '-i', str(filepath),
                     '-c:v', 'libvpx-vp9', '-pass', '1', '-b:v', '{0}k'.format(bitrate_video),
                     '-threads', '8', '-speed', '4',
                     '-tile-columns', '6', '-frame-parallel', '1',
                     '-an',
                     '-f', 'webm',
                     '-y', '/dev/null']
    command_pass2 = ['ffmpeg',
                     '-i', str(filepath),
                     '-c:v', 'libvpx-vp9', '-pass', '2', '-b:v', '{0}k'.format(bitrate_video),
                     '-threads', '8', '-speed', '1',
                     '-tile-columns', '6', '-frame-parallel', '1',
                     '-auto-alt-ref', '1', '-lag-in-frames', '25',
                     '-c:a', 'libopus', '-b:a', '{0}k'.format(bitrate_audio),
                     '-f', 'webm',
                     '-y', str(dstpath)]
    cp = subprocess.run(command_pass1)
    if cp.returncode != 0:
        print('ffmpeg error: pass1')
        return None

    cp = subprocess.run(command_pass2)
    if cp.returncode != 0:
        print('ffmpeg error: pass2')
        return None

    return dstpath


def thumbnail_tile(filepath, fileinfo):
    """格子状サムネイルを作成する"""
    # 4x6
    tile_w = 4
    tile_h = 6
    # 144p
    scale_x = 256
    scale_y = 144
    # 出力先
    imgpath = pathlib.Path(os.path.abspath(__file__)).with_name(filepath.stem + '.jpg')

    # 動画の長さからキャプチャ間隔を決定する
    for line in fileinfo:
        line = line.rstrip().lstrip()
        if line.startswith('Duration'):
            hh, mm, ss = line.split(',')[0].split()[1].split(':')
            duration = int(hh) * 3600 + int(mm) * 60 + int(float(ss)) + 1
            rate = 1 + duration // (tile_w * tile_h)

    # 変換コマンド
    fps_filter = 'fps=fps=1/{0}'.format(rate)
    # 高さを固定とする
    scale_filter = 'scale={0}*iw/ih:{0}'.format(scale_y)
    tile_filter = 'tile={0}x{1}'.format(tile_w, tile_h)
    # アスペクト比を揃えたい場合はpadする
    #pad_filter = 'pad={0}:{1}:(ow-iw)/2:0:black'.format(scale_x, scale_y)
    #vf_filter = ','.join([fps_filter, scale_filter, pad_filter, tile_filter])
    vf_filter = ','.join([fps_filter, scale_filter, tile_filter])
    command = ['ffmpeg',
               '-i', str(filepath),
               '-vf', vf_filter,
               '-y', str(imgpath)]
    cp = subprocess.run(command)
    if cp.returncode != 0:
        print('ffmpeg error: capture')
        return None

    return imgpath


def main():
    # 指定のディレクトリ以下のファイルを全て処理する
    working_directory = pathlib.Path('/path/to/video')
    for srcpath in working_directory.glob('**/*'):
        probe = ffprobe(srcpath)
        if probe is not None:
            dst = ffmpeg_webm(srcpath, probe)
            img = thumbnail_tile(srcpath, probe)


if __name__ == '__main__':
    main()

補足

scale=するときアスペクト比が変わってしまうと単純な拡大縮小にならず、変形(縦横どちらかに間延び)されてしまう。
そんなときはpad=と組み合わせると映像/画像部分はアスペクト比を保ったまま、余白を埋めることでアスペクト比を変更して出力できる。

参考:


See also