2016年1月21日木曜日

[動画][圧縮] 動画の一括分割・圧縮(Windows上、ffmpeg利用)

経緯:昨年(2015年)10月10日に撮影した盆踊り大会の映像2時間分を、曲ごとに約20ファイルに区分けすることになりました。
これまでは高解像度のデジタルビデオ動画ファイル(1080p、mp4)から
--
1. TMPegENC VMW5で動画を圧縮し画素数を減らす(486p、mp4、gop30)
2. 圧縮済の動画を再生しつつ、開始時間・終了時間と曲名をメモ書き
3. メモに基づいてAvidemuxで動画を無劣化切断しファイル名をつける
--
の順、または
--
1. 元動画を再生しつつ、開始時間・終了時間と曲名をメモ書き
2. メモに基づいてAvidemuxで動画を無劣化切断しファイル名をつける(元動画がmp4形式の場合のみ)
3. 分割した全ファイルをhandbrakeで圧縮し画素数を減らす
--
の順で曲ごとのファイルを作っていたのですが、無劣化切断にかなり手間がかかります。(それもあって今日まで放置していたのですが・・)
音声ファイルについては以前よりフリーソフト「wavez」等を使って、時間指定のみでとても楽に無劣化分割していました。
今回、動画ファイルについても同様に一括処理することを思いつきました。
新しい手順は次のようになります。
--
1. 元動画を再生しつつ、開始時間・終了時間と曲名をカンマ区切りでメモ書き
2. メモ書きファイルを右クリック → 送る →分割スクリプト起動コマンドを指定
--
元動画と同じフォルダ内に出力サイズ864x486、画質2000kbに変換しつつ分割し保存します。
DVDより画素数は高く、スマートフォンに入れれば手軽に滑らかな動画を楽しめます。
入手したばかりのChromecast2経由でテレビ画面上でもきれいに再生できました。

--------
材料:
1. Windows版ffmpeg
音質のよいfdk-aacのモジュールを含むffmegのバイナリは配布されていないため、次を参考に作成し、パスを通しました。
「FFmpegをWindows上でビルドする」
http://k-pi.hatenablog.com/entry/2015/10/25/220120

2. Windows版python2
ライブラリが充実したスクリプト言語です。インストールし、パスを通しておきます。

3. 時間指定メモファイル
以下のように開始点・終了点・分割後ファイル名を含むカンマ区切りファイルを作成しておきます。
先頭の行のファイル名部には圧縮前動画ファイルのフルパスを記述します。コメント(#・・)の記述も可能です。
「input.csv」(下例では2曲分のファイルを出力します)
--この下から--
0,0,C:\Users\myname\Videos\20151010_みんなで踊る盆踊り_1080p.mp4
00:5:37,00:08:45,と/東京音頭_in20151010みなとまつり.mp4
00:8:57,00:12:30,た/炭坑節_in20151010みなとまつり.mp4
--この上まで--

4. 分割スクリプト起動コマンド「Video2mp4a_486p.cmd」
--この下から--
cd /d "%~dp0"
python Video2mp4b_486p.py %1
@if "%ERRORLEVEL%"=="0" timeout /t 4
@if not "%ERRORLEVEL%"=="0" pause ERROR!! see abobe.
--この上まで--

5. 分割スクリプト「Video2mp4b_486p.py」(自作、pythonスクリプト)
--この下から--
#!/usr/bin/env python
# -*- coding: cp932 -*-

"""    ##
リストファイルに記述された時間指定とファイル名にしたがって、元動画ファイルを分割・圧縮・保存する。

第1引数: リストファイル
形式例:
0,0,C:\Users\myname\Videos\20151010_みんなで踊る盆踊り_1080p.mp4
00:5:37,00:08:45,と/東京音頭_in20151010みなとまつり.mp4
00:8:57,00:12:30,た/炭坑節_in20151010みなとまつり.mp4
"""    ##

import sys
import codecs
## MSコンソールで利用する場合のみ以下を有効にする(デフォルト指定はasciiらしい)
sys.stdin = codecs.getreader('cp932')(sys.stdin)
sys.stdout = codecs.getwriter('cp932')(sys.stdout)

## import宣言その他
import re
import os
import datetime
import locale
import commands
#import traceback

## 現在パスを得る
sWorkBase="G1K"
sNowFolder =os.path.dirname(__file__)

##引数(sjis)を読み込む
aArg =sys.argv
#print("len(aArg)=%i, aArg[0]=%s" % (len(aArg), aArg[0]))

##適切な時間指定パターンを定義する
re_hms =re.compile('^([0-9]+)(:?)([0-9]*)(:?)([0-9]*)$')
##適切なファイル名パターンを定義する
re_pattern_mp4 =re.compile('.mp4$', re.I)
##適切なコメント行パターンを定義する
re_comment =re.compile('^#')
##処理中断・終了用パターンを定義する
re_exit =re.compile('^#exit', re.I)

##各行分析後格納配列
aData=[]

##時間指定文字列から秒数を得る関数の定義
def fGetSecFromHms(hms):
  global re_hms
  iRet =0
  res =re_hms.search(hms)
  if res:
    if res.group(5)!="":
      iRet =int(res.group(1))*3600 +int(res.group(3))*60 +int(res.group(5))
    elif res.group(3)!="":
      iRet =int(res.group(1))*60 +int(res.group(3))
    else:
      iRet =int(res.group(1))
  return(iRet)

##mpeg変換オプションを指定する
aFfmpegOption =[ [
    u'-y'    # OverWrite
  ,u'-ss %d'    # set the start time offset
  ,u'-i \"%s\"'    # infile
  ,u'-f mp4' # ファイルフォ ーマット(コンテナ)
  ,u'-vcodec libx264'    # ビデオ(CODEC・関連オ プション)
  ,u'-maxrate 2000k -bufsize 2000k'    # ビデ オビットレート
  ,u'-g 30'    # GOP サイズ
  ,u'-r 30000/1001'    # フレームレート
  ,u'-aspect 16:9'    # アスペクト比
  ,u'-s 864x486'    # 解像度
  ,u'-acodec libfdk_aac'    # オーディオ(CODEC・関連オプション)
  ,u'-ab 128k'    # オーディオビットレー ト
  ,u'-to %d'    # record or translate stop time
  ,u'\"%s\"'    # outfile
],[
    u'-y'    # OverWrite
  ,u'-ss %d'    # set the start time offset
  ,u'-i \"%s\"'    # infile
  ,u'-f mp4' # ファイルフォ ーマット(コンテナ)
  ,u'-vcodec libx264'    # ビデオ(CODEC・関連オ プション)
  ,u'-maxrate 2000k -bufsize 2000k'    # ビデ オビットレート
  ,u'-g 30'    # GOP サイズ
  ,u'-r 30000/1001'    # フレームレート
  ,u'-aspect 4:3'    # アスペクト比
  ,u'-s 640x480'    # 解像度
  ,u'-acodec libfdk_aac'    # オーディオ(CODEC・関連オプション)
  ,u'-ab 128k'    # オーディオビットレー ト
  ,u'-to %d'    # record or translate stop time
  ,u'\"%s\"'    # outfile
] ]
iEncFormatNum =0    #0の場合は16:9、1の場合は4:3のアスペクト比でエンコードする

##諸変数を初期化し処理を開始する
sInVideoPathName =""
sInCsvPathname =""
sOutVideoDir =""
sErrMes =""
sWarningMes =""
sCheck ="NA"
try:
  # 引数の数が不適切な場合は処理を中断する
  if len(aArg)!=2:
    raise Exception("010",u"引数の数(%d)が適切ではありません。\n%s/" % (len(aArg), "/\n".join(aArg)))
  # 引数ファイルが存在しない場合は処理を中断する
  sInCsvPathname =aArg[1]
  if not re.search(r'\\', sInCsvPathname):
    sInCsvPathname =sNowFolder +'\\' +sInCsvPathname
    print("sInCsvPathname=%s" % sInCsvPathname)
  if not (os.path.isfile(sInCsvPathname)):
    raise Exception("020", u"引数で指定したファイルが存在しません: %s" % sInCsvPathname)
  # 引数ファイルの内容を取り出し、行に分解する
  sIn =codecs.open(aArg[1], 'r', 'cp932', 'replace').read()
  aIn =sIn.splitlines()

  #各行をカンマで区切り、開始時、終了時、ファイル名に分解し、配列に格納する
  for i in range(0, len(aIn)):
    iStart =0
    iEnd =0
    sOutVideoName =""
    aIn[i] =re.sub(r"^#*[ \t]*$", "", aIn[i])
    aIn[i] =re.sub(r"[,  ]+$", "", aIn[i])
    aLine=aIn[i].split(",")
    #各データの前後のダブルクォート文字があれば除去する
    for j in range(0, len(aLine)):
      aLine[j] =re.sub(r'^"', '', aLine[j])
      aLine[j] =re.sub(r'"$', '', aLine[j])
    #exitで終わる行で処理を中断し終了する
    if re_exit.search(aLine[0]):
      break;
    #空行あるいはコメント行の場合は処理をスキップする
    if (aLine[0]=="") or (re_comment.search(aLine[0])):
      print("skip.")
      continue
    #カンマ区切りデータ数が不適切な場合は処理を中断する
    if len(aLine) !=3:
      raise Exception("030", u"第%d行にてカンマ区切りが不適切です。[%s]" % ((i+1), aIn[i]))
    #開始時秒数を得る
    if re_hms.search(aLine[0]):
      iStart =fGetSecFromHms(aLine[0])
    #時分秒の指定が不適切な場合は処理を中断する
    else:
      raise Exception("040", u'第%d行にて開始時の指定が不適切です。[%s (%s)]"' % ((i+1), aLine[0], aLine[2]))
    #終了時秒数を得る
    if re_hms.search(aLine[1]):
      iEnd =fGetSecFromHms(aLine[1])
      # 終了時の指定が開始時より先の場合は警告メッセージを追加する
      if iStart >iEnd and iEnd!=fGetSecFromHms(0):
       sWarn =u"警告:第%d行にて開始時と終了時の大小が不適切です。[%s,%s (%s)]\n" % ((i+1), aLine[0], aLine[1], aLine[2])
       sWarningMes =sWarningMes +sWarn
       continue
    #時分秒の指定が不適切な場合は処理を中断する
    else:
      raise Exception("050", u'第%d行にて終了時の指定が不適切です。[%s (%s)]"' % ((i+1), aLine[1], aLine[2]))
    #出力先ファイル名を得る
    sOutVideoName =aLine[2] +("" if re_pattern_mp4.search(aLine[2]) else ".mp4")
    #print(u"開始秒=%d, 終了秒=%d, 出力先名=%s" % (iStart, iEnd, sOutVideoName))
    aData.append([iStart, iEnd, sOutVideoName])
    if iStart==fGetSecFromHms("0") and iEnd==fGetSecFromHms("0"):    # 0,0,で始まる行は入力ファイル指定。
      sInVideoPathName =sOutVideoName
      #入力動画ファイルが存在しない場合は処理を中断する
      if not (os.path.isfile(sInVideoPathName)):
       raise Exception("060", u"入力元動画ファイル %s が存在しません。" % sInVideoPathName)
      sOutVideoDir =re.sub(r'\\[^\\]+$', '', sInVideoPathName)
    else:
      iEncFormatNum=0
      # 入力ファイル名に "-1609-"が含まれる場合はアスペクト比 16:9でエンコード
      if re.search(u"-1609-", sInVideoPathName):
       iEncFormatNum=0
      # 入力ファイル名に "-0403-"が含まれる場合はアスペクト比 4:3でエンコード
      elif re.search(u"-0403-", sInVideoPathName):
       iEncFormatNum=1
      sCmd =(("ffmpeg.exe " +" ".join(aFfmpegOption[iEncFormatNum])) % (iStart, sInVideoPathName, iEnd -iStart, sOutVideoDir +'\\' +sOutVideoName))
      print (sCmd)
      sCmd2 =sCmd.encode('cp932', 'replace')
      os.system(sCmd2)
  if sWarningMes !="":
    print ("Warning:\n%s" % sWarningMes)
    sys.exit(1)
  sys.exit(0)

except Exception, inst:
  #print(inst.args)
  print ("%s" % (" ".join(inst.args)))
  sys.exit(1)
--この上まで--

上記 4.と5.のファイルは共にメモ帳で作成し、C:\Users\(ユーザ名)\AppData\Roaming\Microsoft\Windows\SendTo の中に入れておけばokです。
なお、アニメ動画の圧縮には未対応です。フレームレート自動識別をさせればすむ話ですが。

以上。

0 件のコメント:

コメントを投稿