Refactored all MP4 code to better handle custom atoms

This commit is contained in:
David Howden 2015-05-27 08:43:47 +10:00
parent e6dce4ae2a
commit 36d9163868

240
mp4.go
View File

@ -21,6 +21,7 @@ var atomTypes = map[int]string{
21: "uint8",
}
// NB: atoms does not include "----", this is handled separately
var atoms = atomNames(map[string]string{
"\xa9alb": "album",
"\xa9art": "artist",
@ -70,63 +71,9 @@ func ReadAtoms(r io.ReadSeeker) (Metadata, error) {
return m, err
}
func readCustomAtom(r io.ReadSeeker, size uint32) (string, uint32, error) {
var datasize uint32
var datapos int64
var name string
var mean string
for size > 8 {
var subsize uint32
err := binary.Read(r, binary.BigEndian, &subsize)
if err != nil {
return "----", size - 4, err
}
subname, err := readString(r, 4)
if err != nil {
return "----", size - 8, err
}
b, err := readBytes(r, int(subsize-8))
if err != nil {
return "----", size - subsize, err
}
// Remove the size of the atom from the size counter
size -= subsize
switch string(subname) {
case "mean":
mean = string(b[4:])
case "name":
name = string(b[4:])
case "data":
datapos, err = r.Seek(0, os.SEEK_CUR)
if err != nil {
return "----", size, err
}
datasize = subsize
datapos -= int64(subsize)
}
}
// there should remain only the header size
if size != 8 {
return "----", size, errors.New("---- atom out of bound")
}
if mean == "com.apple.iTunes" && datasize != 0 && name != "" {
// we jump just before the data subatom
_, err := r.Seek(datapos, os.SEEK_SET)
if err != nil {
return "----", size, err
}
return name, datasize + 8, nil
}
return "----", size, nil
}
func (m metadataMP4) readAtoms(r io.ReadSeeker) error {
for {
var size uint32
ok := false
err := binary.Read(r, binary.BigEndian, &size)
name, size, err := readAtomHeader(r)
if err != nil {
if err == io.EOF {
return nil
@ -134,11 +81,6 @@ func (m metadataMP4) readAtoms(r io.ReadSeeker) error {
return err
}
name, err := readString(r, 4)
if err != nil {
return err
}
switch name {
case "meta":
// next_item_id (int32)
@ -147,81 +89,141 @@ func (m metadataMP4) readAtoms(r io.ReadSeeker) error {
return err
}
fallthrough
case "moov", "udta", "ilst":
return m.readAtoms(r)
case "free":
_, err := r.Seek(int64(size-8), os.SEEK_CUR)
if err != nil {
return err
}
continue
case "mdat": // skip the data, the metadata can be at the end
_, err := r.Seek(int64(size-8), os.SEEK_CUR)
if err != nil {
return err
}
continue
case "----":
// Generic atom.
// Should have 3 sub atoms : mean, name and data.
// We check that mean=="com.apple.iTunes" and we use the subname as
// the name, and move to the data atom.
// If anything goes wrong, we jump at the end of the "----" atom.
}
_, ok := atoms[name]
if name == "----" {
name, size, err = readCustomAtom(r, size)
if err != nil {
return err
}
ok = (name != "----")
default:
_, ok = atoms[name]
if name != "----" {
ok = true
}
}
b, err := readBytes(r, int(size-8))
if err != nil {
return err
}
// At this point, we allow all known atoms and the valid "----" atoms
if !ok {
_, err := r.Seek(int64(size-8), os.SEEK_CUR)
if err != nil {
return err
}
continue
}
// 16: name + size + "data" + size (4 bytes each), have already read 8
b = b[8:]
class := getInt(b[1:4])
contentType, ok := atomTypes[class]
if !ok {
return fmt.Errorf("invalid content type: %v", class)
}
b = b[8:]
switch name {
case "trkn", "disk":
m[name] = int(b[3])
m[name+"_count"] = int(b[5])
default:
var data interface{}
// 4: atom version (1 byte) + atom flags (3 bytes)
// 4: NULL (usually locale indicator)
switch contentType {
case "text":
data = string(b)
case "uint8":
data = getInt(b[:1])
case "jpeg", "png":
data = &Picture{
Ext: contentType,
MIMEType: "image/" + contentType,
Data: b,
}
}
m[name] = data
err = m.readAtomData(r, name, size-8)
if err != nil {
return err
}
}
}
func (m metadataMP4) readAtomData(r io.ReadSeeker, name string, size uint32) error {
b, err := readBytes(r, int(size))
if err != nil {
return err
}
// "data" + size (4 bytes each)
b = b[8:]
class := getInt(b[1:4])
contentType, ok := atomTypes[class]
if !ok {
return fmt.Errorf("invalid content type: %v (%x) (%x)", class, b[1:4], b)
}
// 4: atom version (1 byte) + atom flags (3 bytes)
// 4: NULL (usually locale indicator)
b = b[8:]
if name == "trkn" || name == "disk" {
fmt.Printf("%#v: %x\n", name, b)
m[name] = int(b[3])
m[name+"_count"] = int(b[5])
return nil
}
var data interface{}
switch contentType {
case "text":
data = string(b)
case "uint8":
data = getInt(b[:1])
case "jpeg", "png":
data = &Picture{
Ext: contentType,
MIMEType: "image/" + contentType,
Data: b,
}
}
m[name] = data
return nil
}
func readAtomHeader(r io.ReadSeeker) (name string, size uint32, err error) {
err = binary.Read(r, binary.BigEndian, &size)
if err != nil {
return
}
name, err = readString(r, 4)
return
}
// Generic atom.
// Should have 3 sub atoms : mean, name and data.
// We check that mean is "com.apple.iTunes" and we use the subname as
// the name, and move to the data atom.
// If anything goes wrong, we jump at the end of the "----" atom.
func readCustomAtom(r io.ReadSeeker, size uint32) (string, uint32, error) {
subNames := make(map[string]string)
var dataSize uint32
for size > 8 {
subName, subSize, err := readAtomHeader(r)
if err != nil {
return "", 0, err
}
// Remove the size of the atom from the size counter
size -= subSize
switch subName {
case "mean", "name":
b, err := readBytes(r, int(subSize-8))
if err != nil {
return "", 0, err
}
subNames[subName] = string(b[4:])
case "data":
// Found the "data" atom, rewind
dataSize = subSize + 8 // will need to re-read "data" + size (4 + 4)
_, err := r.Seek(-8, os.SEEK_CUR)
if err != nil {
return "", 0, err
}
}
}
// there should remain only the header size
if size != 8 {
err := errors.New("---- atom out of bounds")
return "", 0, err
}
if subNames["mean"] != "com.apple.iTunes" || subNames["name"] == "" || dataSize == 0 {
return "----", 0, nil
}
return subNames["name"], dataSize, nil
}
func (metadataMP4) Format() Format { return MP4 }
func (metadataMP4) FileType() FileType { return AAC }