どこでも見れるメモ帳

とあるSEの備忘録。何かあれば気軽にコメントください〜

ffmpegで動画ファイルの静止画サムネイルを作成する

はじめに

「動画ファイルが多量に存在するけど,ファイル名が適当すぎて目的のファイルをなかなか見つけられない」状況が生じたのでメモ.
方法をざっくり言うと,特定ディレクトリ内のすべての動画ファイルについて,N x Nマスの静止画サムネイルを作成*1する.

やりかた

#!/bin/sh
dir_path="." #1
tiles_h=8 #4
tiles_v=8 #4
tiles=$(($tiles_h * $tiles_v))
for file in `find $dir_path -type f \( -name "*.flv" -o -name "*.mp4" -o -name "*.avi" \)`
do 
  frames=`ffmpeg -i "$file" 2>&1 | awk '/totalframes/{print $3}'`
  if [ "$frames" = "" ]; then 
    continue;
  else
    fps=$(($frames / $tiles)); #3
    if [ $fps -gt 1000 ]; then #2
      fps=1000;
    fi
    ffmpeg -y -i "$file" -vf thumbnail=${fps},tile=${tiles_h}x${tiles_v},scale=1920:-1 "${file%.*}_thumb.jpg"; #3
  fi
done

#1: サムネイル作成対象のディレクトリ.
#2: ffmpegのバグ?か分からないが,-vfオプションのthumbnail値であまりにも大きな値(例えば5000等)を設定するとエラーで落ちるため,1000を上限閾値とした.
#3: ここで静止画サムネイルを作成.出力サイズはscale値で設定し,-1の場合はソースから自動決定する.
#4: サムネイルの分割数.
ワンラインで書くと次の通り.コピペで便利.

$ for file in `find . -type f \( -name "*.flv" -o -name "*.mp4" -o -name "*.avi" \)`; do frames=`ffmpeg -i "$file" 2>&1 | awk '/totalframes/{print $3}'`; if [ "$frames" = "" ]; then continue; else (fps=$(($frames/64)); if [ $fps -gt 1000 ]; then fps=1000; fi; ffmpeg -y -i "$file" -vf thumbnail=$fps,tile=8x8,scale=1920:-1 "${file%.*}_thumb.jpg") fi; done

補足

定期的に実行する場合は,下記内容を「/home/ni66ling/local/bin/mk_thumbnail.sh」とかに保存して,

#!/bin/sh
dir_path="."
tiles_h=8
tiles_v=8
tiles=$(($tiles_h * $tiles_v))
for file in `find $dir_path -type f \( -name "*.flv" -o -name "*.mp4" -o -name "*.avi" \)`
do 
  frames=`ffmpeg -i "$file" 2>&1 | awk '/totalframes/{print $3}'`
  if [ "$frames" = "" ]; then 
    continue;
  else
    fps=$(($frames / $tiles));
    if [ $fps -gt 1000 ]; then
      fps=1000;
    fi
    ffmpeg -y -i "$file" -vf thumbnail=${fps},tile=${tiles_h}x${tiles_v},scale=1920:-1 "${file%.*}_thumb.jpg";
  fi
done

cronで定期実行するように設定してあげれば良さそう.

00  3  *  *  * /home/ni66ling/local/bin/mk_thumbnail.sh

まだ試してないけど,あとで試す予定.*2

追記 2014/10/12

上記でうまくいかないケースがあった*3ので,その対処を行った.
全フレーム数の取得を,totalframesから取得するのではなく,durationとfpsから算出するように変更した.
それと,ログ出力と,不要サムネイルの削除*4を追加した.

#!/bin/sh

input_dir_path="."                                                          # 入力ディレクトリパス
output_dir_path="${input_dir_path}/thumbs/"                                 # 出力ディレクトリパス
log_file_path="${output_dir_path}/mk_thumbnail_`date '+%Y%m%d%H%M%S'`.log"  # ログファイルパス

ffmpeg="./ffmpeg"  # ffmpegバイナリパス
find="/bin/find"   # findバイナリパス

tiles_h=8          # サムネイルタイルの水平方向数
tiles_v=8          # サムネイルタイルの垂直方向数

# 出力ディレクトリ生成
if [ ! -e "$output_dir_path" ]; then
  mkdir -p "$output_dir_path"
fi

# サムネイル作成対象のファイル数の取得
IFS=$'\n';
input_files_cmd='$find $input_dir_path -maxdepth 1 -type f \( -name "*.flv" -o -name "*.mp4" -o -name "*.avi" -o -name "*.ts" -o -name "*.wmv" \)'
nof_files=`eval "$input_files_cmd" | wc -l`
echo "#input files: $nof_files" | tee $log_file_path

counter=1
for file in `eval "$input_files_cmd"`; do
  IFS=$' \t\n'
    
  printf "processing:$file ($counter/$nof_files) is started... " | tee -a $log_file_path
  counter=$(($counter + 1))

  # 既にサムネイルがあるならスキップ
  input_filename="${file##*/}"
  output_filepath="${output_dir_path}${input_filename%.*}_thumb.jpg"
  if [ -e  "$output_filepath" ]; then
    printf "already existing.\n" | tee -a $log_file_path
    continue
  fi
  
  # 動画フレーム数の計算(Frames = Duration * FPS)
  hms=(`echo \`$ffmpeg -i "$file" 2>&1 | awk '/Duration/{print $2}' | sed 's/\..*//g' | tr -s ':' ' ' \` `)
  if [ ${#hms[*]} -eq 0 ]; then
    printf "error (duration is not defined).\n" | tee -a $log_file_path
    continue
  fi
  duration_sec=`expr ${hms[0]} \* 3600 + ${hms[1]} \* 60 + ${hms[2]} \* 1`
  src_fps=`$ffmpeg -i "$file" 2>&1 | awk '/fps/{print $0}' | sed 's/\ fps.*//' | sed 's/.*\ //'`
  if [ "$src_fps" = "" ]; then
    printf "error (fps is not defined).\n" | tee -a $log_file_path
    continue
  fi
  frames=`echo "$duration_sec * $src_fps" | bc | sed 's/\..*//g'`

  # サムネイル作成処理
  tiles=$(($tiles_h * $tiles_v))
  thumb_fps=`expr $frames \/ $tiles` 
  if [ $thumb_fps -gt 1000 ]; then 
    thumb_fps=1000
  fi
  ffmpeg_msg=`$ffmpeg -y -i "$file" -vf thumbnail=${thumb_fps},tile=${tiles_h}x${tiles_v},scale=1920:-1 "$output_filepath" \
    2>&1 > /dev/null | grep -E 'error|failed' | tr -d '\n'`
  if [ "$ffmpeg_msg" = "" ]; then
    printf "succeeded.\n" | tee -a $log_file_path
  else
    printf "error (ffmpeg:$ffmpeg_msg)\n" | tee -a $log_file_path
  fi
done
printf "making thumbnail is done !!\n\n"
printf "================================================================================\n\n"

# 出力ディレクトリにおけるサムネイルについて,対応するソースの動画が存在しないなら,サムネイルを削除
echo "checking existing thumbnails."
IFS=$'\n';
input_files_cmd='$find $output_dir_path -maxdepth 1 -mindepth 1 -regex ".*_thumb\.jpg$"'
nof_files=`eval "$input_files_cmd" | wc -l`
echo "#thumbnail files: $nof_files" | tee -a $log_file_path

counter=1
for file in `eval "$input_files_cmd"`; do
  counter=$(($counter + 1))
  video_file_name=`echo $file | sed 's/.*\/\([^\/]*\)_thumb\.jpg/\1/' | sed 's/\(\[\|\]\|\*\|\?\)/\\\\\1/g'`
  find_video_cmd_result=`$find $input_dir_path -maxdepth 1 -mindepth 1 -name "${video_file_name}.*"`
  if [ "" = "$find_video_cmd_result" ]; then
    rm -f "$file"
    printf "source video file of thumbnail:${file} is missing. therefore, this thumbnail is removed.\n" | tee -a $log_file_path
  fi
done
printf "complete !!"

ソースコードGitHubにおいています. https://github.com/KenshoFujisaki/MakeVideoThumbnail

*1:ffmpegを用いる

*2:そのときは,findの-mtimeオプションだけじゃなくて,${file%.*}_thumb.jpgが既に存在するかをチェックするようにしないと….

*3:ffmpeg -i $fileしたときに,totalframesが出ないケース.ただしdurationは存在する.

*4:サムネイル作成後に動画を削除した場合、追従してサムネイルが削除されるように