Logo wizaman's blog (legacy)
Table of Contents

(2017/11/05追記)事情が古くなってきたので、「ゲーム動画のエンコード事情を整理」も参照してください。
(2015/12/21追記)VBScriptコードのAUCパス設定を修正、OneDriveでスクリプト公開。
(2014/08/17追記)VBScriptコードの比較ミスを修正。

AviUtlという有名なフリーソフトがあります。これは単体では動画のカットやフィルタなど簡単な編集ができますが、各種プラグインを導入することで対応フォーマットが増えたり、動画編集機能が大幅に強化されたり・・・とフリーソフトにしてはかなり優秀で、私もエクストルーパーズの動画をニコニコやYouTubeに投稿する際に利用させてもらいました。**「拡張編集」プラグインで編集し、「拡張 x264 出力(GUI) Ex」**プラグインで.mp4出力しています。編集と.mp4出力がこのソフト1本で完結するのですごくありがたい(もちろん必要な環境を準備すればの話)。

まあ、今回は動画編集じゃなくて、単にエンコードするだけの話なんですが。

HDキャプボ買ってから、エクストルーパーズの対戦動画をよく撮るようになったんですが、これがたまるたまる・・・。録画ファイルは.aviなのですが、そのままだと非常に重いので、.mp4にしないとすぐにHDDがパンパンになってしまいます。今まで、自分でファイル名を入力して、ひとつずつバッチ登録して、バッチ出力で寝てる間に一括してエンコードしてました。が、このバッチ登録は手で行う必要があったため、それがめんどくさくて後回しにしてると、動画がたまるたまる・・・。

で、自動化できないの!?というのが本題。

ここで紹介するスクリプトはOneDrive上に配置したので、下記からダウンロードして下さい。

自動でAviUtlの出力プラグインを利用する

肝心の方法ですが、調べたら、やっぱりありました。

大雑把にやってることを説明すると、AviUtlを外部から操作するための小規模なプログラムを集めた**「AviUtl Control」**をスクリプトから組み合わせて利用することで自動化を実現しています。「AviUtl Control」にサンプルとして用意されている sample1.vbs スクリプトでは、指定ディレクトリ直下のファイルすべてを出力プラグインによりエンコードしています。流れとしては、次のようになります。

  1. 起動しているAviUtlがあれば特定し、なければ起動する。
  2. 指定ディレクトリ直下のファイルのリストを得る。ファイルがなければ終了。
  3. 修正日時が最も古いファイルを指定の出力プラグインでエンコードし、指定ディレクトリに出力する。
  4. エンコード済みの入力ファイルを指定ディレクトリに移動。
  5. 2.に戻る。

このサンプルスクリプトは先頭にディレクトリの指定とかの記述があるので、ここを書き換えて実行すればいいわけです。スクリプトはVBScriptという言語で書かれていて、Windowsなら標準で使えるというものです。なので、今回必要なものをまとめると以下のようになります。

  • AviUtl本体
  • 利用したいAviUtl出力プラグイン(私は「拡張 x264 出力(GUI) Ex」を利用)
  • AviUtl Control(と付属するサンプルスクリプト)

サンプルスクリプトの修正

サンプルスクリプトでも最低限の要求は満たしていると思いますが、サブディレクトリが考慮されないとか、チェックが甘いので途中で実行をやめたいときに困るとかいった問題があるので、これに対応したいと思います。修正版は以下になります。

'''
''' AviUtlエンコード&ファイル移動
'''
' あるフォルダの中のサブディレクトリ下も含めたすべてのファイルについて、
' プロファイル&出力プラグイン指定でエンコードし、
' エンコードが終わったら指定のフォルダに移動する
 
' 元のサンプルスクリプトのtypo修正およびウインドウが消滅したとき
' スクリプトを停止するようにした
 
' VBScript 参考
' http://vbscript.g.hatena.ne.jp/cx20/20100131/1264906231
' http://www.vacant-eyes.jp/Tips/twsh/170.aspx
 
Option Explicit
'On Error Resume Next
 
' キャプチャしたファイルがあるフォルダ(最後の文字は"\")
Const SOURCE_FOLDER = "C:\Users\user\Videos\amarec\_auto_encode_targets\"
 
' エンコードが終わったファイルを移すフォルダ(最後の文字は"\") ※必ず SOURCE_FOLDER と違う場所にする
Const MOVE_FOLDER = "C:\Users\user\Videos\amarec\_auto_encode_complete\"
 
' エンコードしたデータを出力するフォルダ(最後の文字は"\")
Const OUTPUT_FOLDER = "C:\Users\user\Videos\aviutl\_auto_encoded\"
 
' プロファイル番号(メニューの一番上が0)
Const OUTPUT_PROFILE = 0
 
' 出力プラグイン番号(メニューの一番上が0)
Const OUTPUT_PLUGIN = 1
 
' 出力ファイルの拡張子
Const OUTPUT_EXT = ".mp4"
 
' AviUtlのフルパス
Const AVIUTL_PATH = "C:\Program File?(x86)\AviUtl\aviutl100\aviutl.exe"
 
' AviUtl Controlのフルパス(最後の文字は"\")
Const AUC_FOLDER = "C:\Program File?(x86)\AviUtl\auc_15\"
 
' !!! 以下、編集の必要なし !!!
 
Dim WSHShell, Fs
Set WSHShell = WScript.CreateObject("WScript.Shell")
Set Fs = CreateObject("Scripting.FileSystemObject")
 
Function Hash()
    Set Hash = CreateObject("Scripting.Dictionary")
End Function
 
Function Auc(command, arg)
    Auc = WSHShell.Run("""" & AUC_FOLDER & "auc_" & command & ".exe"" " & arg, 2, True)
End Function
 
Function WindowExists()
    WindowExists = (Auc("findwnd", "") <> 0) ' != 0
End function
 
' フォルダに含まれるファイルをすべて取得する(サブディレクトリ考慮)
Function GetFiles(objFolder)
    Dim files, objSubFolder, objSubFolders, tmpFiles, tmpFile
    Set files = Hash()
    For Each tmpFile In objFolder.Files
        If Fs.GetExtensionName(tmpFile.Name) <> "db" Then ' サムネイルファイルは除外
            files.Add files.Count, tmpFile
        End If
    Next
    Set objSubFolders = objFolder.SubFolders
    For Each objSubFolder In objSubFolders
        Set tmpFiles = GetFiles(objSubFolder)
        For Each tmpFile In tmpFiles.Items
            files.Add files.Count, tmpFile
        Next
    Next
    Set GetFiles = files
End Function
 
' 再帰的ディレクトリ作成
Sub CreateFolder(folder)
    Call WSHShell.Run("cmd /c mkdir " & """" & folder & """", 2, True)
End Sub
 
Dim hwnd, open
 
' AviUtlのウィンドウ番号を取得
hwnd = Auc("findwnd", "")
open = False
If hwnd = 0 Then
    hwnd = Auc("exec", """" & AVIUTL_PATH & """")
    If hwnd = 0 Then
        Call WScript.Quit(-1)
    End if
    open = True
End if
 
' キャプチャしたファイルがあるフォルダが空になるまで繰り返す
Dim srcFiles, srcFile, date, input, inputDir, subDir, output, outputDir, isFirst
Do While True
    Set srcFiles = GetFiles(Fs.GetFolder(SOURCE_FOLDER))
 
    If srcFiles.Count = 0 Then
        Exit Do
    End If
 
    ' SOURCE_FOLDER で一番古いファイルを探す
    isFirst = True
    For Each srcFile In srcFiles.Items
        If isFirst Then
            date = srcFile.DateLastModified
            input = srcFile.Path
            isFirst = False
        Elseif date > srcFile.DateLastModified then
            date = srcFile.DateLastModified
            input = srcFile.Path
        End if
    Next
 
    ' サブディレクトリを相対パスで得る
    inputDir = Fs.GetParentFolderName(input)
    If InStr(inputDir, SOURCE_FOLDER) = 1 Then ' SOURCE_FOLDER末尾の\まで含まれているならサブディレクトリ
        subDir = Mid(inputDir, Len(SOURCE_FOLDER) + 1, Len(inputDir) - Len(SOURCE_FOLDER)) & "\"
    Else
        subDir = ""
    End If
 
    ' 出力先ディレクトリがなければ作成
    outputDir = OUTPUT_FOLDER & subDir
    If Not Fs.FolderExists(outputDir) Then
        'Fs.CreateFolder(outputDir)
        CreateFolder outputDir
    End If
    output = outputDir & Fs.GetBaseName(input) & OUTPUT_EXT
    'MsgBox(input & vbCrLf & output)
 
    'Exit Do
 
    ' キャプチャしたファイルを開く
    If Not WindowExists() Then
        Exit Do
    End If
    Call Auc("open",    CStr(hwnd) & " """ & input & """")
    Call WScript.Sleep(3000)
 
    ' プロファイルを設定する
    If Not WindowExists() Then
        Exit Do
    End If
    Call Auc("setprof", CStr(hwnd) & " " & CStr(OUTPUT_PROFILE))
    Call WScript.Sleep(1000)
 
    ' 出力プラグインから出力する
    If Not WindowExists() Then
        Exit Do
    End If
    Call Auc("plugout", CStr(hwnd) & " " & CStr(OUTPUT_PLUGIN) & " """ & output & """")
 
    ' 出力が終わるまで待つ
    If Not WindowExists() Then
        Exit Do
    End If
    Call Auc("wait",    CStr(hwnd))
 
    ' 出力ファイルの存在確認
    If Not Not Fs.FileExists(outputDir) Then
        Exit Do
    End If
 
    ' ファイルを閉じる
    If Not WindowExists() Then
        Exit Do
    End If
    Call Auc("close",    CStr(hwnd))
    Call WScript.Sleep(3000)
 
    ' エンコードが終わったファイルを移動する(出力先ディレクトリがなければ作成)
    outputDir = MOVE_FOLDER & subDir
    If Not Fs.FolderExists(outputDir) Then
        'Fs.CreateFolder(outputDir)
        CreateFolder outputDir
    End If
    Call Fs.MoveFile(input, outputDir)
 
Loop
 
' スクリプトで起動したAviUtlを終了する
If open Then
    If WindowExists() Then
        Call Auc("exit", CStr(hwnd))
    End If
End if

元のスクリプトにtypoがあったのも修正しました(ver1.5)。VBScriptは初めて触ったんですが、言語仕様は難しくないのに、独特の癖があって意外と苦労しました・・・。主に参考にしたのは、以下のサイト。

あとは、FileSystemObjectの使い方を調べたとかですかね。

修正版では、サブディレクトリを考慮して動きます。入力ファイルが「{指定入力元ディレクトリ}/{サブディレクトリ}」にあるとすると、出力ファイルは「{指定出力先ディレクトリ}/{サブディレクトリ}」に吐き出され、エンコード済みの入力ファイルは「{指定移動先ディレクトリ}/{サブディレクトリ}」に移動されます。サブディレクトリの構造を維持するということです。対応するサブディレクトリが存在しなければ、作成します。

気をつけなければいけないのは、「指定した入力元ディレクトリ下にファイルが存在しなくなること」が終了条件なので、エンコード後のファイルの退避ができなければ終了しません(元のスクリプトも同じです)。つまり、入力元と移動先は別の場所にする必要があります。修正版は、起動中のAviUtlが存在するかどうか頻繁に確認し、なければ処理を中断する処理も追加しています。

他に気をつけたいのは、AviUtlを複数起動しているときの対応をしていないので、複数起動しないこと。それと、出力ファイルの存在確認を入れてみましたが、出力途中でも残る可能性があるので(実際、私の場合は残る)、**途中でやめたかったら、速やかにエンコードの中止とAviUtlの終了を行ってください。**AviUtlが終了すれば消滅を確認した後にスクリプトも終了します。

Pythonで書き直してみた

VBScriptクソクソ!とか叫びながら、ついでにPythonで書き直してみました。実行するにはPython 2.x系を導入している必要があります。

# -*- coding: utf-8 -*-
 
''' AviUtlエンコード&ファイル移動 '''
 
# あるディレクトリの中のサブディレクトリ下も含めたすべてのファイルについて、
# プロファイル&出力プラグイン指定でエンコードし、
# エンコードが終わったら指定のディレクトリに移動する
 
#################################################
# 設定
# パスの区切り文字は円マーク(バックスラッシュ)ではなく
# スラッシュを使う
# (raw文字列でも\uに反応しちゃうから)
#################################################
 
# キャプチャしたファイルがあるディレクトリ
INPUT_DIR = ur'C:/Users/user/Videos/amarec/_auto_encode_targets'
 
# エンコードが終わったファイルを移すディレクトリ
COMPLETED_DIR = ur'C:/Users/user/Videos/amarec/_auto_encode_complete'
 
# エンコードしたデータを出力するディレクトリ
OUTPUT_DIR = ur'C:/Users/user/Videos/aviutl/_auto_encoded'
 
# プロファイル番号(メニューの一番上が0)
OUTPUT_PROFILE = 0
 
# 出力プラグイン番号(メニューの一番上が0)
OUTPUT_PLUGIN = 1
 
# 入力ファイルの拡張子
INPUT_EXTENSIONS = [u'.avi', u'.mp4']
 
# 出力ファイルの拡張子
OUTPUT_EXTENSION = u'.mp4'
 
# AviUtlのフルパス
AVIUTL_PATH = ur'C:/Program File (x86)/AviUtl/aviutl100/aviutl.exe'
 
# AviUtl Controlのフルパス
AUC_DIR = ur'C:/Program File (x86)/AviUtl/aviutl100/auc_15'
 
#################################################
# 以下、処理開始
#################################################
 
import os, sys, codecs, locale, subprocess, time, shutil, traceback
 
# デリミタ(/)を末尾に追加
addDelimiter = lambda s: s if s.endswith('/') else s + '/'
 
INPUT_DIR = addDelimiter(INPUT_DIR)
COMPLETED_DIR = addDelimiter(COMPLETED_DIR)
OUTPUT_DIR = addDelimiter(OUTPUT_DIR)
 
print u'以下のパラメータが与えられました:'
print u'INPUT_DIR = %s' % INPUT_DIR
print u'COMPLETED_DIR = %s' % COMPLETED_DIR
print u'OUTPUT_DIR = %s' % OUTPUT_DIR
print u'OUTPUT_PROFILE = %s' % OUTPUT_PROFILE
print u'OUTPUT_PLUGIN = %s' % OUTPUT_PLUGIN
print u'INPUT_EXTENSIONS = %s' % INPUT_EXTENSIONS
print u'OUTPUT_EXTENSION = %s' % OUTPUT_EXTENSION
print u'AVIUTL_PATH = %s' % AVIUTL_PATH
print u'AUC_DIR = %s' % AUC_DIR
print
 
enc = lambda u: u.encode(locale.getpreferredencoding())
dec = lambda s: s.decode(locale.getpreferredencoding())
quote = lambda s: u'"%s"' % s
replace = lambda s: s.replace(u'/', u'\\')
 
def run(command):
    p = subprocess.Popen(command, stdout = subprocess.PIPE)
    output = p.stdout.read()
    p.wait()
    return output.strip()
#run = lambda command: subprocess.call(command, shell = True)
 
class AUC(object):
    def __init__(self):
        self._isAutoLaunch = False
        self._hwnd = self.findwnd()
        if self._hwnd == 0:
            self._hwnd = self.launch(AVIUTL_PATH)
            self._isAutoLaunch = (self._hwnd != 0)
    @staticmethod
    def _auc(command, *args):
        args = u' '.join(map(unicode, args))
        command = u'auc_%s.exe %s' % (command, args)
        print 'Run: ' + command
        return dec(run(enc(command)))
    def isAutoLaunch(self):
        return self._isAutoLaunch
    def exists(self):
        return self._hwnd != 0 and self._hwnd == self.findwnd()
    def launch(self, path):
        return int(self._auc(u'exec', quote(path)))
    def findwnd(self):
        return int(self._auc(u'findwnd'))
    def exit(self):
        self._auc(u'exit', self._hwnd)
        self._hwnd = 0
        self._isAutoLaunch = False
    def open(self, file):
        self._auc(u'open', self._hwnd, quote(replace(file)))
    def openadd(self, file):
        self._auc(u'openadd', self._hwnd, quote(replace(file)))
    def audioadd(self, file):
        self._auc(u'audioadd', self._hwnd, quote(replace(file)))
    def close(self):
        self._auc(u'close', self._hwnd)
    def setprof(self, profile):
        self._auc(u'setprof', self._hwnd, profile)
    def aviout(self, file):
        self._auc(u'aviout', self._hwnd, quote(replace(file)))
    def wavout(self, file):
        self._auc(u'wavout', self._hwnd, quote(replace(file)))
    def plugout(self, plugin, file):
        self._auc(u'plugout', self._hwnd, plugin, quote(replace(file)))
    def plugbatch(self, plugin, file):
        self._auc(u'plugbatch', self._hwnd, plugin, quote(replace(file)))
    def wait(self):
        self._auc(u'wait', self._hwnd)
 
def getFiles(dir):
    files = []
    for file in os.listdir(dir):
        path = dir + file
        if os.path.isfile(path):
            if os.path.splitext(path)[1] in INPUT_EXTENSIONS:
                files.append(path)
        else:
            files += getFiles(addDelimiter(path))
    return files
 
def getSubDir(path):
    dir = addDelimiter(os.path.split(path)[0])
    if dir.startswith(INPUT_DIR):
        return dir[len(INPUT_DIR):]
    else:
        return ''
 
try:
    os.chdir(AUC_DIR) # カレントディレクトリをAviUtl Controlのある場所へ変更
 
    files = getFiles(INPUT_DIR)
    files.sort()
    if len(files) == 0:
        print u'処理すべきファイルが見つかりませんでした'
        raw_input('Press Enter Key...')
        exit()
 
    auc = AUC()
    getmtime = os.path.getmtime
 
    print u'次のファイルを取得しました:'
    for file in files: print '-> ' + file
    print
 
    while auc.exists() and len(files) > 0:
        # 一番古いファイルから処理する
        inputFile = None
        mtime = 0
        for file in files:
            if inputFile == None or mtime > getmtime(file):
                inputFile = file
                mtime = getmtime(file)
        files.remove(inputFile)
 
        # サブディレクトリ取得とか出力先ディレクトリ設定とか
        basename = os.path.basename(inputFile)
        subDir = getSubDir(inputFile)
        completedDir = COMPLETED_DIR + subDir if COMPLETED_DIR != None else ''
        completedFile = completedDir + basename if COMPLETED_DIR != None else ''
        outputDir = OUTPUT_DIR + subDir
        outputFile = outputDir + os.path.splitext(basename)[0] + OUTPUT_EXTENSION
 
        print u'次の設定でエンコードを開始します:'
        print u'入力ファイル: %s' % inputFile
        print u'出力ファイル: %s' % outputFile
        print u'エンコード後ファイル: %s' % completedFile
        print
 
        # ファイルオープン、プロファイルの設定
        if not auc.exists(): break
        auc.open(inputFile)
        print u'ファイルをオープンしました'
        if not auc.exists(): break
        auc.setprof(OUTPUT_PROFILE)
        time.sleep(1.000)
        print u'プロファイルを設定しました'
 
        # 出力先ディレクトリがなければ作成
        if not os.path.exists(outputDir):
            os.makedirs(outputDir)
            print u'出力先ディレクトリを作成しました'
 
        # 出力プラグインによる出力
        if not auc.exists(): break
        auc.plugout(OUTPUT_PLUGIN, outputFile)
        print u'エンコード中です...'
        if not auc.exists(): break
        auc.wait()
 
        # ファイルクローズ
        if not os.path.exists(outputFile):
            break
        print u'エンコードが完了しました'
 
        # ファイルクローズ
        if not auc.exists(): break
        auc.close()
        print u'ファイルをクローズしました'
        time.sleep(3.000)
 
        # エンコードが終わったファイルを移動する
        # 移動先ディレクトリがなければ作成
        if COMPLETED_DIR != None:
            if not os.path.exists(completedDir):
                os.makedirs(completedDir)
                print u'移動先ディレクトリを作成しました'
            if not auc.exists(): break
            shutil.move(inputFile, completedFile)
            print u'エンコードが終了したファイルを移動しました'
        print
 
    if auc.isAutoLaunch() and auc.exists():
        auc.exit()
 
except:
    traceback.print_exc()
    raw_input('Press Enter Key...')

若干、機能を追加していますが、基本的にやってることは変わりません。エラーが起きたら、エラーを表示してエンターキーが押されるまで待機します。エラー内容を表示するだけでもないよりかなりマシだと思う。

ちゃんと動作確認してないけど、エンコード済みのファイルを移動させたくなかったら、COMPLETED_DIR を None にすれば多分対応してます。最初に作ったファイルリストが空になるまで処理を繰り返す形に変えたので、入力元ファイルを移動させなくてもいいってことです。

あと、これも試してませんが、plugout を plugbatch に置き換えれば、エンコードはせずにバッチ登録だけしてくれる筈です。そうすれば、一括でバッチ登録してくれるようになりますね。バッチ登録リストはAviUtlの方で管理していて、処理が完了していないものは自分でリストから消さない限りちゃんと保持してくれるメリットがあります。このように、あとでまとめてバッチ出力する場合では、入力ファイルを移動されると困るので、COMPLETED_DIR を None にしましょう。

「AviUtl Control」には、各実行ファイルのソースコードも付属しています。中身見ればわかりますが、やってることは単純です。PythonにはWin32APIのモジュールも用意されているので、Pythonだけですべて実装することもできそうですね。

ゴミの削除

エンコード済みのファイルや、エンコードの際に出力された必要のないファイル(「拡張 x264 出力(GUI) Ex」を使うと.statsファイルと.stats.mbtreeファイルができる)を消したいので、バッチファイル作りました。これを実行すると、バッチファイルの存在するディレクトリ以下に対して、削除処理を行います。

.aviファイルの削除

del /Q /S *.avi

.statsファイルと.stats.mbtreeファイルの削除

del /Q /S *.stats
del /Q /S *.stats.mbtree

おしまい。

これで今まで以上に動画撮りまくれるぞー!やったー!