Repository: Eyevinn/mp4ff
Pull request #409 (2025-01-31 14:03:07)

SmDm and CoLL box support
Added support for SmDm and CoLL boxes defined for Webm (VP9) video codec
in https://www.webmproject.org/vp9/mp4/


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e79f9d..47d431a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,8 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - mp4.SetUUID() can take base64 string as well as hex-encoded.
 - Support for weird dac3 box with initial 4 zero bytes (Issue #395)
 - Lots of fuzzying tests and changes to avoid panic on bad input data
-- Support for VP8 and VP9 video codecs (vp08 and vp09 boxes)
-- Support for VP Codec Configuration Box (vpcC)
+- Support for SMPTE-2086 Mastering Display Metadata Box (SmDm)
+- Support for Content Light Level Box (CoLL)
+- Test for CoLL and SmDM inside vp09 box
 
 ### Fixed
 
diff --git a/mp4/box.go b/mp4/box.go
index 182f4ee..7a90c70 100644
--- a/mp4/box.go
+++ b/mp4/box.go
@@ -35,6 +35,7 @@ func init() {
 		"cdsc":    DecodeTrefType,
 		"clap":    DecodeClap,
 		"co64":    DecodeCo64,
+		"CoLL":    DecodeCoLL,
 		"colr":    DecodeColr,
 		"cslg":    DecodeCslg,
 		"ctim":    DecodeCtim,
@@ -107,6 +108,7 @@ func init() {
 		"silb":    DecodeSilb,
 		"sinf":    DecodeSinf,
 		"skip":    DecodeFree,
+		"SmDm":    DecodeSmDm,
 		"smhd":    DecodeSmhd,
 		"ssix":    DecodeSsix,
 		"stbl":    DecodeStbl,
diff --git a/mp4/boxsr.go b/mp4/boxsr.go
index deb69bb..7bc31f1 100644
--- a/mp4/boxsr.go
+++ b/mp4/boxsr.go
@@ -26,6 +26,7 @@ func init() {
 		"cdsc":    DecodeTrefTypeSR,
 		"clap":    DecodeClapSR,
 		"co64":    DecodeCo64SR,
+		"CoLL":    DecodeCoLLSR,
 		"colr":    DecodeColrSR,
 		"cslg":    DecodeCslgSR,
 		"ctim":    DecodeCtimSR,
@@ -98,6 +99,7 @@ func init() {
 		"silb":    DecodeSilbSR,
 		"sinf":    DecodeSinfSR,
 		"skip":    DecodeFreeSR,
+		"SmDm":    DecodeSmDmSR,
 		"smhd":    DecodeSmhdSR,
 		"ssix":    DecodeSsixSR,
 		"stbl":    DecodeStblSR,
diff --git a/mp4/coll.go b/mp4/coll.go
new file mode 100644
index 0000000..9c2de7d
--- /dev/null
+++ b/mp4/coll.go
@@ -0,0 +1,102 @@
+package mp4
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/Eyevinn/mp4ff/bits"
+)
+
+// CoLLBox - Content Light Level Box (coll)
+// Can be used for VP9 codec in vp09 box (VisualSampleEntryBox).
+// Defined in [WebM Project].
+//
+// [WebM Project]: https://www.webmproject.org/vp9/mp4/
+type CoLLBox struct {
+	Version byte
+	Flags   uint32
+	MaxCLL  uint16 // Maximum Content Light Level
+	MaxFALL uint16 // Maximum Frame-Average Light Level
+}
+
+// CreateCoLLBox - Create a new CoLLBox with specified values
+func CreateCoLLBox(maxCLL, maxFALL uint16) *CoLLBox {
+	return &CoLLBox{
+		Version: 0,
+		Flags:   0,
+		MaxCLL:  maxCLL,
+		MaxFALL: maxFALL,
+	}
+}
+
+const coLLBoxSize = boxHeaderSize + 4 + 2*2 // Header + version/flags + 2 uint16s
+
+// DecodeCoLL - box-specific decode
+func DecodeCoLL(hdr BoxHeader, startPos uint64, r io.Reader) (Box, error) {
+	// Only allow header size of 8 and correct total box size
+	if hdr.Hdrlen != boxHeaderSize || hdr.Size != coLLBoxSize {
+		return nil, fmt.Errorf("invalid box size %d", hdr.Size)
+	}
+	data, err := readBoxBody(r, hdr)
+	if err != nil {
+		return nil, err
+	}
+	sr := bits.NewFixedSliceReader(data)
+	return DecodeCoLLSR(hdr, startPos, sr)
+}
+
+// DecodeCoLLSR - decode box from SliceReader
+func DecodeCoLLSR(hdr BoxHeader, startPos uint64, sr bits.SliceReader) (Box, error) {
+	// Only allow header size of 8 and correct total box size
+	if hdr.Hdrlen != boxHeaderSize || hdr.Size != coLLBoxSize {
+		return nil, fmt.Errorf("invalid box size %d", hdr.Size)
+	}
+	b := &CoLLBox{}
+	b.Version = sr.ReadUint8()
+	b.Flags = sr.ReadUint24()
+	b.MaxCLL = sr.ReadUint16()
+	b.MaxFALL = sr.ReadUint16()
+	return b, sr.AccError()
+}
+
+// Type - box type
+func (b *CoLLBox) Type() string {
+	return "CoLL"
+}
+
+// Size - calculated size of box
+func (b *CoLLBox) Size() uint64 {
+	return coLLBoxSize
+}
+
+// Encode - write box to w
+func (b *CoLLBox) Encode(w io.Writer) error {
+	sw := bits.NewFixedSliceWriter(int(b.Size()))
+	err := b.EncodeSW(sw)
+	if err != nil {
+		return err
+	}
+	_, err = w.Write(sw.Bytes())
+	return err
+}
+
+// EncodeSW - box-specific encode to slicewriter
+func (b *CoLLBox) EncodeSW(sw bits.SliceWriter) error {
+	err := EncodeHeaderSW(b, sw)
+	if err != nil {
+		return err
+	}
+	sw.WriteUint8(b.Version)
+	sw.WriteUint24(b.Flags)
+	sw.WriteUint16(b.MaxCLL)
+	sw.WriteUint16(b.MaxFALL)
+	return sw.AccError()
+}
+
+// Info - write box-specific information
+func (b *CoLLBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string) error {
+	bd := newInfoDumper(w, indent, b, -1, 0)
+	bd.write(" - maxCLL: %d", b.MaxCLL)
+	bd.write(" - maxFALL: %d", b.MaxFALL)
+	return bd.err
+}
diff --git a/mp4/smdm.go b/mp4/smdm.go
new file mode 100644
index 0000000..4e9bed7
--- /dev/null
+++ b/mp4/smdm.go
@@ -0,0 +1,139 @@
+package mp4
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/Eyevinn/mp4ff/bits"
+)
+
+// SmDmBox - Sample Mastering Display Metadata Box (smdm)
+// Can be used for VP9 codec in vp09 box (VisualSampleEntryBox).
+// Defined in [WebM Project].
+//
+// [WebM Project]: https://www.webmproject.org/vp9/mp4/
+type SmDmBox struct {
+	Version                 byte
+	Flags                   uint32
+	PrimaryRChromaticityX   uint16
+	PrimaryRChromaticityY   uint16
+	PrimaryGChromaticityX   uint16
+	PrimaryGChromaticityY   uint16
+	PrimaryBChromaticityX   uint16
+	PrimaryBChromaticityY   uint16
+	WhitePointChromaticityX uint16
+	WhitePointChromaticityY uint16
+	LuminanceMax            uint32
+	LuminanceMin            uint32
+}
+
+// CreateSmDmBox - Create a new SmDmBox with specified values
+func CreateSmDmBox(primaryRX, primaryRY, primaryGX, primaryGY, primaryBX, primaryBY, whitePointX, whitePointY uint16,
+	luminanceMax, luminanceMin uint32) *SmDmBox {
+	return &SmDmBox{
+		Version:                 0,
+		Flags:                   0,
+		PrimaryRChromaticityX:   primaryRX,
+		PrimaryRChromaticityY:   primaryRY,
+		PrimaryGChromaticityX:   primaryGX,
+		PrimaryGChromaticityY:   primaryGY,
+		PrimaryBChromaticityX:   primaryBX,
+		PrimaryBChromaticityY:   primaryBY,
+		WhitePointChromaticityX: whitePointX,
+		WhitePointChromaticityY: whitePointY,
+		LuminanceMax:            luminanceMax,
+		LuminanceMin:            luminanceMin,
+	}
+}
+
+const smDmBoxSize = boxHeaderSize + 4 + 8*2 + 2*4 // Header + version/flags + 8 uint16s + 2 uint32s
+
+// DecodeSmDm - box-specific decode
+func DecodeSmDm(hdr BoxHeader, startPos uint64, r io.Reader) (Box, error) {
+	// Only allow header size of 8 and correct total box size
+	if hdr.Hdrlen != boxHeaderSize || hdr.Size != smDmBoxSize {
+		return nil, fmt.Errorf("invalid box size %d", hdr.Size)
+	}
+	data, err := readBoxBody(r, hdr)
+	if err != nil {
+		return nil, err
+	}
+	sr := bits.NewFixedSliceReader(data)
+	return DecodeSmDmSR(hdr, startPos, sr)
+}
+
+// DecodeSmDmSR - decode box from SliceReader
+func DecodeSmDmSR(hdr BoxHeader, startPos uint64, sr bits.SliceReader) (Box, error) {
+	// Only allow header size of 8 and correct total box size
+	if hdr.Hdrlen != boxHeaderSize || hdr.Size != smDmBoxSize {
+		return nil, fmt.Errorf("invalid box size %d", hdr.Size)
+	}
+	b := &SmDmBox{}
+	b.Version = sr.ReadUint8()
+	b.Flags = sr.ReadUint24()
+	b.PrimaryRChromaticityX = sr.ReadUint16()
+	b.PrimaryRChromaticityY = sr.ReadUint16()
+	b.PrimaryGChromaticityX = sr.ReadUint16()
+	b.PrimaryGChromaticityY = sr.ReadUint16()
+	b.PrimaryBChromaticityX = sr.ReadUint16()
+	b.PrimaryBChromaticityY = sr.ReadUint16()
+	b.WhitePointChromaticityX = sr.ReadUint16()
+	b.WhitePointChromaticityY = sr.ReadUint16()
+	b.LuminanceMax = sr.ReadUint32()
+	b.LuminanceMin = sr.ReadUint32()
+	return b, sr.AccError()
+}
+
+// Type - box type
+func (b *SmDmBox) Type() string {
+	return "SmDm"
+}
+
+// Size - calculated size of box
+func (b *SmDmBox) Size() uint64 {
+	return smDmBoxSize
+}
+
+// Encode - write box to w
+func (b *SmDmBox) Encode(w io.Writer) error {
+	sw := bits.NewFixedSliceWriter(int(b.Size()))
+	err := b.EncodeSW(sw)
+	if err != nil {
+		return err
+	}
+	_, err = w.Write(sw.Bytes())
+	return err
+}
+
+// EncodeSW - box-specific encode to slicewriter
+func (b *SmDmBox) EncodeSW(sw bits.SliceWriter) error {
+	err := EncodeHeaderSW(b, sw)
+	if err != nil {
+		return err
+	}
+	sw.WriteUint8(b.Version)
+	sw.WriteUint24(b.Flags)
+	sw.WriteUint16(b.PrimaryRChromaticityX)
+	sw.WriteUint16(b.PrimaryRChromaticityY)
+	sw.WriteUint16(b.PrimaryGChromaticityX)
+	sw.WriteUint16(b.PrimaryGChromaticityY)
+	sw.WriteUint16(b.PrimaryBChromaticityX)
+	sw.WriteUint16(b.PrimaryBChromaticityY)
+	sw.WriteUint16(b.WhitePointChromaticityX)
+	sw.WriteUint16(b.WhitePointChromaticityY)
+	sw.WriteUint32(b.LuminanceMax)
+	sw.WriteUint32(b.LuminanceMin)
+	return sw.AccError()
+}
+
+// Info - write box-specific information
+func (b *SmDmBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string) error {
+	bd := newInfoDumper(w, indent, b, -1, 0)
+	bd.write(" - primaryR: (%d,%d)", b.PrimaryRChromaticityX, b.PrimaryRChromaticityY)
+	bd.write(" - primaryG: (%d,%d)", b.PrimaryGChromaticityX, b.PrimaryGChromaticityY)
+	bd.write(" - primaryB: (%d,%d)", b.PrimaryBChromaticityX, b.PrimaryBChromaticityY)
+	bd.write(" - whitePoint: (%d,%d)", b.WhitePointChromaticityX, b.WhitePointChromaticityY)
+	bd.write(" - luminanceMax: %d", b.LuminanceMax)
+	bd.write(" - luminanceMin: %d", b.LuminanceMin)
+	return bd.err
+}
diff --git a/mp4/visualsampleentry.go b/mp4/visualsampleentry.go
index 5e1ed9d..4d561c8 100644
--- a/mp4/visualsampleentry.go
+++ b/mp4/visualsampleentry.go
@@ -26,6 +26,8 @@ type VisualSampleEntryBox struct {
 	Clap               *ClapBox
 	Pasp               *PaspBox
 	Sinf               *SinfBox
+	SmDm               *SmDmBox
+	CoLL               *CoLLBox
 	Children           []Box
 }
 
@@ -74,6 +76,10 @@ func (b *VisualSampleEntryBox) AddChild(child Box) {
 		b.Pasp = box
 	case *SinfBox:
 		b.Sinf = box
+	case *SmDmBox:
+		b.SmDm = box
+	case *CoLLBox:
+		b.CoLL = box
 	}
 	b.Children = append(b.Children, child)
 }
diff --git a/mp4/vppc.go b/mp4/vppc.go
index f1782ea..9264bc3 100644
--- a/mp4/vppc.go
+++ b/mp4/vppc.go
@@ -70,8 +70,9 @@ func DecodeVppCSR(hdr BoxHeader, startPos uint64, sr bits.SliceReader) (Box, err
 	if hdr.Size != b.expectedSize(codecInitSize) {
 		return nil, fmt.Errorf("incorrect box size")
 	}
-	b.CodecInitData = sr.ReadBytes(int(codecInitSize))
-
+	if codecInitSize != 0 {
+		b.CodecInitData = sr.ReadBytes(int(codecInitSize))
+	}
 	return &b, sr.AccError()
 }
 


Test patch:
diff --git a/mp4/coll_test.go b/mp4/coll_test.go
new file mode 100644
index 0000000..4c08f2e
--- /dev/null
+++ b/mp4/coll_test.go
@@ -0,0 +1,21 @@
+package mp4
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestCoLL(t *testing.T) {
+	// Create a sample CoLL box with test values
+	coll := CreateCoLLBox(
+		1000, // maxCLL
+		500,  // maxFALL
+	)
+
+	// Test the box using the common test function
+	boxDiffAfterEncodeAndDecode(t, coll)
+
+	// Test bad box size
+	data := encodeBox(t, coll)
+	changeBoxSizeAndAssertError(t, data, 0, uint32(coll.Size()-1), fmt.Sprintf("decode CoLL pos 0: invalid box size %d", coll.Size()-1))
+}
diff --git a/mp4/helpers_test.go b/mp4/helpers_test.go
index 14a2881..ddff54a 100644
--- a/mp4/helpers_test.go
+++ b/mp4/helpers_test.go
@@ -141,14 +141,31 @@ func assertBoxDecodeError(t *testing.T, data []byte, pos uint64, errMsg string)
 	t.Helper()
 	_, err := DecodeBox(pos, bytes.NewBuffer(data))
 	if err == nil || err.Error() != errMsg {
-		t.Errorf("DecodeBox: Expected error msg: %q", errMsg)
+		got := ""
+		if err != nil {
+			got = err.Error()
+		}
+		t.Errorf("DecodeBox: Expected error msg: %q, got: %q", errMsg, got)
 	}
 	_, err = DecodeBoxSR(pos, bits.NewFixedSliceReader(data))
 	if err == nil || err.Error() != errMsg {
-		t.Errorf("DecodeBoxSR: Expected error msg: %q", errMsg)
+		got := ""
+		if err != nil {
+			got = err.Error()
+		}
+		t.Errorf("DecodeBox: Expected error msg: %q, got: %q", errMsg, got)
 	}
 }
 
+func encodeBox(t *testing.T, box Box) []byte {
+	buf := bytes.Buffer{}
+	err := box.Encode(&buf)
+	if err != nil {
+		t.Error(err)
+	}
+	return buf.Bytes()
+}
+
 // writeGolden - write golden file that to be used for later tests
 func writeGolden(t *testing.T, goldenAssetPath string, data []byte) error {
 	t.Helper()
diff --git a/mp4/smdm_test.go b/mp4/smdm_test.go
new file mode 100644
index 0000000..6ffa8ea
--- /dev/null
+++ b/mp4/smdm_test.go
@@ -0,0 +1,24 @@
+package mp4
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestSmDm(t *testing.T) {
+	// Create a sample SmDm box with test values
+	smdm := CreateSmDmBox(
+		1000, 2000, // Primary R
+		3000, 4000, // Primary G
+		5000, 6000, // Primary B
+		7000, 8000, // White Point
+		10000, 100, // Luminance Max/Min
+	)
+
+	// Test the box using the common test function
+	boxDiffAfterEncodeAndDecode(t, smdm)
+
+	// Test bad box size
+	data := encodeBox(t, smdm)
+	changeBoxSizeAndAssertError(t, data, 0, uint32(smdm.Size()-1), fmt.Sprintf("decode SmDm pos 0: invalid box size %d", smdm.Size()-1))
+}