再生時間のおかしいFLVファイルを直す

##やりたいこと
FLVファイルにおいて、2分程度の動画なのに再生時間が2時間以上となり、うまく映像が再生されないものがあった。これを直して綺麗に再生できるようにする。
[FLV MetaData Injector](https://www.gigafree.net/media/flashe/flvmetadatainjector.html)でどうにかなる問題ではなさそうなので、まずは原因を探して直せるものなら直したい。

##原因を探る
とりあえず[FLV Extract](https://www.gigafree.net/media/flashe/flvextract.html)を利用して映像ファイル(.264)、音声ファイル(.aac)、タイムコード(.txt)に分割したところ、映像と音声は問題なく、タイムコードの記述に問題があるようだった。
開いてみると下記のようになっている。

```timecode.txt
# timecode format v2
0
0
70
134
334
609
952
1358
1816
2352
2957
```

ミリ秒で増えていくタイムコードが、どんどん膨らんで大きくなっていく。
しかもたまに数値が小さくなったりしている（タイムコードは経過時間の数値が入っていくので、減る＝巻き戻ることは無いはず……）。

```
1305237
1329824
1354481
24718
49511
74372
99290
```
##解決方法を推測
いろいろ計算してみたところ、

+ **0は0のままが正しい値**
+ **ひとつ前の値との差分が正しい値**
+ **数値が小さくなった際、その一番最初は正しい値**
+ **数値が小さくなっていなくても、差分をとった場合にひとつ前の値より小さい場合、差分をとる前が正しい値**

ということのようだ（少なくともそう見えた）。
これらを踏まえて計算すると下記のようになった。

```
0       -> 0
0       -> 0
70      -> 70
134     -> 134
334     -> 200
609     -> 275
952     -> 343
1358    -> 406
...
1329824 -> 24587
1354481 -> 24657
24718   -> 24718
49511   -> 24793
74372   -> 24861
99290   -> 24918
```
約66(ミリ秒)ずつ増えていて（おおよそ15フレームのタイムコード）、正しいように見える。

##コードを書く
この数値の修正を下記のようにした（ファイル入出力と単純計算のみなので、言語はなんでもよい）

```timecode.py
import sys
import os
import datetime

filename =''

if(len(sys.argv) > 1):
	filename = os.path.abspath(sys.argv[1])
else:
    filename = 'timecode.txt'

f = open(filename)

lines = []
header = []

for i,line in enumerate(f.readlines()):
    # 行頭にあるヘッダとタイムコードを分ける
    if(line.startswith('#') == False):
        lines.append(int(line))
    else:
        header.append(line)

f.close()

timecodes = []
val = 0

for i,line in enumerate(lines):
 
    #0はそのまま
    if(line == 0):
         timecodes.append(line)
 
    #手前より小さい数値は正しいtimecodeになったと推定されるのでそのまま挿入
    elif(line < lines[i-1]):
        if(timecodes[i-1] < line):
            timecodes.append(line)

        #不定な場合（手前のtimecodeよりも小さい）、前のタイムコードをそのまま挿入
        else:
            timecodes.append(timecodes[i-1])

    else:
        #一行前との差分をとると正しいtimecodeになりそう
        val = line - lines[i-1]

        #同じ数値が続く場合は前の数値を設定
        if(val == 0):
            timecodes.append(timecodes[i-1])
            
        #差分をとると手前より小さくなる（元々の数値が正しいtimecodeと想定される）場合はそのまま挿入
        elif(timecodes[i-1] > val):
            timecodes.append(line)

        #そうではない場合、差分を挿入
        else:
            timecodes.append(val)


#末尾に改行を入れた文字列リストにする
output = [str(v)+'\n' for v in timecodes]


now = datetime.datetime.now()

#timecodeのヘッダを挿入する
header.extend(output)

#ファイルを保存して終了
if(len(sys.argv) > 1):
    o_file,o_ext = os.path.splitext(os.path.abspath(sys.argv[1]))
    filename = o_file + now.strftime('_%Y%m%d%H%M%S') + o_ext
else:
    filename = 'timecode_change' + now.strftime('_%Y%m%d%H%M%S') + '.txt'

with open(filename, mode='w') as f:
    f.writelines(header)
```

##修正したタイムコードを反映した映像ファイルを作る
修正したタイムコードと、最初に分けた映像・音声を結合するには[MKVToolNix](https://www.gigafree.net/media/me/mkvtoolnix.html)を使用した。
出来上がったmkvファイルを再生したところ、綺麗に再生できた。

##実行ファイル化しておく
pythonがインストールされていない人も使えるように、[pyinstaller](https://qiita.com/takanorimutoh/items/53bf44d6d5b37190e7d1)を使って実行ファイルにする。
出来上がったtimecode.exeにファイルをドラッグ＆ドロップすれば、修正後のタイムコードファイルが同一フォルダに生成される。


```
$ pyinstaller --onefile timecode.py
```

##おさらい
**1.[FLV Extract](https://www.videohelp.com/software/FLV-Extract)に対象のFLVファイルを入れて映像・音声・タイムコードファイルに分割する
2.修正ロジックを割り出す
3.上記の修正処理をタイムコードファイルにかける
4.修正したタイムコードと、分割した映像・音声ファイルを[MKVToolNix](https://www.gigafree.net/media/me/mkvtoolnix.html)に入れて結合し、mkvファイルを作る
5.再生する**

1~4についてもまとめて処理できた方が便利なので、上記のコードに追加して下記のようになった。

```timecode.py
###################################################################################################
#
# FLVの再生時間がおかしいので、
# FLV Extractorから抽出したタイムコードファイルを修正するスクリプト
# 修正したtimecodeを使って映像ファイル・音声ファイルと一緒にMKVtoolnixにて結合すると
# 今回の規則に従ったタイムコードであればきちんと再生できるmkvファイルになる
# FLVExtractとMKVToolNixをインストールして、実行ファイルにはPATHを通しておくこと
#
###################################################################################################

import sys
import os
import datetime
import subprocess
import tempfile
import shutil

#===================================================================================================
# flvextractでタイムコード抽出
#===================================================================================================
def flvExtract(flvFilename,outputDir):

    cmd = [
        'flvextractcl',
        '-t',
        '-v',
        '-a',
        '-d',
        outputDir,
        flvFilename,
   ]
    print(flvFilename)
    subprocess.call(cmd)

#===================================================================================================
# タイムコードファイルを読み込んで修正する 
#===================================================================================================
def repairTimecode(timecodeFilename):

    f = open(timecodeFilename)

    lines = []
    header = []

    for i,line in enumerate(f.readlines()):
        # 行頭にあるヘッダとタイムコードを分ける
        if(line.startswith('#') == False):
            lines.append(int(line))
        else:
            header.append(line)

    f.close()

    timecodes = []
    val = 0

    for i,line in enumerate(lines):
    
        #0はそのまま
        if(line == 0):
            timecodes.append(line)
    
        #手前より小さい数値は正しいtimecodeになったと推定されるのでそのまま挿入
        elif(line < lines[i-1]):
            if(timecodes[i-1] < line):
                timecodes.append(line)

            #不定な場合（手前のtimecodeよりも小さい）、前のタイムコードをそのまま挿入
            else:
                timecodes.append(timecodes[i-1])

        else:
            #一行前との差分をとると正しいtimecodeになりそう
            val = line - lines[i-1]

            #同じ数値が続く場合は前の数値を設定
            if(val == 0):
                timecodes.append(timecodes[i-1])
            
            #差分をとると手前より小さくなる（元々の数値が正しいtimecodeと想定される）場合はそのまま挿入
            elif(timecodes[i-1] > val):
                timecodes.append(line)

            #そうではない場合、差分を挿入
            else:
                timecodes.append(val)
        
        if(i > 0 and timecodes[i] < timecodes[i-1]):
            print(timecodes[i-1])
            print(timecodes[i])
            print('\n')

    #末尾に改行を入れた文字列リストにする
    output = [str(v)+'\n' for v in timecodes]

    #timecodeのヘッダを挿入する
    header.extend(output)

    #ファイルを上書き保存
    with open(timecodeFilename, mode='w') as f:
        f.writelines(header)

    return

#===================================================================================================
# MKVToolNIXで映像・音声・修正したタイムコードを結合
#===================================================================================================
def mkvMerge(video,audio,timecode,outputDir):

    now = datetime.datetime.now()
    basename = os.path.splitext(os.path.abspath(video))[0]
    mkv = basename + now.strftime('_%Y%m%d%H%M%S') + '.mkv'

    #mkvmergeを使う
    #各種ファイルをmuxしてmkvを作成
    cmd = [
        'mkvmerge',
        '-o',
        mkv,
        '--timecodes',
        '0:' + timecode,
        video,
        audio
    ]

    subprocess.call(cmd)

    #指定したディレクトリに出力
    shutil.move(mkv,outputDir)

    return

#===================================================================================================
# メイン処理
#===================================================================================================
def main():

    flvFilename = ''

    #ファイル名取得
    if(len(sys.argv) > 1):
        flvFilename = os.path.abspath(sys.argv[1])
    else:
        flvFilename = os.path.join(os.getcwd(),'test.flv')

    #映像、音声、タイムコードの3つのファイル名を準備
    o_file = os.path.splitext(os.path.basename(flvFilename))[0]
    outputDir = os.path.dirname(os.path.abspath(flvFilename))

    #作業用に一時ディレクトリ作成
    with tempfile.TemporaryDirectory() as tmpDir:

        print(tmpDir+'\n')

        #FLVExtractで抽出
        flvExtract(flvFilename,tmpDir)

        timecodeFilename = os.path.join(tmpDir,o_file + '.txt')
        videoFilename = os.path.join(tmpDir,o_file + '.264')
        audioFilename =os.path.join(tmpDir,o_file + '.aac')

        #映像・音声・タイムコードのファイルが存在していればタイムコード修復とMKV作成
        if(os.path.exists(timecodeFilename) and os.path.exists(videoFilename) and os.path.exists(audioFilename)):

            repairTimecode(timecodeFilename)
            mkvMerge(videoFilename,audioFilename,timecodeFilename,outputDir)
 

            #処理に使用したFLVファイルはタイムスタンプをファイル名につけて直下のFLVディレクトリに移動させる
            os.makedirs(os.path.join(outputDir,'flv'),exist_ok=True)

            flv = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + '_' + os.path.splitext(os.path.basename(flvFilename))[0] + '.flv'
            shutil.move(flvFilename,os.path.join(os.path.join(outputDir,'flv'),flv))
 

main()

```

##おわりに
これは上手くいった方で、そもそも解決できない、タイムコードが全て0でどうしようもないなどもあった。
（音声ファイルの長さから尺を割り出して映像コマ数で割り算して暫定のタイムコードを出力することはできるが、当然のことながら音ズレがひどくなってしまう）

とはいえもし同様な状況で困った人がいたら、とりあえずタイムコードを見てみるのがおすすめ。