Repository: Eyevinn/mp4ff
Pull request #206 (2023-01-23 22:23:48)

Add support for QuickTime meta atom (differs from MP4 meta box)
The QuickTime meta atom differs from the MP4 meta box in that it does not have any version and flags bytes.

The QuickTime atom is defined in 

https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html

Another PR, #204, made it possible to avoid decoding boxes like the QuickTime meta box which would cause an error. This PR makes that particular box variation decodable, by looking ahead in the data to determine if a `hdlr` box is appearing directly after the  meta header. 

A new method has been introduced, MetaBox.IsQuickTime(), to check if the box is of QuickTime variety.

Decoding and Encoding QuickTime meta atoms is henceforth possible. To create a QuickTime meta atom from scratch is, however, not supported.


Patch:
diff --git a/bits/fixedslicereader.go b/bits/fixedslicereader.go
index 736d35a..29c5546 100644
--- a/bits/fixedslicereader.go
+++ b/bits/fixedslicereader.go
@@ -179,7 +179,7 @@ func (s *FixedSliceReader) ReadZeroTerminatedString(maxLen int) string {
 	maxPos := startPos + maxLen
 	for {
 		if s.pos >= maxPos {
-			s.err = errors.New("Did not find terminating zero")
+			s.err = errors.New("did not find terminating zero")
 			return ""
 		}
 		c := s.slice[s.pos]
@@ -231,7 +231,7 @@ func (s *FixedSliceReader) SkipBytes(n int) {
 		return
 	}
 	if s.pos+n > s.Length() {
-		s.err = fmt.Errorf("Attempt to skip bytes to pos %d beyond slice len %d", s.pos+n, s.len)
+		s.err = fmt.Errorf("attempt to skip bytes to pos %d beyond slice len %d", s.pos+n, s.len)
 		return
 	}
 	s.pos += n
@@ -240,7 +240,7 @@ func (s *FixedSliceReader) SkipBytes(n int) {
 // SetPos - set read position is slice
 func (s *FixedSliceReader) SetPos(pos int) {
 	if pos > s.len {
-		s.err = fmt.Errorf("Attempt to set pos %d beyond slice len %d", pos, s.len)
+		s.err = fmt.Errorf("attempt to set pos %d beyond slice len %d", pos, s.len)
 		return
 	}
 	s.pos = pos
@@ -255,3 +255,12 @@ func (s *FixedSliceReader) GetPos() int {
 func (s *FixedSliceReader) Length() int {
 	return s.len
 }
+
+// LookAhead returns data ahead of current pos if within bounds.
+func (s *FixedSliceReader) LookAhead(offset int, data []byte) error {
+	if s.pos+offset+len(data) >= s.len {
+		return fmt.Errorf("out of bounds")
+	}
+	copy(data, s.slice[s.pos+offset:])
+	return nil
+}
diff --git a/bits/slicereader.go b/bits/slicereader.go
index d8d07f0..ca034c2 100644
--- a/bits/slicereader.go
+++ b/bits/slicereader.go
@@ -4,7 +4,7 @@ import "fmt"
 
 // SliceReader errors
 var (
-	ErrSliceRead = fmt.Errorf("Read too far in SliceReader")
+	ErrSliceRead = fmt.Errorf("read too far in SliceReader")
 )
 
 type SliceReader interface {
@@ -26,4 +26,5 @@ type SliceReader interface {
 	SetPos(pos int)
 	GetPos() int
 	Length() int
+	LookAhead(offset int, data []byte) error
 }
diff --git a/mp4/meta.go b/mp4/meta.go
index 4dbd664..be48a8c 100644
--- a/mp4/meta.go
+++ b/mp4/meta.go
@@ -1,21 +1,34 @@
 package mp4
 
 import (
+	"bytes"
 	"encoding/binary"
+	"fmt"
 	"io"
 
 	"github.com/Eyevinn/mp4ff/bits"
 )
 
-// MetaBox - MetaBox meta ISO/IEC 14496-12 Ed. 6 2020 Section 8.11
+// MetaBox is MPEG-4 Meta box or QuickTime meta Atom (without version and flags)
+
+// MPEG box defined in ISO/IEC 14496-12 Ed. 6 2020 Section 8.11
+//
+// Note. QuickTime meta atom has no version and flags field.
+// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW10
 type MetaBox struct {
-	Version  byte
-	Flags    uint32
-	Hdlr     *HdlrBox
-	Children []Box
+	Version     byte
+	Flags       uint32
+	Hdlr        *HdlrBox
+	Children    []Box
+	isQuickTime bool // Has no version and flags
+}
+
+// IsQuickTime returns true if box is QuickTime compatible (has no version and flags)
+func (m *MetaBox) IsQuickTime() bool {
+	return m.isQuickTime
 }
 
-// CreateMetaBox - Create a new MetaBox
+// CreateMetaBox creates a new MetaBox
 func CreateMetaBox(version byte, hdlr *HdlrBox) *MetaBox {
 	b := &MetaBox{
 		Version: version,
@@ -25,7 +38,7 @@ func CreateMetaBox(version byte, hdlr *HdlrBox) *MetaBox {
 	return b
 }
 
-// AddChild - Add a child box
+// AddChild adds a child box
 func (b *MetaBox) AddChild(child Box) {
 	switch box := child.(type) {
 	case *HdlrBox:
@@ -34,71 +47,77 @@ func (b *MetaBox) AddChild(child Box) {
 	b.Children = append(b.Children, child)
 }
 
-// DecodeMeta - box-specific decode
+// DecodeMeta decodes a MetaBox in either MPEG or QuickTime version
 func DecodeMeta(hdr BoxHeader, startPos uint64, r io.Reader) (Box, error) {
-	var versionAndFlags uint32
-	err := binary.Read(r, binary.BigEndian, &versionAndFlags)
+	data, err := readBoxBody(r, hdr)
 	if err != nil {
 		return nil, err
 	}
-	//Note higher startPos below since not simple container
-	children, err := DecodeContainerChildren(hdr, startPos+12, startPos+hdr.Size, r)
-	if err != nil {
-		return nil, err
-	}
-	b := &MetaBox{
-		Version: byte(versionAndFlags >> 24),
-		Flags:   versionAndFlags & flagsMask,
-	}
-	for _, child := range children {
-		b.AddChild(child)
-	}
-	return b, nil
+	sr := bits.NewFixedSliceReader(data)
+	return DecodeMetaSR(hdr, startPos, sr)
 }
 
-// DecodeMetaSR - box-specific decode
+// DecodeMetaSR decodes a MetaBox in either MPEG or QuickTime version
 func DecodeMetaSR(hdr BoxHeader, startPos uint64, sr bits.SliceReader) (Box, error) {
-	versionAndFlags := sr.ReadUint32()
-	//Note higher startPos below since not simple container
-	children, err := DecodeContainerChildrenSR(hdr, startPos+12, startPos+hdr.Size, sr)
+	b := MetaBox{}
+	lookAheadData := make([]byte, 4)
+	err := sr.LookAhead(4, lookAheadData)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("could not look ahead in Meta box")
+	}
+	var offset uint64 = 8
+	if bytes.Equal(lookAheadData, []byte("hdlr")) {
+		b.isQuickTime = true
+	} else {
+		//Note larger offset below since not simple container
+		offset += 4
+		versionAndFlags := sr.ReadUint32()
+		b.Version = byte(versionAndFlags >> 24)
+		b.Flags = versionAndFlags & flagsMask
 	}
-	b := &MetaBox{
-		Version: byte(versionAndFlags >> 24),
-		Flags:   versionAndFlags & flagsMask,
+
+	children, err := DecodeContainerChildrenSR(hdr, startPos+offset, startPos+hdr.Size, sr)
+	if err != nil {
+		return nil, err
 	}
+
 	for _, child := range children {
 		b.AddChild(child)
 	}
-	return b, nil
+	return &b, nil
 }
 
-// Type - box type
+// Type returns box type
 func (b *MetaBox) Type() string {
 	return "meta"
 }
 
-// Size - calculated size of box
+// Size calculates size of box
 func (b *MetaBox) Size() uint64 {
-	return 4 + containerSize(b.Children)
+	size := 4 + containerSize(b.Children)
+	if b.IsQuickTime() {
+		size -= 4
+	}
+	return size
 }
 
-// GetChildren - list of child boxes
+// GetChildren lists child boxes
 func (b *MetaBox) GetChildren() []Box {
 	return b.Children
 }
 
-// Encode - write minf container to w
+// Encode writes minf container to w
 func (b *MetaBox) Encode(w io.Writer) error {
 	err := EncodeHeader(b, w)
 	if err != nil {
 		return err
 	}
-	versionAndFlags := (uint32(b.Version) << 24) + b.Flags
-	err = binary.Write(w, binary.BigEndian, versionAndFlags)
-	if err != nil {
-		return err
+	if !b.isQuickTime {
+		versionAndFlags := (uint32(b.Version) << 24) + b.Flags
+		err = binary.Write(w, binary.BigEndian, versionAndFlags)
+		if err != nil {
+			return err
+		}
 	}
 	for _, b := range b.Children {
 		err = b.Encode(w)
@@ -109,7 +128,7 @@ func (b *MetaBox) Encode(w io.Writer) error {
 	return nil
 }
 
-// Encode - write minf container to sw
+// Encode writes minf container to sw
 func (b *MetaBox) EncodeSW(sw bits.SliceWriter) error {
 	err := EncodeHeaderSW(b, sw)
 	if err != nil {
@@ -126,12 +145,15 @@ func (b *MetaBox) EncodeSW(sw bits.SliceWriter) error {
 	return nil
 }
 
-// Info - box-specific info
+// Info writes box-specific info
 func (b *MetaBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string) error {
 	bd := newInfoDumper(w, indent, b, int(b.Version), b.Flags)
 	if bd.err != nil {
 		return bd.err
 	}
+	if b.isQuickTime {
+		bd.write(" - is QuickTime meta atom")
+	}
 	var err error
 	for _, c := range b.Children {
 		err = c.Info(w, specificBoxLevels, indent+indentStep, indentStep)


Test patch:
diff --git a/mp4/box_test.go b/mp4/box_test.go
index 9b64f9e..d41da37 100644
--- a/mp4/box_test.go
+++ b/mp4/box_test.go
@@ -12,7 +12,7 @@ import (
 //
 // The box is then interpreted as an UnknownBox and its data is not further processed with decoded.
 func TestBadBoxAndRemoveBoxDecoder(t *testing.T) {
-	badMetaBox := (`000000416d6574610000002168646c7200000000000000006d64746100000000` +
+	badMetaBox := (`000000416d6574610000002168646c7300000000000000006d64746100000000` +
 		`000000000000000000000000106b657973000000000000000000000008696c7374`)
 	data, err := hex.DecodeString(badMetaBox)
 	if err != nil {
diff --git a/mp4/meta_test.go b/mp4/meta_test.go
index 012f302..b5715fc 100644
--- a/mp4/meta_test.go
+++ b/mp4/meta_test.go
@@ -1,6 +1,13 @@
 package mp4
 
-import "testing"
+import (
+	"bytes"
+	"encoding/hex"
+	"strings"
+	"testing"
+
+	"github.com/Eyevinn/mp4ff/bits"
+)
 
 func TestMeta(t *testing.T) {
 	hdlr, err := CreateHdlr("zzzz")
@@ -9,5 +16,41 @@ func TestMeta(t *testing.T) {
 	}
 	meta := CreateMetaBox(0, hdlr)
 	boxDiffAfterEncodeAndDecode(t, meta)
+}
 
+func TestQuickTimeMeta(t *testing.T) {
+	quickTimeMetaAtom := (`000000416d6574610000002168646c7200000000000000006d64746100000000` +
+		`000000000000000000000000106b657973000000000000000000000008696c7374`)
+	data, err := hex.DecodeString(quickTimeMetaAtom)
+	if err != nil {
+		t.Error(err)
+	}
+	sr := bits.NewFixedSliceReader(data)
+	box, err := DecodeBoxSR(0, sr)
+	if err != nil {
+		t.Error(err)
+	}
+	meta, ok := box.(*MetaBox)
+	if !ok {
+		t.Error("box is not meta")
+	}
+	if !meta.IsQuickTime() {
+		t.Errorf("meta box not detected as QuickTime")
+	}
+	info := bytes.Buffer{}
+	err = meta.Info(&info, "", "", "")
+	if err != nil {
+		t.Error(err)
+	}
+	if !strings.Contains(info.String(), "is QuickTime meta atom") {
+		t.Error("lacks QuickTime in info string")
+	}
+	outBuf := bytes.Buffer{}
+	err = meta.Encode(&outBuf)
+	if err != nil {
+		t.Error(err)
+	}
+	if !bytes.Equal(data, outBuf.Bytes()) {
+		t.Errorf("output meta for QuickTime differs from input")
+	}
 }