Recursive AI

Change a video’s recorded date in C#

All onemedia recipes

Media.SetCaptureTime(input, output, when) sets the time a recording was made in every date field the file carries, each in that field’s own notation and zone, and returns a report of what it wrote, what it left and which dates still disagree. The audio and video bytes are copied unchanged. A preview in 0.4.0-preview.1. The inputs are MDN’s CC0 flower clip and the iPhone-style test file from the OneMedia repository (MIT OR Apache-2.0; synthetic picture, invented tags).

Terminal
dotnet new console -o Redate && cd Redate
dotnet add package RecursiveAI.OneMedia --version 0.4.0-preview.1
curl -L -o flower.mp4 https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4
curl -L -o iphone-style.mov https://raw.githubusercontent.com/recursive-ai-oss/onemedia-csharp/v0.4.0-preview.1/testdata/metadata/tags_keys.mov

Put this in Program.cs and run dotnet run:

C#
using RecursiveAI.OneMedia;
using RecursiveAI.OneMedia.Tags;
 
// 00:30 on 1 May 2024 in a UTC+2 zone, which is 22:30 UTC on 30 April.
var when = new DateTimeOffset(2024, 5, 1, 0, 30, 0, TimeSpan.FromHours(2));
 
foreach ((string input, string output) in new[] { ("flower.mp4", "flower-redated.mp4"), ("iphone-style.mov", "iphone-redated.mov") })
{
    using (MediaFile before = Media.Open(input))
        Console.WriteLine($"{input}: recorded {before.CaptureTime}");
 
    // Writes a new file; the input is not touched.
    CaptureTimeReport report = Media.SetCaptureTime(input, output, when);
    Console.WriteLine($"  header times written as {(report.HeaderTimesLocal ? "local" : "UTC")} ({report.HeaderTimesBasis})");
    foreach (DateFieldWrite w in report.Written)
        Console.WriteLine($"  wrote {w.System}:{w.Field}: {w.OldValue ?? "(none)"} -> {w.NewValue}");
    foreach (DateFieldLeft left in report.Left)
        Console.WriteLine($"  left {left.System}:{left.Field}: {left.Reason}");
    foreach (DateDisagreement d in report.Disagreements)
        Console.WriteLine($"  still differs: {d.Source} = {d.RawValue} ({d.Note})");
 
    using (MediaFile after = Media.Open(output))
        Console.WriteLine($"{output}: recorded {after.CaptureTime}");
}
Text
flower.mp4: recorded 2018-03-07T15:21:21Z
  header times written as UTC (specification: header times are UTC)
  wrote mvhd:creation_time: 2018-03-07T15:21:21 -> 2024-04-30T22:30:00
  wrote mvhd:modification_time: 2018-03-07T15:21:21 -> 2024-04-30T22:30:00
  wrote tkhd:creation_time (track 1): 2018-03-07T15:21:21 -> 2024-04-30T22:30:00
  wrote tkhd:modification_time (track 1): 2018-03-07T15:21:21 -> 2024-04-30T22:30:00
  wrote mdhd:creation_time (track 1): 2018-03-07T15:21:21 -> 2024-04-30T22:30:00
  wrote mdhd:modification_time (track 1): 2018-03-07T15:21:21 -> 2024-04-30T22:30:00
  wrote tkhd:creation_time (track 2): 2018-03-07T15:21:21 -> 2024-04-30T22:30:00
  wrote tkhd:modification_time (track 2): 2018-03-07T15:21:21 -> 2024-04-30T22:30:00
  wrote mdhd:creation_time (track 2): 2018-03-07T15:21:21 -> 2024-04-30T22:30:00
  wrote mdhd:modification_time (track 2): 2018-03-07T15:21:21 -> 2024-04-30T22:30:00
flower-redated.mp4: recorded 2024-04-30T22:30:00Z
iphone-style.mov: recorded 2023-04-05T10:11:12+02:00
  header times written as UTC (specification: header times are UTC)
  wrote mvhd:creation_time: 1904-01-01T00:00:00 -> 2024-04-30T22:30:00
  wrote mvhd:modification_time: 1904-01-01T00:00:00 -> 2024-04-30T22:30:00
  wrote mdta:com.apple.quicktime.creationdate: 2023-04-05T10:11:12+0200 -> 2024-05-01T00:30:00+0200
  left tkhd:creation_time (track 1): unset (0); left unset
  left mdhd:creation_time (track 1): unset (0); left unset
  left tkhd:creation_time (track 2): unset (0); left unset
  left mdhd:creation_time (track 2): unset (0); left unset
iphone-redated.mov: recorded 2024-05-01T00:30:00+02:00
  • The movie and track headers (mvhd, tkhd, mdhd) hold UTC seconds, as the MP4 specification says, so 00:30 at UTC+2 is written as 22:30 on 30 April. A device that stores local time there can be matched with CaptureTimeOptions.HeaderTimesAreLocal or a device profile; the report says which rule it used.
  • Apple’s creationdate key keeps the offset, +0200, as Apple cameras write it. It ranks first when OneMedia reads the date back, so the iPhone-style file reads back with its zone, and the flower clip, which has only header times, reads back in UTC.
  • Header times that were never set. Every header time in the iPhone-style file was 0. Its track headers are left and listed with the reason; its movie header was written, as the report says. (The date-writes page says a header whose times are both 0 is left unset; we have asked the OneMedia team which is intended.) Fields that are absent are not added in MP4 and MOV: no ©day or XMP is created.

Read it back with ExifTool and ffprobe

Terminal
exiftool -a -G1 -time:all --File:all flower-redated.mp4 iphone-redated.mov
Text
======== flower-redated.mp4
[QuickTime]     Create Date                     : 2024:04:30 22:30:00
[QuickTime]     Modify Date                     : 2024:04:30 22:30:00
[Track1]        Track Create Date               : 2024:04:30 22:30:00
[Track1]        Track Modify Date               : 2024:04:30 22:30:00
[Track1]        Media Create Date               : 2024:04:30 22:30:00
[Track1]        Media Modify Date               : 2024:04:30 22:30:00
[Track2]        Track Create Date               : 2024:04:30 22:30:00
[Track2]        Track Modify Date               : 2024:04:30 22:30:00
[Track2]        Media Create Date               : 2024:04:30 22:30:00
[Track2]        Media Modify Date               : 2024:04:30 22:30:00
======== iphone-redated.mov
[QuickTime]     Create Date                     : 2024:04:30 22:30:00
[QuickTime]     Modify Date                     : 2024:04:30 22:30:00
[Track1]        Track Create Date               : 0000:00:00 00:00:00
[Track1]        Track Modify Date               : 0000:00:00 00:00:00
[Track1]        Media Create Date               : 0000:00:00 00:00:00
[Track1]        Media Modify Date               : 0000:00:00 00:00:00
[Track2]        Track Create Date               : 0000:00:00 00:00:00
[Track2]        Track Modify Date               : 0000:00:00 00:00:00
[Track2]        Media Create Date               : 0000:00:00 00:00:00
[Track2]        Media Modify Date               : 0000:00:00 00:00:00
[Keys]          Creation Date                   : 2024:05:01 00:30:00+02:00
    2 image files read
Terminal
for f in flower-redated.mp4 iphone-redated.mov; do
  echo "$f"
  ffprobe -v error -show_entries format_tags=creation_time,com.apple.quicktime.creationdate:stream_tags=creation_time -of default=nw=1 $f
done
Text
flower-redated.mp4
TAG:creation_time=2024-04-30T22:30:00.000000Z
TAG:creation_time=2024-04-30T22:30:00.000000Z
TAG:creation_time=2024-04-30T22:30:00.000000Z
iphone-redated.mov
TAG:creation_time=2024-04-30T22:30:00.000000Z
TAG:com.apple.quicktime.creationdate=2024-05-01T00:30:00+0200

ExifTool shows the header times in UTC and the Apple key with its offset; ffprobe reads the headers as creation_time in UTC and the key as written. It lists creation_time for the file and for each track whose header holds one, so the flower clip shows three and the iPhone-style file one.

What file managers and photo apps show

From the README: written dates were “Read back in ExifTool 13.60, MediaInfo 26.10 and ffprobe 7.0.2. Windows Explorer and Photos (Windows 11) and macOS AVFoundation were checked: they show the mvhd date, not ©day or Apple’s creationdate. Apple Photos and Google Photos display checks are pending (they need a GUI session and an account)”.

Those display checks were run by the OneMedia project, not by us for this page; the table is on its date-writes page. The practical upshot: Windows and macOS show the mvhd time, read as UTC and displayed in the viewer’s own zone.

From the command line

onemedia tags clip.mp4 --capture-time 2024-05-01T00:30:00+02:00 --out redated.mp4 does the same and prints the report; --replace or --in-place writes to the file itself.

Limits

  • A preview. The README: “Checked against ExifTool 13.60 and MediaInfo 26.10 on published samples and on files we generate, not on a corpus of device recordings.” Its device-profile caveat applies to the local-time rule: “Device-profile rows for phones and cameras rest on ExifTool’s tag documentation and the few vendor samples available; treat the zone rules for devices not listed there as unverified”.
  • Formats. MP4, MOV and 3GP; Matroska and WebM (DateUTC, DATE_RECORDED); WAV/BWF/RF64; MP3; FLAC, Ogg Vorbis and Opus; ASF. Not AVI, MPEG-TS, PS, FLV, DV, CAF or AIFF.
  • Not rewritten: GPS times in telemetry tracks (the report lists them as disagreements when they differ), tmcd timecode, sidecar files, and the file system’s own dates.
  • Range. 1904 to 2262; a version 0 header box holds dates to 2040 and is left, with a note, for a later date.
  • Saving. Media.SetCaptureTime writes a new file; CaptureTimeWriter.Save replaces the file through a temporary file and SaveInPlace writes within existing padding only. To change other tags, see Edit MP4, MKV and MP3 tags.