Question

I have many MPEG-4 files transcoded from various digital camera formats for which the file system modified date is correct. I'd like to set the "Media Created" tag to match. This can be done manually in Windows Explorer via the Details tab of the Properties window. Setting Media Created is useful because Windows Live Photo Gallery keys off this field for its Date Taken property. Unfortunately, the number of files makes setting all their dates by hand impractical.

A couple avenues for automation have potential. TagLib# seems to support all the MP4 tags, but the API to get at more than basic tags is unclear. Another angle is the Windows shell. Presumably, Windows Explorer is using it to write the tags. There is an example for reading via the shell, but there doesn't appear to be an API for writing.

Was it helpful?

Solution 2

Use the Windows Property System. See the Property Edit Sample to get started. Set these properties:

  • System.DateImported
  • System.Media.DateEncoded
  • System.ItemDate

OTHER TIPS

I have had success with exiftool. Here are the commands to list all tags in a media file and update selected tags (batch processing of files is also possible):

C:\>exiftool.exe -short -groupNames test.mp4

[ExifTool]      ExifToolVersion                 : 10.61
[File]          FileName                        : test.mp4
[File]          Directory                       : .
[File]          FileSize                        : 91 MB
[File]          FileModifyDate                  : 2018:06:30 19:25:34+05:00
[File]          FileAccessDate                  : 2018:07:15 14:12:50+05:00
[File]          FileCreateDate                  : 2018:07:15 14:12:50+05:00
[File]          FilePermissions                 : rw-rw-rw-
[File]          FileType                        : MP4
[File]          FileTypeExtension               : mp4
[File]          MIMEType                        : video/mp4
[QuickTime]     MajorBrand                      : MP4 v2 [ISO 14496-14]
[QuickTime]     MinorVersion                    : 0.0.0
[QuickTime]     CompatibleBrands                : isom, mp42
[QuickTime]     MovieDataSize                   : 95484206
[QuickTime]     MovieDataOffset                 : 32
[QuickTime]     MovieHeaderVersion              : 0
[QuickTime]     CreateDate                      : 2018:06:30 14:25:34
[QuickTime]     ModifyDate                      : 2018:06:30 14:25:34
[QuickTime]     TimeScale                       : 1000
[QuickTime]     Duration                        : 0:01:02
-- snip --
[QuickTime]     TrackCreateDate                 : 2018:06:30 14:25:34
[QuickTime]     TrackModifyDate                 : 2018:06:30 14:25:34
-- snip --
[QuickTime]     MediaCreateDate                 : 2018:06:30 14:25:34
[QuickTime]     MediaModifyDate                 : 2018:06:30 14:25:34
-- snip --

C:\>exiftool.exe ^
-QuickTime:CreateDate="2018:07:15 13:15:00" ^
-QuickTime:ModifyDate="2018:07:15 13:15:00" ^
-QuickTime:TrackCreateDate="2018:07:15 13:15:00" ^
-QuickTime:TrackModifyDate="2018:07:15 13:15:00" ^
-QuickTime:MediaCreateDate="2018:07:15 13:15:00" ^
-QuickTime:MediaModifyDate="2018:07:15 13:15:00" ^
test.mp4


C:\>exiftool.exe -short -groupNames test.mp4

-- snip --
[File]          FileModifyDate                  : 2018:07:15 14:19:52+05:00
[File]          FileAccessDate                  : 2018:07:15 14:19:51+05:00
[File]          FileCreateDate                  : 2018:07:15 14:19:39+05:00
-- snip --
[QuickTime]     CreateDate                      : 2018:07:15 13:15:00
[QuickTime]     ModifyDate                      : 2018:07:15 13:15:00
-- snip --
[QuickTime]     TrackCreateDate                 : 2018:07:15 13:15:00
[QuickTime]     TrackModifyDate                 : 2018:07:15 13:15:00
-- snip --
[QuickTime]     MediaCreateDate                 : 2018:07:15 13:15:00
[QuickTime]     MediaModifyDate                 : 2018:07:15 13:15:00
-- snip --

I solved this by writing reading/writing the MP4 file format directly. Here's the code in VB:

Sub Main()
    ' Retrieve creation-time and modification-time, embedded inside the metadata of MP4 files
    Dim ft = Mp4Times("a.mp4")
    Console.WriteLine(ft.CreationTime)
    Console.WriteLine(ft.ModificationTime)

    ' Update those times
    Mp4Times("a.mp4", Date.Now, Date.Now)
End Sub

Class FileTimes
    Public CreationTime As Date
    Public ModificationTime As Date
End Class

Function Mp4Times(fn As String, Optional newCreationTime As Date? = Nothing, Optional newModificationTime As Date? = Nothing) As FileTimes
    Dim ft As FileTimes
    Using f = If(newCreationTime.HasValue OrElse newModificationTime.HasValue, IO.File.Open(fn, IO.FileMode.Open), IO.File.OpenRead(fn))
        f.Seek(0, IO.SeekOrigin.End) : Dim fend = f.Position

        ' The file is made up of a sequence of boxes, with a standard way to find size and FourCC "kind" of each.
        ' Some box kinds contain a kind-specific blob of binary data. Other box kinds contain a sequence
        ' of sub-boxes. You need to look up the specs for each kind to know whether it has a blob or sub-boxes.
        ' We look for a top-level box of kind "moov", which contains sub-boxes, and then we look for its sub-box
        ' of kind "mvhd", which contains a binary blob. This is where Creation/ModificationTime are stored.
        Dim pos = 0L, payloadStart = 0L, payloadEnd = 0L, boxKind = ""
        While ReadNextBoxInfo(f, pos, fend, boxKind, payloadStart, payloadEnd) AndAlso boxKind <> "moov"
            pos = payloadEnd
        End While
        If boxKind <> "moov" Then Return Nothing
        pos = payloadStart : fend = payloadEnd
        While ReadNextBoxInfo(f, pos, fend, boxKind, payloadStart, payloadEnd) AndAlso boxKind <> "mvhd"
            pos = payloadEnd
        End While
        If boxKind <> "mvhd" Then Return Nothing

        ' The "mvhd" binary blob consists of 1byte (version, either 0 or 1), 3bytes (flags),
        ' and then either 4bytes (creation), 4bytes (modification)
        ' or 8bytes (creation), 8bytes (modification)
        ' If version=0 then it's the former, otherwise it's the later.
        ' In both cases "creation" and "modification" are big-endian number of seconds since 1st Jan 1904 UTC
        f.Seek(pos + 8, IO.SeekOrigin.Begin) : Dim version = f.ReadByte()
        f.Seek(pos + 12, IO.SeekOrigin.Begin)
        Dim creationTime As Date, modificationTime As Date
        '
        If newCreationTime.HasValue Then
            creationTime = newCreationTime.Value
            If version = 0 Then Write4byteDate(f, creationTime) Else Write8byteDate(f, creationTime)
        Else
            creationTime = If(version = 0, ReadNext4byteDate(f), ReadNext8byteDate(f))
        End If
        '
        If newModificationTime.HasValue Then
            modificationTime = newModificationTime.Value
            If version = 0 Then Write4byteDate(f, modificationTime) Else Write8byteDate(f, modificationTime)
        Else
            modificationTime = If(version = 0, ReadNext4byteDate(f), ReadNext8byteDate(f))
        End If
        ft = New FileTimes With {.CreationTime = creationTime, .ModificationTime = modificationTime}
    End Using

    If newCreationTime.HasValue Then IO.File.SetCreationTime(fn, newCreationTime.Value)
    If newModificationTime.HasValue Then IO.File.SetLastWriteTime(fn, newModificationTime.Value)
    Return ft
End Function

Function ReadNextBoxInfo(f As IO.Stream, pos As Long, fend As Long, ByRef boxKind As String, ByRef payloadStart As Long, ByRef payloadEnd As Long) As Boolean
    boxKind = "" : payloadStart = 0 : payloadEnd = 0
    If pos + 8 > fend Then Return False
    Dim b(3) As Byte
    f.Seek(pos, IO.SeekOrigin.Begin)
    f.Read(b, 0, 4) : If BitConverter.IsLittleEndian Then Array.Reverse(b)
    Dim size = BitConverter.ToUInt32(b, 0)
    f.Read(b, 0, 4)
    Dim kind = ChrW(b(0)) & ChrW(b(1)) & ChrW(b(2)) & ChrW(b(3))
    If size <> 1 Then
        If pos + size > fend Then Return False
        boxKind = kind : payloadStart = pos + 8 : payloadEnd = payloadStart + size - 8 : Return True
    End If
    If size = 1 AndAlso pos + 16 <= fend Then
        ReDim b(7)
        f.Read(b, 0, 8) : If BitConverter.IsLittleEndian Then Array.Reverse(b)
        Dim size2 = CLng(BitConverter.ToUInt64(b, 0))
        If pos + size2 > fend Then Return False
        boxKind = kind : payloadStart = pos + 16 : payloadEnd = payloadStart + size2 - 16 : Return True
    End If
    Return False
End Function

ReadOnly TZERO As Date = New Date(1904, 1, 1, 0, 0, 0, DateTimeKind.Utc)

Function ReadNext4byteDate(f As IO.Stream) As Date
    Dim b(3) As Byte
    f.Read(b, 0, 4) : If BitConverter.IsLittleEndian Then Array.Reverse(b)
    Dim secs = BitConverter.ToUInt32(b, 0)
    Return TZERO.AddSeconds(secs)
End Function

Function ReadNext8byteDate(f As IO.Stream) As Date
    Dim b(7) As Byte
    f.Read(b, 0, 8) : If BitConverter.IsLittleEndian Then Array.Reverse(b)
    Dim secs = BitConverter.ToUInt64(b, 0)
    Return TZERO.AddSeconds(secs)
End Function

Sub Write4byteDate(f As IO.Stream, d As Date)
    Dim secs = CUInt((d - TZERO).TotalSeconds)
    Dim b = BitConverter.GetBytes(secs) : If BitConverter.IsLittleEndian Then Array.Reverse(b)
    f.Write(b, 0, 4)
End Sub

Sub Write8byteDate(f As IO.Stream, d As Date)
    Dim secs = CULng((d - TZERO).TotalSeconds)
    Dim b = BitConverter.GetBytes(secs) : If BitConverter.IsLittleEndian Then Array.Reverse(b)
    f.Write(b, 0, 8)
End Sub

Here is a PowerShell script I wrote to tackle this in batches with the use of the exiftool.

It will write out all the exiftool commands into a CMD file. The exiftool.exe needs to be in the same location as the files and the exiftool.exe

Be sure to have Powershell ISE / the PS1 file in the same directory you are working on the files. Once it's completed you can look at the CMD file in notepad. If all looks well, run it with CMD. :)

$allFiles = Get-ChildItem -Filter * -Exclude *.exe

$exifFile = @()
$exifFile += "@echo off"

foreach($file in $allFiles)
{
    $month = $file.CreationTime.Month
    $day = $file.CreationTime.Day
    $hour = $file.CreationTime.Hour
    $min = $file.CreationTime.Minute
    $sec = $file.CreationTime.Second

    if($month -lt 10)
    {
        $month = "0$($file.CreationTime.Month)"
    }

    if($day -lt 10)
    {
        $day = "0$($file.CreationTime.Day)"
    }

    if($hour -lt 10)
    {
        $hour = "0$($file.CreationTime.Hour)"
    }

    if($min -lt 10)
    {
        $min = "0$($file.CreationTime.Minute)"
    }

    if($sec -lt 10)
    {
        $sec = "0$($file.CreationTime.Second)"
    }

    $exifFile += "exiftool.exe ^ -QuickTime:CreateDate=`"$($file.CreationTime.Year):$($month):$($day) $($hour):$($min):$($sec)`" ^ `"$($file.Name)`""
}

$exifFile | Out-File "MassCreateFix.cmd" -Encoding ascii -NoClobber
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top