Skip to content

Instantly share code, notes, and snippets.

@phelian
Last active December 16, 2023 06:57
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save phelian/81bbb30cd78aceb05c8d467243edb217 to your computer and use it in GitHub Desktop.
Save phelian/81bbb30cd78aceb05c8d467243edb217 to your computer and use it in GitHub Desktop.
Golang: Read creation time from mov/mp4
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)
}
@davidrenne
Copy link

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment