エンコード失敗などで生じた不良のファイルを見つけ出すためもあり、ビデオ情報を一括取得するツールを作りました。
pythonからffmpegを呼び出して動画情報を取得しています。
調べたいフォルダまたはファイルを右クリックし、「送る」の中にある下記分割スクリプト起動コマンドを指定すると、拡張子が mts、ts、mpg、mp4 等のファイルを対象に次の情報をとり、csvファイルに結果を保存します。DVD形式は対象外です。
・ファイル形式
・動画長さ(ビデオファイルに異常がある場合は「NA」)
・動画ピクセル数
・動画サンプルレート
・動画ビットレート
・動画ファイルサイズ
・動画ファイル最終更新日
・情報取得時間(秒)
・直前の非動画ファイルスキップ時間(秒)
・動画パス名
なおコマンドラインから起動する場合は、第1引数の対象フォルダパス名指定に加えて、第2引数にカンマで区切った除外パターンを指定可能です。
--------
材料:
1. Windows版python2
ライブラリが充実したスクリプト言語です。インストールし、パスを通しておきます。
2. ffmpegを利用可能にし、パスを通しておきます。新しめのバージョンであれば使えます。
3. 再生不可動画判別スクリプト起動コマンド「GetVideoInfoFromFolderV1a.cmd」
--この下から--
cd /d "%~dp0"
python GetVideoInfoFromFolderV1b.py %*
@if "%ERRORLEVEL%"=="0" timeout /t 4
@if not "%ERRORLEVEL%"=="0" pause ERROR!! see abobe.
--この上まで--4. 再生不可動画判別スクリプト本体「GetVideoInfoFromFolderV1b.py」
--この下から--
#!/usr/bin/env python
# -*- coding: cp932 -*-
""" ##
第1引数で指定された動画ファイル(拡張子が ts,mts,mov,mpg,mp4,mpeg,m2v,flv,avi,wmv,f1v,dpg,mkv)につき
動画タイプ(拡張子),長さ(hh:mm:ss)、ビットレート、サイズ、パス名を表示する。
第1引数がフォルダの場合はサブフォルダも含めたすべての動画ファイルを処理する。
ログファイルに処理結果を追記する。
""" ##
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 subprocess
import time
import glob
##ffmpegのパス名出力部がsjisかutf-8かを設定する。(入出力文字列変換のため。今回はutf-8)
sFfmpegCharCode='utf-8'
##引数(sjis)を読み込む
aArg =sys.argv
## このスクリプトの絶対パスを得る
sNowFolder =os.path.dirname(os.path.abspath(__file__))
## 現在日時を得る
sNowDate = datetime.datetime.today().strftime('%Y%m%d%H%M%S')
## このスクリプトと同じフォルダ内に日付名でログファイルと作業用ファイルを初期化する
sLogFilePath=sNowFolder +('\\VideoInfoLog_%s.csv' % sNowDate)
## このスクリプトと同じフォルダ内に(日付+連番)名で作成する一時ファイル名を決める
sTempFilePathTop=sNowFolder +('\\_temp_%s' % (sNowDate))
## 検出対象の拡張子のパターンを定義する
re_moviename =re.compile('\.(ts|mts|mov|mpg|mp4|flv|avi|wmv|f1v|dpg|mkv)$', re.I)
## Ffmpegの出力から動画長さを抽出するパターン
# Duration: 00:59:57.36, start: 82134.638111, bitrate: 9045 kb/s
re_Duration1 =re.compile('Duration[: ]+([0-9:]+)', re.I)
## Ffmpegの出力から動画ビットレートを抽出するパターン
re_BitRate1 =re.compile('Stream.*Video.* ([0-9\.]+ kb/s)', re.I)
re_BitRate1Mkv =re.compile('^ *BPS *: *([0-9\.]+) *$')
##Ffmpegの出力から## 動画解像度を抽出するパターン
re_PixelSize =re.compile('Stream.*Video.* ([0-9]+x[0-9]+)[ \,]')
## Ffmpegの出力からフレームレートを抽出するパターン
re_FrameRate =re.compile('Stream.*Video.* ([0-9\.]+ fps)[ \,]')
## Ffmpegの出力からパス名をを抽出するパターン
re_Pathname =re.compile(r'Input.*from \'(.*)\'.?$')
## コンマ以下の秒数を検出し削除するためのパターン
re_AfterComma =re.compile(r'\.[0-9]+$')
## 数字のみで構成された文字列の判定
re_NumOnly =re.compile(r'^[0-9]+$')
## 指定ファイルに文字列を追記する関数の定義
def fAppendText(sPath, s):
f =open(sPath, "ab")
f.write(s)
f.close()
## 指定ファイルの文字列を全て読み込む関数の定義
def fReadTextAllB(sPath):
f =open(sPath, "rb")
sOut =f.read()
f.close()
return(sOut)
## 秒数表記を時分秒表記に変換する(flvのDuration用)
def fSec2hms(str):
if re_NumOnly.search(str):
iTemp =int(str)
str =datetime.time(iTemp /3600, (iTemp/60) %60, iTemp% 60).strftime('%H:%M:%S')
return str
## 指定文字列のコマンドを実行する。(エラー発生時も放置。delコマンド用)
def fSystemExec(sCmd):
proc_now = subprocess.Popen(sCmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
proc_now.wait()
## 1ファイルを処理する関数の定義
def fProcessOneFile(sPath):
global iAllFile
global iVideoCount
global iVideoNACount
global iVideoSizeSum
global iLastEnd
global aExcludePattern
iAllFile =iAllFile +1
aInfo =[""]
m =re_moviename.search(sPath)
if m:
for sPattern in aExcludePattern:
if sPath.find(sPattern)!=-1: #除外パターンがパスに含まれている場合は処理をスキップする
print(u"除外指定によりスキップ: \"%s\"" % sPath)
return("")
iStart = datetime.datetime.now() # 処理時間計測用
sType =m.group(1).lower()
sDuration, sBitRate, sPixelSize, sFrameRate, sPathOut ="NA", "NA", "NA", "NA", sPath
# UTF-8と相互変換できない機種依存文字のファイル名があれば「NG_NAME」を出力し情報取得しない。
# ('illegal multibyte sequence' への対応)
sPath2 =sPath.encode("cp932", "replace").decode("cp932", "replace")
if sPath !=sPath2:
iFileSize=0
sTimeStamp ='1980/01/01 00:00:00'
sDuration, sBitRate, sPixelSize, sFrameRate, sPathOut ="NA", "NG_NAME", "NG_NAME", "NG_NAME", sPath2
print(u'次のファイル名の文字コードが不適切なのでリネームして下さい。\"%s\"' % sPath2)
elif not os.path.isfile(sPath):
iFileSize=0
sTimeStamp ='1980/01/01 00:00:00'
sDuration, sBitRate, sPixelSize, sFrameRate, sPathOut ="NA", "NO_FILE", "NO_FILE", "NO_FILE", sPath
print(u"ファイルが存在しません。\"%s\"" % sPath)
else:
iFileSize=os.path.getsize(sPath)
sTimeStamp =datetime.datetime.fromtimestamp(os.stat(sPath).st_mtime).strftime('%Y-%m-%d %H:%M:%S')
if sType=="dpg":
sDuration, sBitRate, sPixelSize, sFrameRate, sPathOut ="NA", "DPG", "DPG", "DPG", sPath
aInfo =fGetVideoFileInfoWithFfmpegWithFile(sPath).splitlines()
for line in aInfo:
m=re_Pathname.search(line)
if m:
sPathOut =m.group(1)
if sDuration =="NA": #未検出の場合のみ判定する
m=re_Duration1.search(line)
if m:
sDuration =fSec2hms(m.group(1))
if sBitRate =="NA": #未検出の場合のみ判定する
m=re_BitRate1.search(line)
if m:
sBitRate =m.group(1)
m=re_BitRate1Mkv.search(line)
if m:
sBitRate ="%.1f kb/s" % (int(m.group(1))/1000)
if sPixelSize =="NA": #未検出の場合のみ判定する
m=re_PixelSize.search(line)
if m:
sPixelSize =m.group(1)
if sFrameRate =="NA": #未検出の場合のみ判定する
m=re_FrameRate.search(line)
if m:
sFrameRate =m.group(1)
iVideoCount =iVideoCount +1
iVideoSizeSum =iVideoSizeSum +iFileSize
if sDuration=="NA": #再生不可の動画をカウントする (
iVideoNACount =iVideoNACount +1
iEnd = datetime.datetime.now() # 処理時間計測用
sInfo ="%s,%s,%s,%s,%s,%d,%s,%s,%s,\"%s\"" % (sType,sDuration, sPixelSize, sFrameRate, sBitRate, iFileSize, sTimeStamp,(iEnd -iStart).seconds,(iStart -iLastEnd).seconds,sPathOut)
#情報を標準出力およびログファイルに出力する。
fAppendText(sLogFilePath, sInfo.encode("cp932", "replace") +'\r\n' )
print(sInfo)
iLastEnd =iEnd # 動画ファイルの処理終了時刻を覚えておく
return ("")
##動画ファイルの情報を得る関数の定義(ffmpegの標準エラー出力をファイル出力後に取得、タイムアイウト有、プロセス管理不十分)
def fGetVideoFileInfoWithFfmpegWithFile(sPath):
global iTimeOutSec
global iTimeOutOnFfmpeg
global iVideoCount
global sTempFilePathTop
#一時ファイルの先頭に対象動画パスを書き込む
sTempFilePath="%s%06d.txt" % (sTempFilePathTop, iVideoCount)
#不要となった一時ファイルを削除するコマンドを作成する
sCmd2 =u"del /q \"%s\"" % sTempFilePath
sStdError=""
# Ffmpeg -i の標準エラー出力を一時ファイルに追記するように実行コマンドを作成する。
fAppendText(sTempFilePath, sPath.encode('utf-8', 'replace') +'\r\n' )
sCmd =u"ffmpeg.exe -timelimit %d -i \"%s\" >>\"%s\" 2>&1" % (iTimeOutOnFfmpeg,sPath,sTempFilePath)
sCmdsjis =sCmd.encode("cp932", "replace")
iStart = datetime.datetime.now() # タイムアウト制御用
proc = subprocess.Popen(sCmdsjis, shell=True)
while proc.poll() is None:
time.sleep(0.2)
iNow = datetime.datetime.now() # タイムアウト制御用
if (iNow - iStart).seconds> iTimeOutSec:
proc.kill() #実際にはffmpegはすぐには停止しないため、未終了プロセスが増えてゆく場合がある。
#ファイルから結果を読む
sStdError =fReadTextAllB(sTempFilePath)
if sFfmpegCharCode=='utf-8':
sStdError =sStdError.decode("utf-8", "replace") #利用中のffmpegの出力がutf-8の場合に変換する
sStdError ="\r\n".join(sStdError.splitlines()) #利用中のffmpegのメッセージが\r\nになっていないため整形する
#os.remove(sTempFilePath) #利用中のffmpegが掴んで離さない場合がある。時間がたつと解放される。
fSystemExec(sCmd2) #不要となった一時ファイルを削除する
return(sStdError)
sStdOut, sStdDummy =proc.communicate()
#ファイルから結果を読む
sStdError =fReadTextAllB(sTempFilePath)
if sFfmpegCharCode=='utf-8':
sStdError =sStdError.decode("utf-8", "replace") #利用中のffmpegの出力がutf-8の場合に変換する
sStdError ="\r\n".join(sStdError.splitlines()) #利用中のffmpegのメッセージが\r\nになっていないため整形する
#print("sStdError=\r\n%s" % sStdError)
fSystemExec(sCmd2) #不要となった一時ファイルを削除する
return(sStdError)
##諸変数を初期化し処理を開始する
iAllFile, iVideoCount, iVideoNACount, iVideoSizeSum = 0,0,0,0
iTimeOutSec =12
iTimeOutOnFfmpeg=10
iStart = datetime.datetime.now() # 総処理時間計測用
iLastEnd =datetime.datetime.now()
aExcludePattern=[] # 除外パターン
try:
# 引数の数が不適切な場合は処理を中断する
if len(aArg)==1:
raise Exception("010",u"引数が必要です。対象パスを指定して下さい。\n%s/" % (len(aArg), "/\n".join(aArg)))
elif len(aArg)!=2 and len(aArg)!=3:
print(u" Option: \"検索元フォルダ\" [\"除外パターン[,除外パターン2 . .]\" ]")
raise Exception("020",u"Exit. (No Option)")
# 第1引数の前後のダブルクォート文字あるいは末尾の\文字があれば除去する
sArg1_org =re.sub(r'^"', '', aArg[1])
sArg1_org =re.sub(r'["\\]+$', '', sArg1_org)
sArg1_unicode =sArg1_org.decode('cp932', 'replace')
if sArg1_org !=(sArg1_unicode.encode("cp932", "replace")):
raise Exception("030", u"ファイル名の文字コードが不適切です。リネームして下さい。\"%s\"" % sArg1_unicode)
"""
# 第1引数の前後のダブルクォート文字あるいは末尾の\文字があれば除去する
sArg1_unicode =re.sub(r'^"', '', sArg1_unicode)
sArg1_unicode =re.sub(r'["\\]+$', '', sArg1_unicode)
"""
# 第1引数にパスが含まれていない場合は付加する。(コマンドラインで実行の場合)
if not re.search(r'[\\:]', sArg1_unicode):
sArg1_unicode =sNowFolder +'\\' +sArg1_unicode
# 第2引数が指定された場合は除外パターン配列を得る。
if len(aArg)==3:
sArg2_unicode =aArg[2].decode('cp932', 'replace')
sArg2_unicode =re.sub(r'^"', '', sArg2_unicode)
sArg2_unicode =re.sub(r'["\\]+$', '', sArg2_unicode)
aExcludePattern =re.compile("[,/]").split(sArg2_unicode)
print("aExcludePattern=[%s], num=%d" % ("/".join(aExcludePattern), len(aExcludePattern)))
#引数がファイルなのかフォルダなのかを識別する
bParam1isFile =True if os.path.isfile(sArg1_unicode) else False
bParam1isDir =True if os.path.isdir(sArg1_unicode) else False
# 引数指定のフォルダ又はファイルが存在しない場合は処理を中断する
if not (bParam1isFile or bParam1isDir):
raise Exception("040", u"引数で指定したファイルまたはフォルダが存在しません: %s" % sArg1_unicode)
#引数がファイル名の場合とフォルダの場合に分けて処理する
if bParam1isFile:
sRet =fProcessOneFile(sArg1_unicode, sArg1_org)
elif bParam1isDir:
for root, dirs, files in os.walk(sArg1_unicode +'\\'):
for file in files:
sPath =os.path.join(root, file)
sRet =fProcessOneFile(sPath)
iEnd = datetime.datetime.now() # 総処理時間計測用
sSummary =',,,,,%d,,,%s,\"iAllFile=%d iVideoCount=%d iVideoNACount=%d\"' % (iVideoSizeSum, (iEnd -iStart).seconds, iAllFile,iVideoCount,iVideoNACount)
if len(aExcludePattern)!=0:
sSummary =sSummary +u" 除外=" +"/".join(aExcludePattern)
fAppendText(sLogFilePath, sSummary.encode("cp932", "replace") +'\r\n')
print(sSummary)
sCmd3 =u"del /q \"%s_*.*\"" % sTempFilePathTop
fSystemExec(sCmd3)
print("sLogFilePath=%s" % sLogFilePath)
sys.exit(0)
except Exception, inst:
# エラー発生時に例外の内容を出力する
#print("len(inst.args)=%d" % len(inst.args))
#print ("%s" % (" ".join(inst.args)))
print (inst.args)
sys.exit(1)
--この上まで--上記 3.と4.のファイルは共にメモ帳で作成して任意のフォルダに置いてから、3.のショートカットを作ってこれを C:\Users\(ユーザ名)\AppData\Roaming\Microsoft\Windows\SendTo の中にコピーすれば準備完了です。
ffmpegの標準エラー出力を取り出すための一時ファイルが多数作成されます。実行中に削除しきれなかった一時ファイルは全処理終了直前に一括削除されます。
もし文字化けで止まる場合は、4.の10行目の "utf-8" を違う文字 (""でok)に書き換えて試してください。
以上。



