mp4のメタデータから作成日時を取り出す

目次

公開日 2024-03-15 更新日 -

概要

前回のUNIX時間から得た撮影日時をExifへ書き込むというお話は、スマートフォンから取り出した写真に、Exifの撮影日時データがなく、1608975591739.jpg などのファイル名から日時を取り出せるということから始まりました。同じく動画もあって、1654587847765.mp4 などという具合です。

さて、これもUNIX時間として...と思ったのですが、FFmpeg に付属した ffprobe というコマンドで Exif のようなメタデータを取得することが可能とわかりました。しかもExifと異なり、削除されていることは少ないようです。

ただし、ファイルマネージャや動画の再生ツールのプロパティなどで、再生時間やコーデックはわかるものの、作成日時はわかりません。それをpythonで調べて一覧にしようという話です。

動画のプロパティに作成日時は表示されない

そもそも動画はいろいろな圧縮形式があり、音声の形式との組み合わせがあり、コンテナの種類もあって複雑です。今回はmp4以外に出会わなかったので、mp4だけを取り扱っていますが、mp4以外でも同様にできると思われます。

ffprobe

ネット検索での情報はそれほどなくて、

$ ffprobe -v quiet -print_format json -show_format ファイル名

というものでした。これで再生時間を取得するというものでした。

これを元に、ffprobe --helpman ffplobe で調べていきます。

量が多くて全部は読めません

まず、-vオプションですが、

-loglevel loglevel  set logging level
-v loglevel         set logging level

多くのコマンドで歴史的に -v は verbose か version なのですが、覚えるのが困難になって長いオプション名も使えるようになってきました。長いものは -- で始まることが多いのですが、ここでは -loglevel です。man で詳しく見ると、

quiet, -8
    Show nothing at all; be silent.
panic, 0
    Only show fatal errors which could lead the process to crash, such as an assertion failure. This is not currently used
    for anything.
fatal, 8
    Only show fatal errors. These are errors after which the process absolutely cannot continue.
error, 16
    Show all errors, including ones which can be recovered from.
warning, 24
    Show all warnings and errors. Any message related to possibly incorrect or unexpected events will be shown.
....

となっていて、だんだんとログレベルらしい用語がでてきます。数値での指定もできるのでしょうか。とにかく quiet で十分なのでほかは放置します。

次は -print_format オプションですが、

-print_format format  set the output printing format (available formats are: default, compact, csv, flat, ini, json, xml)

いくつか試しましたが、単純な key:value の形式だと数が多くて見にくくなるので、json がいい選択肢のようです。xmlは人が見るものではないような気がします。json はpythonにツールが標準で使え、以前触った経験があるので json にします。

次の -show_format オプションですが、--helpで -show_something の形式のものがならんでいます。-show_format-show_streams が使えそうです。

-show_data          show packets data
-show_data_hash     show packets data hash
-show_error         show probing error
-show_format        show format/container info
-show_frames        show frames info
-show_format_entry entry  show a particular entry from the format/container info
-show_entries entry_list  show a set of specified entries
-show_log           show log
-show_packets       show packets info
-show_programs      show programs info
-show_streams       show streams info
-show_chapters      show chapters info

両方とも json で構造を持ったデータが得られます。keyの一覧もこれで得られますし、pythonではkeyでvalueを引く辞書のような使い方ができます。

生のffprobeの使用例

コマンドの生の出力を見てみます。

解説の中の要素数やキーの名前は、あくまで例示に使ったファイルのもので、どれも同じわけではありません

-show_format

まず、-show_format では次のように出てきます。formatとコンテナの情報です。

$ ffprobe -v quiet -print_format json -show_format 1654587847765.mp4
{
    "format": {
        "filename": "1654587847765.mp4",
        "nb_streams": 2,
        "nb_programs": 0,
        "format_name": "mov,mp4,m4a,3gp,3g2,mj2",
        "format_long_name": "QuickTime / MOV",
        "start_time": "0.000000",
        "duration": "34.030000",
        "size": "8145098",
        "bit_rate": "1914804",
        "probe_score": 100,
        "tags": {
            "major_brand": "mp42",
            "minor_version": "1",
            "compatible_brands": "isommp41mp42",
            "creation_time": "2022-06-06T04:00:04.000000Z"
        }
    }
}

これがpythonに取り込まれると、一番外側が要素が一つだけの辞書 "format"がキーで、値が11の要素を持つ辞書です。10個のキーの値は文字列だったり数値ですが、"tags"というキーの値だけさらに辞書です。そのなかに"creation_time"というキー名で、作成日時が入っています。"2022-06-06T04:00:04.000000Z" で最後の"Z"はUTCを表します。

durationは再生時間で単位は秒です。

-show_streams

-show_streams は次のようになります。ファイルに含まれる動画部分と音声部分の2つのストリームがあり、それぞれにメタデータがあることがわかります。

$ ffprobe -v quiet -print_format json -show_streams 1654587847765.mp4
{
    "streams": [
        {
            "index": 0,
            "codec_name": "aac",
            "codec_long_name": "AAC (Advanced Audio Coding)",
            "profile": "LC",
            "codec_type": "audio",
            "codec_time_base": "1/44100",
            "codec_tag_string": "mp4a",
            "codec_tag": "0x6134706d",
            "sample_fmt": "fltp",
            "sample_rate": "44100",
            "channels": 2,
            "channel_layout": "stereo",
            "bits_per_sample": 0,
            "r_frame_rate": "0/0",
            "avg_frame_rate": "0/0",
            "time_base": "1/44100",
            "start_pts": 0,
            "start_time": "0.000000",
            "duration_ts": 1500650,
            "duration": "34.028345",
            "bit_rate": "94164",
            "max_bit_rate": "96000",
            "nb_frames": "1468",
            "disposition": {
                "default": 1,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "creation_time": "2022-06-06T04:00:04.000000Z",
                "language": "und",
                "handler_name": "Core Media Audio"
            }
        },
        {
            "index": 1,
            "codec_name": "h264",
            "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
            "profile": "High",
            "codec_type": "video",
            "codec_time_base": "3403/204000",
            "codec_tag_string": "avc1",
            "codec_tag": "0x31637661",
            "width": 960,
            "height": 540,
            "coded_width": 960,
            "coded_height": 544,
            "closed_captions": 0,
            "has_b_frames": 1,
            "pix_fmt": "yuv420p",
            "level": 40,
            "color_range": "tv",
            "color_space": "bt709",
            "color_transfer": "bt709",
            "color_primaries": "bt709",
            "chroma_location": "left",
            "refs": 1,
            "is_avc": "true",
            "nal_length_size": "4",
            "r_frame_rate": "30000/1001",
            "avg_frame_rate": "102000/3403",
            "time_base": "1/600",
            "start_pts": 0,
            "start_time": "0.000000",
            "duration_ts": 20418,
            "duration": "34.030000",
            "bit_rate": "1815425",
            "bits_per_raw_sample": "8",
            "nb_frames": "1020",
            "disposition": {
                "default": 1,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "creation_time": "2022-06-06T04:00:04.000000Z",
                "language": "und",
                "handler_name": "Core Media Video"
            }
        }
    ]
}

これがpythonに取り込まれると、一番外側が要素が一つだけの辞書 "streams"がキーで、値は要素が2つのリストです。

そのリスト内には要素が2つあって、要素は両方とも辞書です。1つ目の要素は"index": 0とあって、音声です。2つ目は"index": 1で動画です。

それぞれの辞書のキーの値は文字列だったり数値ですが、"disposition"と"tags"というキーの値はさらに辞書です。"tags"の辞書のなかにそれぞれ"creation_time"というキー名で、作成日時が入っています。値は "2022-06-06T04:00:04.000000Z" と同じです。

今回の目的には -show_format で十分でしょう。

pythonからjson形式で読む

シェルで実行したいコマンド(+オプション)を文字列にして、subprocess.runを使って実行します。

subprocess の使用方法もさまざまあって古い情報も多いですが、なるべくわかりやすく将来性のあるものを使ったはずです。

使用したsubprocess.runの引数は、

 shell=True: コマンド文字列の解釈をシェルにさせることでコマンドを一連の文字列に書くことができます。さもなければリストにしなければなりません。

 capture_output=True: コマンドの実行結果を取り込む設定です。

 text=True: テキストとして取得。そうでなければbytes型です。いまどきのLinuxはUTF-8でしょう。

戻り値から.stdoutで標準出力を得ています。ちなみに.stderrで標準エラー出力を得ることができます。

これをjson.loadsに与えてjsonデータを得ます。

確認だけするつもりで pprint を使って構造ごと表示しています。

#!/usr/bin/python3
# 2024.3.14
import sys
import subprocess
import json
import pprint

for filename in sys.argv[1:]:
    cmdc = f"ffprobe -v quiet -print_format json -show_format \'{filename}\'"
    #cmdc = f"ffprobe -v quiet -print_format json -show_streams  \'{filename}\'"
    ret = subprocess.run(cmdc, shell=True, capture_output=True, text=True)
    jinfo = json.loads(ret.stdout)
    print(type(jinfo)) #<class 'dict'>
    pprint.pprint(jinfo)

実行結果です。コマンドの出力と同じ構造です。辞書への格納順は違っているようですが、keyで引く辞書なので問題ありません。

$ python3 ~/python3/try06.py 1654587847765.mp4
<class 'dict'>
{'format': {'bit_rate': '1914804',
            'duration': '34.030000',
            'filename': '1654587847765.mp4',
            'format_long_name': 'QuickTime / MOV',
            'format_name': 'mov,mp4,m4a,3gp,3g2,mj2',
            'nb_programs': 0,
            'nb_streams': 2,
            'probe_score': 100,
            'size': '8145098',
            'start_time': '0.000000',
            'tags': {'compatible_brands': 'isommp41mp42',
                     'creation_time': '2022-06-06T04:00:04.000000Z',
                     'major_brand': 'mp42',
                     'minor_version': '1'}}}

日時の取得

json.loads で 文字列が辞書としてアクセスできるようになります。keyがわかれば値を求められます。作成日時に到達するには3つのkeyが必要になります。

import sys
import subprocess
import json
import datetime

for filename in sys.argv[1:]:
    cmdc = f"ffprobe -v quiet -print_format json -show_format \'{filename}\'"
    ret = subprocess.run(cmdc, shell=True, capture_output=True, text=True)
    jinfo = json.loads(ret.stdout)
    crdate = jinfo['format']['tags']['creation_time']
    dtobj  = datetime.datetime.strptime(crdate,'%Y-%m-%dT%H:%M:%S.%f%z')
    #crobj  = datetime.datetime.fromisoformat(crdate[:-1])
    print (f"ffprobe で取り出した作成日時は 文字列で {crdate}")
    print (f"それから作成した datetimeオブジェクトは {dtobj}")

取り出した日時にはZがついていて、協定世界時(UTC)であることがわかります。Z以外はiso形式なので、fromisoformat()で変換できるのですが、Zがついていると受け付けてくれません。フォーマット文字列を用意して、strptime()を使ってdatetimeオブジェクトを作ります。

実行結果です。datetimeオブジェクトの+00:00の部分がUTCであることを表しています。

$ python3 ~/python3/try07.py 1654587847765.mp4
ffprobe で取り出した作成日時は 文字列で 2022-06-06T04:00:04.000000Z
それから作成した datetimeオブジェクトは 2022-06-06 04:00:04+00:00

ほとんどのmp4ファイルには'creation_time'があるのですが、受け取ったファイルに一つだけないものがあってエラーが出ます。

$ python3 ~/python3/try07.py LINE_MOVIE_1587637271968.mp4
Traceback (most recent call last):
  File "/home/adachi/python3/setDateTimeByUn/try07.py", line 21, in <module>
    crdate = jinfo['format']['tags']['creation_time']
KeyError: 'creation_time'

keyがない時を考慮した日時の取得

[key]でアクセスする代わりに、.get(key)メソッドを使えは、エラーにならずに None が返ってきますし、.get(key,'')とすれば、keyがないときは第2引数の値を返すようにすることが可能です。しかし、'format', 'tags' もない可能性がないとは言えないので、工夫します。

crdate = jinfo.get('format',{}).get('tags',{}).get('creation_time','')

これなら、format がないときには空の辞書が返ります。もちろん、空の辞書内には tags はないので、再び空の辞書が返って、その中に creation_time がないので ''(空文字列) が返ります。

import sys
import subprocess
import json
import datetime

for filename in sys.argv[1:]:
    cmdc = f"ffprobe -v quiet -print_format json -show_format \'{filename}\'"
    ret = subprocess.run(cmdc, shell=True, capture_output=True, text=True)
    jinfo = json.loads(ret.stdout)
    crdate = jinfo.get('format',{}).get('tags',{}).get('creation_time','')
    if crdate != '':
        dtobj  = datetime.datetime.strptime(crdate,'%Y-%m-%dT%H:%M:%S.%f%z')
    else:
        dtobj  = datetime.datetime.min
    print (f"ffprobe で取り出した作成日時は 文字列で {crdate}")
    print (f"それから作成した datetimeオブジェクトは {dtobj}")

実行結果です。1つ目のファイルは正常なもの。2つ目は creation_time がないものです。将来関数に仕立てた時に datetimeオブジェクトを返すことにしたいので、datetime.datetime.min を使っています。もっとも、pythonなら返り値のtypeが違っても問題ないのですが。

$ python3 ~/python3/try08.py  1654587847765.mp4 LINE_MOVIE_1587637271968.mp4
ffprobe で取り出した作成日時は 文字列で 2022-06-06T04:00:04.000000Z
それから作成した datetimeオブジェクトは 2022-06-06 04:00:04+00:00
ffprobe で取り出した作成日時は 文字列で 
それから作成した datetimeオブジェクトは 0001-01-01 00:00:00

JSTへの変換

取得した日時がUTCのままでは都合が悪いので、JSTに変換して使うことを考えます。

今回の目的には datetime.astimezone(tz=None) を引数なしで使うのが良さそうです。つまり、

dtobj2 = dtobj.astimezone()

これは、「引数無し (もしくは tz=None の形 ) で呼び出された場合、システムのローカルなタイムゾーンが変更先のタイムゾーンだと仮定されます」というマニュアルの記述によります。返り値は「tz を新たに tzinfo 属性 として持つ datetime オブジェクト」ということです。

この後「道草」と「寄り道」があります。お急ぎの方は2つ跳ばして 最適な撮影日時を選ぶ へどうぞ

道草:datetime.min にタイムゾーンをつける

データがなかったときのためのdatetime.minでしたが、これがエラーになります。

import sys
import subprocess
import json
import datetime

dtobj  = datetime.datetime.min
zfmt = '%Y-%m-%d_%H:%M:%S.%f%z'
xdate = dtobj.strftime(zfmt)
print('(1)そのまま文字列化')
print(f' {xdate} objのタイムゾーンは{dtobj.tzname()}') 
dtobj2 = dtobj.astimezone()
xdate = dtobj2.strftime(zfmt)
print('(2)astimezone()で変換したものを文字列化') 
print(f' {xdate} 変換後のタイムゾーンは{dtobj2.tzname()}') 

実行結果は

(1)そのまま文字列化
 1-01-01_00:00:00.000000 objのタイムゾーンはNone
Traceback (most recent call last):
  File "/home/adachi/python3/setDateTimeByUn/try08min.py", line 17, in <module>
    dtobj2 = dtobj.astimezone()
ValueError: year 0 is out of range

ドキュメントに「バージョン 3.6 で変更: datetime.datetime.astimezone() メソッドを naive なインスタンスに対して呼び出せるようになりました。これは、システムのローカルな時間を表現していると想定されます。」とあります。つまり、datetime.min の時間がJST(+09:00)であるとして9時間引いてからUTCの設定を追加しようとします。minから9時間引けば、0年12月31日の15時になりますが、0年というところで規定の範囲を逸脱します。正当なエラーです。

minのままUTCであるという tzinfo 属性を追加すれば、JSTに変換しても0年にはなりません。datetime.replace()を試します

import sys
import subprocess
import json
import datetime

dtobj  = datetime.datetime.min
zfmt = '%Y-%m-%d_%H:%M:%S.%f%z'
xdate = dtobj.strftime(zfmt)
print('(1)そのまま文字列化')
print(f' {xdate} objのタイムゾーンは{dtobj.tzname()}') 
dtobj2 = dtobj.replace(tzinfo=datetime.timezone.utc)
xdate = dtobj2.strftime(zfmt)
print('(2)replace()で変換したものを文字列化')  
print(f' {xdate} 変換後のタイムゾーンは{dtobj2.tzname()}') 
print('(3)タイムゾーン情報付きの datetime.min と元の datetime.min の比較')  
print(f" == で比較すると、{dtobj2 == datetime.datetime.min}")

これはうまく行きます。

(1)そのまま文字列化
 1-01-01_00:00:00.000000 objのタイムゾーンはNone
(2)replace()で変換したものを文字列化
 1-01-01_00:00:00.000000+0000 変換後のタイムゾーンはUTC
(3)タイムゾーン情報付きの datetime.min と元の datetime.min の比較
 == で比較すると、False

ハマりどころはdatetime.replace()が自らの datetime オブジェクトを変更するのではなく、 新たな datetime オブジェクトを返すところです。

タイムゾーンが追加された datetime オブジェクトは元の datetime.min とは一致しません。creation_time がないときに返す値としていたのですが、あまりスマートでないように思えてきました。

さらに寄り道:zoneinfo

datetime.astimezone(tz=None) を引数なしで使うのが良さそうなのですが、timezoneとzoneinfoの引数をつけることを調べたので書いておきます。

.timezone(timedelta(hours=9))

UTCからの時間差を指定する方法です。「一年のうち異なる日に異なるオフセットが使われていたり、常用時 (civil time) に歴史的な変化が起きた場所のタイムゾーン情報を表すのには使えない」と書いてありますが、わかりにくいですね。夏時間があったり、UTCとの時間差を明石でなくて東京で測ることにしたなどの変更がある場合は使えないということです。

.tzname()で情報を取得すると UTC+09:00 と返ってきます。

.timezone(timedelta(hours=9), 'JST')

.timezone()メソッドに2つ目の引数をつけることで UTC+09:00 を任意の文字列にできます。たいていは'JST'とする例がかいてありますが、任意文字列を試しています。

特殊な状況にも対処できるということなのでしょう。

ZoneInfo("Asia/Tokyo")

「IANAのタイムゾーンデータベースをサポートした具体的なタイムゾーン実装」とありますが、夏時間や歴史的な変化が起きた場所のタイムゾーン情報にも追随するという壮大な努力があるようです。複雑で理解が追いつきませんが、たとえば、tzinfo=ZoneInfo("America/Los_Angeles"))と指定しておけば、もともとの時差と夏時間の考慮もしてくれそうなので、必要になったら調べて見る価値はあります。

import sys
import subprocess
import json
import pprint
import datetime
import zoneinfo

for filename in sys.argv[1:]:
    cmdc = f"ffprobe -v quiet -print_format json -show_format \'{filename}\'"
    cmdc1 = f"ffprobe -loglevel quiet -show_streams -print_format json  \'{filename}\'"
    ret = subprocess.run(cmdc, shell=True, capture_output=True, text=True)
    jinfo = json.loads(ret.stdout)
    crdate = jinfo.get('format',{}).get('tags',{}).get('creation_time','')
    if crdate != '':
        dtobj  = datetime.datetime.strptime(crdate,'%Y-%m-%dT%H:%M:%S.%f%z')
    else:
        dtobj  = datetime.datetime.min
        dtobj  = dtobj.replace(tzinfo=datetime.timezone.utc)
    print (f"filename: {filename} --------------")
    print (f"ffprobe で取り出した作成日時は 文字列で {crdate}")
    print (f"それから作成した datetimeオブジェクトは {dtobj}")
    zfmt = '%Y-%m-%d_%H:%M:%S.%f%z'
    xdate = dtobj.strftime(zfmt)
    print('(1)そのまま文字列化')
    print(f' {xdate} objのタイムゾーンは{dtobj.tzname()}') 
    dtobj2 = dtobj.astimezone()
    xdate = dtobj2.strftime(zfmt)
    print('(2)astimezone()で変換したものを文字列化') 
    print(f' {xdate} 変換後のタイムゾーンは{dtobj2.tzname()}') 
    tz3 = datetime.timezone(datetime.timedelta(hours=9))
    dtobj3 = dtobj.astimezone(tz3)
    xdate = dtobj3.strftime(zfmt)
    print(f'(3)astimezone(tz)で変換したものを文字列化 type(tz3)={type(tz3)}') 
    print(f' {xdate} 変換後のタイムゾーンは{dtobj3.tzname()}') 
    tz4 = datetime.timezone(datetime.timedelta(hours=9), 'JSTabc')
    dtobj4 = dtobj.astimezone(tz4)
    xdate = dtobj4.strftime(zfmt)
    print('(4)astimezone(tz)で変換したものを文字列化 type(tz4)={type(tz4)}') 
    print(f' {xdate} 変換後のタイムゾーンは{dtobj4.tzname()}') 
    tz5 = zoneinfo.ZoneInfo("Asia/Tokyo")
    dtobj5 = dtobj.astimezone(tz5)
    xdate = dtobj5.strftime(zfmt)
    print('(5)astimezone(zoneinfo)で変換したものを文字列化 type(tz5)={type(tz5)}') 
    print(f' {xdate} 変換後のタイムゾーンは{dtobj5.tzname()}') 

正常なファイルの結果です。(1)UTC(+0000)で04時だったものが、(2)〜(5)まで、JST(+9000)で9時間進んだ13時になっています。

任意文字列として入れたJSTabcもそのままでています。

filename: 1654587847765.mp4 --------------
ffprobe で取り出した作成日時は 文字列で 2022-06-06T04:00:04.000000Z
それから作成した datetimeオブジェクトは 2022-06-06 04:00:04+00:00
(1)そのまま文字列化
 2022-06-06_04:00:04.000000+0000 objのタイムゾーンはUTC
(2)astimezone()で変換したものを文字列化
 2022-06-06_13:00:04.000000+0900 変換後のタイムゾーンはJST
(3)astimezone(tz)で変換したものを文字列化 type(tz3)=<class 'datetime.timezone'>
 2022-06-06_13:00:04.000000+0900 変換後のタイムゾーンはUTC+09:00
(4)astimezone(tz)で変換したものを文字列化 type(tz4)=<class 'datetime.timezone'>
 2022-06-06_13:00:04.000000+0900 変換後のタイムゾーンはJSTabc
(5)astimezone(zoneinfo)で変換したものを文字列化 type(tz5)=<class 'zoneinfo.ZoneInfo'>
 2022-06-06_13:00:04.000000+0900 変換後のタイムゾーンはJST

create_timeの情報のないファイルでは、datetime.min にUTCを設定して使っています。

+9を指定したtimezoneによる変更では9時間進んで+0900になっていますが、zoneinfoによる変更では9時間18分59秒進んで+091859になっています。

filename: LINE_MOVIE_1587637271968.mp4 --------------
ffprobe で取り出した作成日時は 文字列で 
それから作成した datetimeオブジェクトは 0001-01-01 00:00:00+00:00
(1)そのまま文字列化
 1-01-01_00:00:00.000000+0000 objのタイムゾーンはUTC
(2)astimezone()で変換したものを文字列化
 1-01-01_09:18:59.000000+091859 変換後のタイムゾーンはLMT
(3)astimezone(tz)で変換したものを文字列化 type(tz3)=<class 'datetime.timezone'>
 1-01-01_09:00:00.000000+0900 変換後のタイムゾーンはUTC+09:00
(4)astimezone(tz)で変換したものを文字列化 type(tz4)=<class 'datetime.timezone'>
 1-01-01_09:00:00.000000+0900 変換後のタイムゾーンはJSTabc
(5)astimezone(zoneinfo)で変換したものを文字列化 type(tz5)=<class 'zoneinfo.ZoneInfo'>
 1-01-01_09:18:59.000000+091859 変換後のタイムゾーンはLMT

"Safie Engineers' Blog!"の「タイムゾーンと、Pythonでのその扱い方の注意点」という記事の中に zoneinfoファイルの「1901年問題」(別ウィンドウで開きます) というのがありまして、詳しく説明してくれています。なんでも日本は1888年の正月より前には今より18分59秒早い標準時間を用いていました(東京の地方平均時)が、これが32ビットで表現された UNIX time との関係で1901年では+0900になっているという記述になっていないものがあることが原因ということです。UNIX time の適用範囲を広げてたために、過去の歴史も調べなければならなくなったという、苦しいお話です。

最適な撮影日時を選ぶ

写真のときと同様に、複数の候補から最適な撮影日時を選びます。ExifのDateTimeOriginalの代わりに、mp4ファイルのメタデータにcreate_timeを考えれば他は同じです。

写真のときとの違いはExifは削除されがちですが、create_timeは残っていることが多く、UNIX時間が必要になることが少ないことです。

create_time があるものについてはこれを採用するのがいいでしょう。次にファイル名のUnix時間または86(66)型の日時ですが、2000〜2024年の範囲なら信用することにします。最後にタイムスタンプですが、OSに問い合わせる時点でJSTになっているはずなのでこのまま使います。撮影日時ではなく保存や受信日時かもしれませんがしかたありません。この原則で"採用する日付候補"とそれぞれの日付の一覧表をつくり、妥当性を検討します。

このページで開発したffprobeを使う部分は def getFromffprobe(filename) の所で、他は写真のところで作成したものと同じです。ファイルの拡張子をjpgからmp4にする程度の変更です。

プログラムはdatetime.minを使うことの妥当性にちょっと迷いましたが、写真の場合と合わせるためにあえて書き換えませんでした。create_timeから日時が得られたものは、タイムゾーンの情報をJSTに変更して "aware" なオブジェクト、得られなかったものはタイムゾーンの情報のない "naive" なオブジェクトである datetime.min を返すことにしましたた。どちらも datetimeオブジェクトとされています。

import sys
import subprocess
import json
import pprint
import datetime
import zoneinfo
import re
import pathlib
import os

def getTimeStamp(filename):
    ostat = (os.stat(filename))
    fmtime = ostat.st_mtime #float値(Unix時刻)
    fmobj = datetime.datetime.fromtimestamp(fmtime) #datetimeオブジェクト
    return fmobj

def getUnixTime(filename):
    results = re.findall('([0-9]{13})\.+(mp4|MP4)',filename)
    if (len(results)>0):
        unixstr = float(results[0][0])/1000
        uobj = datetime.datetime.fromtimestamp(unixstr)
    else:
        uobj = datetime.datetime.min
    return uobj

def get86date(filename):
    results86 = re.findall('([0-9]{8}_[0-9]{6})\.+(mp4|MP4)',filename)
    if (len(results86)>0):
        D8T6str = results86[0][0]
        d8obj = datetime.datetime.strptime(D8T6str,'%Y%m%d_%H%M%S')
    else:
        results66 = re.findall('([0-9]{6}_[0-9]{6})\.+(mp4|MP4)',filename)
        if (len(results66)>0):
            D6T6str = results66[0][0]
            d8obj = datetime.datetime.strptime(D6T6str,'%y%m%d_%H%M%S')
        else:
            d8obj = datetime.datetime.min
    return d8obj

def getFromffprobe(filename):
    cmdc = f"ffprobe -v quiet -print_format json -show_format \'{filename}\'"
    ret = subprocess.run(cmdc, shell=True, capture_output=True, text=True)
    jinfo = json.loads(ret.stdout)
    crdate = jinfo.get('format',{}).get('tags',{}).get('creation_time','')
    if crdate != '':
        dtobj  = datetime.datetime.strptime(crdate,'%Y-%m-%dT%H:%M:%S.%f%z')
        dtobj = dtobj.astimezone()
    else:
        dtobj  = datetime.datetime.min
        #dtobj  = dtobj.replace(tzinfo=datetime.timezone.utc)
    return dtobj

def main():
    print("file name\t採用する日付\tfile-timestamp\tcreate_time\tunixtime-fname\t86(66)-filename\t採用元")
    for x in sys.argv[1:]:
        dsorce = "none"
        goal = "yet"
        fmobj = getTimeStamp(x)
        crobj = getFromffprobe(x)
        uobj = getUnixTime(x)
        d8obj = get86date(x)
        if (crobj != datetime.datetime.min):
            goal = crobj #まずはcreate_time
            dsorce = "create_time" #"file_metadata"
        if (goal == "yet"):
            uyear = int(uobj.strftime('%Y'))
            if (uyear>1999 and 2025>uyear):
                goal = uobj #unixが正しそうならunix
                dsorce = "unixtime-from-fname"
        if (goal == "yet"):
            d8year = int(d8obj.strftime('%Y'))
            if (d8year>1999 and 2025>d8year):
                goal = d8obj #20221020_112233 等が正しそうなら
                dsorce = "D8_T6-from-fname"
        if (goal == "yet"):
            goal = fmobj #最後はtimestamp
            dsorce = "timestamp"
        fname = pathlib.Path(x).name
        fmdate = fmobj.strftime('%Y%m%d_%H%M%S-%f')[:-3]
        gdate  = goal.strftime('%Y%m%d_%H%M%S-%f')[:-3]
        crdate  = crobj.strftime('%Y%m%d_%H%M%S-%f')[:-3]
        if crobj == datetime.datetime.min: crdate = '-'
        udate  = uobj.strftime('%Y%m%d_%H%M%S-%f')[:-3]
        if uobj == datetime.datetime.min: udate = '-'
        d8date = d8obj.strftime('%Y%m%d_%H%M%S-%f')[:-3]
        if d8obj == datetime.datetime.min: d8date = '-'
        print(f"{fname}\t{gdate}\t{fmdate}\t{crdate}\t{udate}\t{d8date}\t{dsorce}")

if __name__ == '__main__':
    main()

実行結果です。

$ python3 ~/python3/try10.py  ./tset/*.mp4
file name	採用する日付	file-timestamp	create_time	unixtime-fname	86(66)-filename	採用元
1609482959473.mp4	20210101_153559-000	20210101_153558-000	20210101_153559-000	20210101_153559-473	-	create_time
1647164845027.mp4	20220313_182422-000	20220313_184724-000	20220313_182422-000	20220313_184725-027	-	create_time
1654587847765.mp4	20220606_130004-000	20220607_164408-000	20220606_130004-000	20220607_164407-765	-	create_time
210304_121331.mp4	20210304_121335-000	20210304_121334-000	20210304_121335-000	-	20210304_121331-000	create_time
LINE_MOVIE_1587637271968.mp4	20200423_192111-968	20200825_022142-000	-	20200423_192111-968	-	unixtime-from-fname
LINE_MOVIE_1588073376688.mp4	20200428_175517-000	20200825_022144-000	20200428_175517-000	20200428_202936-688	-	create_time
....以降省略....

tableにして、検討します。赤色で示したファイルが上で検討したファイルです。妥当なところと感じられます。

JSTに変更したdatetimeオブジェクトから日時の文字列をタイムゾーンを指定せずに書き出すと自然にJSTですので、写真の時と同様に扱うことができました。

file name採用する日付file-timestampcreate_timeunixtime-fname86(66)-filename採用元
1609482959473.mp420210101_153559-00020210101_153558-00020210101_153559-00020210101_153559-473-create_time
1647164845027.mp420220313_182422-00020220313_184724-00020220313_182422-00020220313_184725-027-create_time
1654587847765.mp420220606_130004-00020220607_164408-00020220606_130004-00020220607_164407-765-create_time
LINE_MOVIE_1588073376688.mp420200428_175517-00020200825_022144-00020200428_175517-00020200428_202936-688-create_time
LINE_MOVIE_1689644932710.mp420230718_104842-00020230718_104852-00020230718_104842-00020230718_104852-710-create_time
LINE_MOVIE_1587637271968.mp420200423_192111-96820200825_022142-000-20200423_192111-968-unixtime-from-fname
LINE_MOVIE_1598705616810.mp420200829_215336-81020200825_022204-000-20200829_215336-810-unixtime-from-fname
210304_121331.mp420210304_121335-00020210304_121334-00020210304_121335-000-20210304_121331-000create_time
VID 20201211_132104.mp420201211_132104-00020201211_163814-00020201211_132104-000-20201211_132104-000create_time
VID_20201211_132104.mp420201211_132104-00020201211_163814-00020201211_132104-000-20201211_132104-000create_time

今回の目的は保存や送受信によりずれてしまいがちなファイルのタイムスタンプの代わりになる撮影日時を得る方法を見つけ、最適な撮影日時を選ぶことでした。写真の場合はExifデータを利用してhtml形式のファイルをつくるプログラムを自作していたので、Exifを書き込むことを考えましたが、今回はそれがないのでできるかどうかわからないmp4ファイルのメタデータの書き込みは考えす、最適な撮影日時を選ぶところで終わりにします。