Last active
November 14, 2024 17:10
-
-
Save phelian/81bbb30cd78aceb05c8d467243edb217 to your computer and use it in GitHub Desktop.
Golang: Read creation time from mov/mp4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"encoding/binary" | |
"errors" | |
"fmt" | |
"io" | |
"os" | |
"time" | |
) | |
// mov spec: https://developer.apple.com/standards/qtff-2001.pdf | |
// Page 31-33 contain information used in this file | |
const appleEpochAdjustment = 2082844800 | |
const ( | |
movieResourceAtomType = "moov" | |
movieHeaderAtomType = "mvhd" | |
referenceMovieAtomType = "rmra" | |
compressedMovieAtomType = "cmov" | |
) | |
func getVideoCreationTimeMetadata(videoBuffer io.ReadSeeker) (time.Time, error) { | |
buf := make([]byte, 8) | |
// Traverse videoBuffer to find movieResourceAtom | |
for { | |
// bytes 1-4 is atom size, 5-8 is type | |
// Read atom | |
if _, err := videoBuffer.Read(buf); err != nil { | |
return time.Time{}, err | |
} | |
if bytes.Equal(buf[4:8], []byte(movieResourceAtomType)) { | |
break // found it! | |
} | |
atomSize := binary.BigEndian.Uint32(buf) // check size of atom | |
videoBuffer.Seek(int64(atomSize)-8, 1) // jump over data and set seeker at beginning of next atom | |
} | |
// read next atom | |
if _, err := videoBuffer.Read(buf); err != nil { | |
return time.Time{}, err | |
} | |
atomType := string(buf[4:8]) // skip size and read type | |
switch atomType { | |
case movieHeaderAtomType: | |
// read next atom | |
if _, err := videoBuffer.Read(buf); err != nil { | |
return time.Time{}, err | |
} | |
// byte 1 is version, byte 2-4 is flags, 5-8 Creation time | |
appleEpoch := int64(binary.BigEndian.Uint32(buf[4:])) // Read creation time | |
return time.Unix(appleEpoch-appleEpochAdjustment, 0).Local(), nil | |
case compressedMovieAtomType: | |
return time.Time{}, errors.New("Compressed video") | |
case referenceMovieAtomType: | |
return time.Time{}, errors.New("Reference video") | |
default: | |
return time.Time{}, errors.New("Did not find movie header atom (mvhd)") | |
} | |
} | |
func main() { | |
fd, err := os.Open(os.Args[1]) | |
defer fd.Close() | |
created, err := getVideoCreationTimeMetadata(fd) | |
if err != nil { | |
fmt.Println(err.Error()) | |
return | |
} | |
fmt.Printf("Movie created at (%s)\n", created) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@phelian nice research, kinda crazy how MP4 and MOV both share the same algorithm to extract date created.
I just uploaded a repo where I copied your code:
https://github.com/davidrenne/mediaRenamerToTimestamp
I am a huge dropbox fan of how they sync multiple phone files and digital camera cards and rename to this format. But I wanted my own way to rename files so I wrote this out of necessity to clean up some of my media collection of family photos.