├── .gitignore ├── README.md └── mp4util.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.exe 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mp4util 2 | Golang library for extracting metadata information from mp4 files. 3 | 4 | For now this library just has a method to get the duration of an mp4. 5 | -------------------------------------------------------------------------------- /mp4util.go: -------------------------------------------------------------------------------- 1 | package mp4util 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // Returns the duration, in seconds, of the mp4 file at the provided filepath. 8 | // If an error occurs, the error returned is non-nil 9 | func Duration(filepath string) (int, error) { 10 | file, _ := os.Open(filepath) 11 | defer file.Close() 12 | 13 | moovAtomPosition, _, err := findAtom(0, "moov", file) 14 | if err != nil { 15 | return 0, err 16 | } 17 | 18 | // start searching for the mvhd atom inside the moov atom. 19 | // The first child atom of the moov atom starts 8 bytes after the start of the moov atom. 20 | mvhdAtomPosition, mvhdAtomLength, err := findAtom(moovAtomPosition+8, "mvhd", file) 21 | if err != nil { 22 | return 0, err 23 | } 24 | 25 | duration, err := durationFromMvhdAtom(mvhdAtomPosition, mvhdAtomLength, file) 26 | if err != nil { 27 | return 0, err 28 | } 29 | 30 | return duration, nil 31 | } 32 | 33 | // Finds the starting position of the atom of the given name if it is a direct child of the atom 34 | // that is indicated by the given start position. 35 | // Returns: If found the starting byte position of atom is returned along with the atom's size. 36 | // If not found, -1 is returned as the starting byte position 37 | // If there was an error, the error is non-nil 38 | func findAtom(startPos int64, atomName string, file *os.File) (int64, int64, error) { 39 | buffer := make([]byte, 8) 40 | for true { 41 | _, err := file.ReadAt(buffer, startPos) 42 | if err != nil { 43 | return 0, 0, err 44 | } 45 | 46 | // The structure of an mp4 atom is: 47 | // 4 bytes - length of atom 48 | // 4 bytes - name of atom in ascii encoding 49 | // rest - atom data 50 | lengthOfAtom := int64(convertBytesToInt(buffer[0:4])) 51 | name := string(buffer[4:]) 52 | if name == atomName { 53 | return startPos, lengthOfAtom, nil 54 | } 55 | 56 | // move to next atom's starting position 57 | startPos += lengthOfAtom 58 | } 59 | return -1, 0, nil 60 | } 61 | 62 | // Returns the duration in seconds as given by the data in the mvhd atom starting at mvhdStart 63 | // Returns non-nill error is there is an error. 64 | func durationFromMvhdAtom(mvhdStart int64, mvhdLength int64, file *os.File) (int, error) { 65 | buffer := make([]byte, 8) 66 | _, err := file.ReadAt(buffer, mvhdStart+20) // The timescale field starts at the 21st byte of the mvhd atom 67 | if err != nil { 68 | return 0, err 69 | } 70 | 71 | // The timescale is bytes 21-24. 72 | // The duration is bytes 25-28 73 | timescale := convertBytesToInt(buffer[0:4]) // This is in number of units per second 74 | durationInTimeScale := convertBytesToInt(buffer[4:]) 75 | return int(durationInTimeScale) / int(timescale), nil 76 | } 77 | 78 | func convertBytesToInt(buf []byte) int { 79 | res := 0 80 | for i := len(buf) - 1; i >= 0; i-- { 81 | b := int(buf[i]) 82 | shift := uint((len(buf) - 1 - i) * 8) 83 | res += b << shift 84 | } 85 | return res 86 | } 87 | --------------------------------------------------------------------------------