media.CaptureTime, media.Location and media.Device give the recording date, the GPS position and the camera or phone that made a video. Each is a list of candidates, best first, with where it was read from, because a file often holds several dates that disagree. The two inputs are small test files from the OneMedia repository (MIT OR Apache-2.0): one carries Apple-style QuickTime keys (com.apple.quicktime.*), the other Android-style keys (com.android.*). Their pictures are synthetic and their tags are invented: Contoso, Northwind and the San Francisco position are not real devices or recordings.
dotnet new console -o WhenWhere && cd WhenWhere dotnet add package RecursiveAI.OneMedia --version 0.1.0-preview.1 t=https://raw.githubusercontent.com/recursive-ai-oss/onemedia-csharp/v0.1.0-preview.1/testdata/metadata curl -L -o iphone-style.mov $t/tags_keys.mov curl -L -o android-style.mp4 $t/tags_keys_android.mp4
Put this in Program.cs and run dotnet run:
using RecursiveAI.OneMedia; using RecursiveAI.OneMedia.Metadata; foreach (string path in new[] { "iphone-style.mov", "android-style.mp4" }) { using MediaFile media = Media.Open(path); Console.WriteLine(path); // The value is the first candidate; every candidate says where it came from. foreach (FactCandidate<MediaDateTime> c in media.CaptureTime.Candidates) Console.WriteLine($" recorded {c.Value} ({c.Certainty}) from {c.Source}"); // Test Best, not Value: Value is a struct and reads 0, 0 when there is no location. if (media.Location.Best is { } loc) Console.WriteLine($" location {loc.Value.Latitude}, {loc.Value.Longitude}" + $" altitude {loc.Value.Altitude?.ToString() ?? "-"} m" + $" from {loc.Source}"); DeviceFacts device = media.Device; Console.WriteLine($" device {device.Make.Value} {device.Model.Value}," + $" software {device.Software.Value}"); // As a DateTimeOffset, when the file states the zone. MediaDateTime when = media.CaptureTime.Value; if (media.CaptureTime.HasValue && when.OffsetMinutes is int offset) Console.WriteLine(" as DateTimeOffset " + new DateTimeOffset(when.Value, TimeSpan.FromMinutes(offset)).ToString("o")); }
iphone-style.mov recorded 2023-04-05T10:11:12+02:00 (Offset) from mdta:com.apple.quicktime.creationdate location 37.7749, -122.4194 altitude 12.5 m from mdta:com.apple.quicktime.location.ISO6709 device Contoso Camera_X1, software 17.1 as DateTimeOffset 2023-04-05T10:11:12.0000000+02:00 android-style.mp4 recorded 2023-06-01T09:30:00Z (Utc) from mdta:creation_time recorded 2023-06-01T09:30:00Z (Utc) from mvhd device Northwind Droid_7, software 14
The iPhone-style file has com.apple.quicktime.creationdate, the local time with its UTC offset, so the zone is known (Offset). The Android-style file has a UTC creation_time key and the same instant in the movie header (mvhd), so both candidates appear, the key first. Location comes from the ISO 6709 text (+37.7749-122.4194+012.500/) as decimal degrees and meters.
Which candidate wins
The order is fixed and documented in Tags and capture facts. For capture time: the Apple creationdate key first, then XMP dates with a zone, the mdta creation_time, ©day, XMP dates without a zone, Matroska DATE_RECORDED, Matroska DateUTC, and the movie header last. For location: QuickTime keys, then ©xyz user data, then the 3GPP loci box, then XMP GPS.
The movie header is a weak source. Some cameras store local time in it as if it were UTC, and some muxers write a placeholder: the Sintel trailer’s says 1970-01-01T00:00:00Z, which OneMedia reports as it is, as the only candidate. c.Interpretation says what each source means, and c.RawValue is the text or number as stored.
Limits
- Dates as stored. OneMedia doesn’t guess a zone or correct a wrong clock.
Certaintysays what the file states:Utc,Offset,LocalorUnknown. - Sources read. QuickTime keys and user data, iTunes
©day, 3GPP boxes, XMP (uuidbox andXMP_atom), the movie header, and Matroska tags andDateUTC. A GPS log stored as its own timed metadata track, as some action cameras and dashcams write, is not among them. - Matroska dates.
DateUTCis when the file was muxed, which for a converted or remuxed file is not when it was recorded. - No value. Many converters and upload sites drop these tags. When a file has none,
HasValueis false,Bestis null andValueis the type’s default: forLocationthat is 0, 0, a real place, so testBestorHasValuefirst.
The same facts include rotation: Get video rotation.