Search code examples
batch-filetimestampmetadatamp4exiftool

How can I batch-modify timestamp metadata on MP4 video files


I recently discovered that one of my cameras had its internal clock set almost two-years behind the actual date/time, which upset the ordering of photographs and videos in my library timeline. I quickly discovered the wonderful batch-update functionality of exiftool's date/time shift feature. Unfortunately however it does not yet support modification of MP4 video files:

$ exiftool -AllDates+="0:0:729 3:17:0" test.mp4
Error: Writing of MP4 files is not yet supported - test.mp4
    0 image files updated
    1 files weren't updated due to errors

How can I perform similar batch-date-shifting functionality for MP4 video files?


Solution

  • Based on the QuickTime File Format Specification I put together a proof-of-concept Python script for crude shifting of the timestamps to get me by for now:

    #!/usr/bin/env python
    import datetime
    import sys
    
    def int_bytes(raw):
        value = 0
        for byte in raw:
            value <<= 8
            value += ord(byte)
        return value
    
    def bytes_int(value, size=4):
        raw = []
        for byte in range(size):
            raw.append(chr((value >> 8*byte) % 256))
        return ''.join(reversed(raw))
    
    
    ATOM_FORMAT = (
        ('atom_size', 4, int_bytes),
        ('type', 4, str),
        ('version', 1, int_bytes),
        ('flags', 3, int_bytes),
        ('creation_time', 4, int_bytes),
        ('modification_time', 4, int_bytes),
        # that's all I need for now, and is common
        # between tkhd, mvhd and mdhd
        # ...
    )
    
    ATOM_TYPES = ('tkhd', 'mvhd', 'mdhd')
    
    TIMESTAMP_EPOCH = datetime.datetime(1904, 1, 1, 0, 0)
    
    def from_timestamp(timestamp):
        return TIMESTAMP_EPOCH + datetime.timedelta(0, timestamp)
    
    def to_timestamp(datetime_obj):
        return int((datetime_obj - TIMESTAMP_EPOCH).total_seconds())
    
    def shift_dates(mp4, atom_type, delta):
        # TODO: refactor
        mp4.seek(0)
        data = mp4.read() # TODO: don't load whole file
        type_index = -1
        while True:
            try:
                type_index = data.index(atom_type, type_index+1)
            except ValueError:
                if type_index < 0:
                    raise RuntimeError('Cannot find atom: {}'.format(atom_type))
                else:
                    break
            else:
                sys.stdout.write(
                    '  Found {} at {}\n'.format(atom_type, type_index))
            offset = type_index - ATOM_FORMAT[0][1]
    
            header_data = {}
            offsets = {}
            for field, size, convert in ATOM_FORMAT:
                offsets[field] = offset
                offset += size
                header_data[field] = convert(data[offsets[field]:][:size])
    
            for field in ('creation_time', 'modification_time'):
                original = from_timestamp(header_data[field])
                shifted = original + delta
                mp4.seek(offsets[field])
                mp4.write(bytes_int(to_timestamp(shifted)))
                sys.stdout.write(
                    '    {}: {} -> {}\n'.format(field, original, shifted))
    
    
    if __name__ == '__main__':
        try:
            filename = sys.argv[1]
            days, seconds = map(int, sys.argv[2:])
        except (IndexError, TypeError, ValueError):
            sys.stderr.write(
                "USAGE: {} mp4_file days seconds\n".format(
                    sys.argv[0]
                )
            )
            sys.exit(1)
    
        try:
            f = open(filename, 'rwb+')
        except IOError:
            sys.stderr.write("ERROR: cannot open {}\n".format(filename))
            sys.exit(1)
        else:
            delta = datetime.timedelta(days, seconds)
            sys.stdout.write(
                'Shifting timestamps of {} by {!r}:\n'.format(filename, delta))
            for atom_type in ATOM_TYPES:
                shift_dates(f, atom_type, delta)
            f.close()
            sys.stdout.write('Done.\n')