Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: Add support for keys and numbered ilst items BoxTypes. #159

Merged
merged 9 commits into from
Jan 16, 2024
3 changes: 3 additions & 0 deletions box_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ type Context struct {
// IsQuickTimeCompatible represents whether ftyp.compatible_brands contains "qt ".
IsQuickTimeCompatible bool

// QuickTimeKeysMetaEntryCount the expected number of items under the ilst box as observed from the keys box
QuickTimeKeysMetaEntryCount int

// UnderWave represents whether current box is under the wave box.
UnderWave bool

Expand Down
85 changes: 85 additions & 0 deletions box_types_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ const (
DataTypeFloat64BigEndian = 23
)

// Data is a Value BoxType
// https://developer.apple.com/documentation/quicktime-file-format/value_atom
type Data struct {
Box
DataType uint32 `mp4:"0,size=32"`
Expand Down Expand Up @@ -167,6 +169,89 @@ func (sd *StringData) StringifyField(name string, indent string, depth int, ctx
return "", false
}

/*************************** numbered items ****************************/

// Item is a numbered item under an item list atom
// https://developer.apple.com/documentation/quicktime-file-format/metadata_item_list_atom/item_list
type Item struct {
AnyTypeBox
Version uint8 `mp4:"0,size=8"`
Flags [3]byte `mp4:"1,size=8"`
ItemName []byte `mp4:"2,size=8,len=4"`
Data Data `mp4:"3"`
dtrejod marked this conversation as resolved.
Show resolved Hide resolved
}

// StringifyField returns field value as string
func (i *Item) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) {
switch name {
case "ItemName":
return fmt.Sprintf("\"%s\"", util.EscapeUnprintables(string(i.ItemName))), true
}
return "", false
}

func isUnderIlstFreeFormat(ctx Context) bool {
return ctx.UnderIlstFreeMeta
}

func BoxTypeKeys() BoxType { return StrToBoxType("keys") }

func init() {
AddBoxDef(&Keys{})
}

/*************************** keys ****************************/
dtrejod marked this conversation as resolved.
Show resolved Hide resolved

// Keys is the Keys BoxType
// https://developer.apple.com/documentation/quicktime-file-format/metadata_item_keys_atom
type Keys struct {
FullBox `mp4:"0,extend"`
EntryCount int32 `mp4:"1,size=32"`
Entries []Key `mp4:"2,len=dynamic"`
}

// GetType implements the IBox interface and returns the BoxType
func (*Keys) GetType() BoxType {
return BoxTypeKeys()
}

// GetFieldLength implements the ICustomFieldObject interface and returns the length of dynamic fields
func (k *Keys) GetFieldLength(name string, ctx Context) uint {
switch name {
case "Entries":
return uint(k.EntryCount)
}
panic(fmt.Errorf("invalid name of dynamic-length field: boxType=keys fieldName=%s", name))
}

/*************************** key ****************************/

// Key is a key value field in the Keys BoxType
// https://developer.apple.com/documentation/quicktime-file-format/metadata_item_keys_atom/key_value_key_size-8
type Key struct {
BaseCustomFieldObject
KeySize int32 `mp4:"0,size=32"`
KeyNamespace []byte `mp4:"1,size=8,len=4"`
KeyValue []byte `mp4:"2,size=8,len=dynamic"`
}

// GetFieldLength implements the ICustomFieldObject interface and returns the length of dynamic fields
func (k *Key) GetFieldLength(name string, ctx Context) uint {
switch name {
case "KeyValue":
// sizeOf(KeySize)+sizeOf(KeyNamespace) = 8 bytes
return uint(k.KeySize) - 8
}
panic(fmt.Errorf("invalid name of dynamic-length field: boxType=key fieldName=%s", name))
}

// StringifyField returns field value as string
func (k *Key) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) {
switch name {
case "KeyNamespace":
return fmt.Sprintf("\"%s\"", util.EscapeUnprintables(string(k.KeyNamespace))), true
case "KeyValue":
return fmt.Sprintf("\"%s\"", util.EscapeUnprintables(string(k.KeyValue))), true
}
return "", false
}
54 changes: 54 additions & 0 deletions box_types_metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,60 @@ func TestBoxTypesMetadata(t *testing.T) {
str: `Data=".foo"`,
ctx: Context{UnderIlstFreeMeta: true},
},
{
name: "ilst numbered item",
src: &Item{
AnyTypeBox: AnyTypeBox{Type: Uint32ToBoxType(1)},
Version: 0,
Flags: [3]byte{'0'},
ItemName: []byte("data"),
Data: Data{DataType: 0, DataLang: 0x12345678, Data: []byte("foo")}},
dst: &Item{
AnyTypeBox: AnyTypeBox{Type: Uint32ToBoxType(1)},
},
bin: []byte{
0x00, // Version
0x30, 0x00, 0x0, // Flags
0x64, 0x61, 0x74, 0x61, // Item Name
0x0, 0x0, 0x0, 0x0, // data type
0x12, 0x34, 0x56, 0x78, // data lang
0x66, 0x6f, 0x6f, // data
},
str: `Version=0 Flags=0x000000 ItemName="data" Data={DataType=BINARY DataLang=305419896 Data=[0x66, 0x6f, 0x6f]}`,
ctx: Context{UnderIlst: true, QuickTimeKeysMetaEntryCount: 1},
},
{
name: "keys",
src: &Keys{
EntryCount: 2,
Entries: []Key{
{
KeySize: 27,
KeyNamespace: []byte("mdta"),
KeyValue: []byte("com.android.version"),
},
{
KeySize: 25,
KeyNamespace: []byte("mdta"),
KeyValue: []byte("com.android.model"),
},
},
},
dst: &Keys{},
bin: []byte{
0x0, // Version
0x0, 0x0, 0x0, // Flags
0x0, 0x0, 0x0, 0x2, // entry count
0x0, 0x0, 0x0, 0x1b, // entry 1 keysize
0x6d, 0x64, 0x74, 0x61, // entry 1 key namespace
0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, // entry 1 key value
0x0, 0x0, 0x0, 0x19, // entry 2 keysize
0x6d, 0x64, 0x74, 0x61, // entry 2 key namespace
0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, // entry 2 key value
},
str: `Version=0 Flags=0x000000 EntryCount=2 Entries=[{KeySize=27 KeyNamespace="mdta" KeyValue="com.android.version"}, {KeySize=25 KeyNamespace="mdta" KeyValue="com.android.model"}]`,
ctx: Context{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
19 changes: 19 additions & 0 deletions mp4.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mp4

import (
"encoding/binary"
"errors"
"fmt"
"reflect"
Expand All @@ -19,6 +20,13 @@ func StrToBoxType(code string) BoxType {
return BoxType{code[0], code[1], code[2], code[3]}
}

// Uint32ToBoxType returns a new BoxType from the provied uint32
func Uint32ToBoxType(i uint32) BoxType {
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, i)
return BoxType{b[0], b[1], b[2], b[3]}
}

func (boxType BoxType) String() string {
if isPrintable(boxType[0]) && isPrintable(boxType[1]) && isPrintable(boxType[2]) && isPrintable(boxType[3]) {
s := string([]byte{boxType[0], boxType[1], boxType[2], boxType[3]})
Expand Down Expand Up @@ -100,6 +108,17 @@ func (boxType BoxType) getBoxDef(ctx Context) *boxDef {
return boxDef
}
}
if ctx.UnderIlst {
typeID := int(binary.BigEndian.Uint32(boxType[:]))
if typeID >= 1 && typeID <= ctx.QuickTimeKeysMetaEntryCount {
payload := &Item{}
return &boxDef{
dataType: reflect.TypeOf(payload).Elem(),
isTarget: isIlstMetaContainer,
fields: buildFields(payload),
}
}
}
return nil
}

Expand Down
17 changes: 17 additions & 0 deletions read.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ func readBoxStructureFromInternal(r io.ReadSeeker, bi *BoxInfo, path BoxPath, ha
}
}

// parse numbered ilst items after keys box by saving EntryCount field to context
if bi.Type == BoxTypeKeys() {
var keys Keys
if _, err := Unmarshal(r, bi.Size-bi.HeaderSize, &keys, bi.Context); err != nil {
return nil, err
}
bi.QuickTimeKeysMetaEntryCount = int(keys.EntryCount)
if _, err := bi.SeekToPayload(r); err != nil {
return nil, err
}
}

ctx := bi.Context
if bi.Type == BoxTypeWave() {
ctx.UnderWave = true
Expand Down Expand Up @@ -172,6 +184,11 @@ func readBoxStructure(r io.ReadSeeker, totalSize uint64, isRoot bool, path BoxPa
if bi.IsQuickTimeCompatible {
ctx.IsQuickTimeCompatible = true
}

// preserve keys entry count on context for subsequent ilst number item box
if bi.Type == BoxTypeKeys() {
ctx.QuickTimeKeysMetaEntryCount = bi.QuickTimeKeysMetaEntryCount
}
}

if totalSize != 0 && !ctx.IsQuickTimeCompatible {
Expand Down
101 changes: 61 additions & 40 deletions read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,14 @@ func TestReadBoxStructureQT(t *testing.T) {
_, err = ReadBoxStructure(f, func(h *ReadHandle) (interface{}, error) {
n++
switch n {
case 5, 45: // unsupported
case 51, 44: // unsupported
require.False(t, h.BoxInfo.IsSupportedType())
buf := bytes.NewBuffer(nil)
n, err := h.ReadData(buf)
require.NoError(t, err)
require.Equal(t, h.BoxInfo.Size-h.BoxInfo.HeaderSize, n)
assert.Len(t, buf.Bytes(), int(n))
case 40: // mp4a
case 39: // mp4a
require.True(t, h.BoxInfo.IsSupportedType())
require.Equal(t, StrToBoxType("mp4a"), h.BoxInfo.Type)
box, n, err := h.ReadPayload()
Expand All @@ -158,7 +158,7 @@ func TestReadBoxStructureQT(t *testing.T) {
assert.Equal(t, []byte{0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2}, box.(*AudioSampleEntry).QuickTimeData)
_, err = h.Expand()
require.NoError(t, err)
case 43: // mp4a
case 42: // mp4a
require.True(t, h.BoxInfo.IsSupportedType())
require.Equal(t, StrToBoxType("mp4a"), h.BoxInfo.Type)
box, n, err := h.ReadPayload()
Expand All @@ -167,6 +167,20 @@ func TestReadBoxStructureQT(t *testing.T) {
assert.Equal(t, []byte{0x0, 0x0, 0x0, 0x0}, box.(*AudioSampleEntry).QuickTimeData)
_, err = h.Expand()
require.NoError(t, err)
case 54: // keys
require.True(t, h.BoxInfo.IsSupportedType())
require.Equal(t, StrToBoxType("keys"), h.BoxInfo.Type)
box, n, err := h.ReadPayload()
require.NoError(t, err)
require.Equal(t, uint64(35), n)
assert.Equal(t, int32(1), box.(*Keys).EntryCount)
_, err = h.Expand()
require.NoError(t, err)
case 56: // ilst number item
require.True(t, h.BoxInfo.IsSupportedType())
_, err = h.Expand()
require.Equal(t, Uint32ToBoxType(1), h.BoxInfo.Type)
require.NoError(t, err)
default: // otherwise
require.True(t, h.BoxInfo.IsSupportedType())
_, err = h.Expand()
Expand All @@ -175,59 +189,66 @@ func TestReadBoxStructureQT(t *testing.T) {
return nil, nil
})
require.NoError(t, err)
assert.Equal(t, 49, n)
assert.Equal(t, 56, n)
}

// > mp4tool dump -full mp4a sample_qt.mp4 | cat -n
// 1 [ftyp] Size=20 MajorBrand="qt " MinorVersion=512 CompatibleBrands=[{CompatibleBrand="qt "}]
// 2 [free] Size=42 Data=[...] (use "-full free" to show all)
// 3 [moov] Size=340232
// 4 [udta] Size=31
// 5 [(c)enc] (unsupported box type) Size=23 Data=[...] (use "-full (c)enc" to show all)
// 3 [ftyp] Size=20 MajorBrand="qt " MinorVersion=512 CompatibleBrands=[{CompatibleBrand="qt "}]
// 4 [free] Size=42 Data=[...] (use "-full free" to show all)
// 5 [moov] Size=340357
// 6 [mvhd] Size=108 ... (use "-full mvhd" to show all)
// 7 [trak] Size=115889
// 8 [tkhd] Size=92 ... (use "-full tkhd" to show all)
// 9 [mdia] Size=115789
// 10 [mdhd] Size=32 Version=0 Flags=0x000000 CreationTimeV0=2082844800 ModificationTimeV0=2082844800 Timescale=24 DurationV0=14315 Language="```" PreDefined=0
// 10 [mdhd] Size=32 Version=0 Flags=0x000000 CreationTimeV0=2082844800 ModificationTimeV0=2082844800 Timescale=24 DurationV0=14315 Language="und" PreDefined=0
// 11 [hdlr] Size=45 Version=0 Flags=0x000000 PreDefined=1835560050 HandlerType="vide" Name="VideoHandler"
// 12 [minf] Size=115704
// 13 [hdlr] Size=44 Version=0 Flags=0x000000 PreDefined=1684565106 HandlerType="url " Name="DataHandler"
// 14 [vmhd] Size=20 Version=0 Flags=0x000001 Graphicsmode=0 Opcolor=[0, 0, 0]
// 15 [dinf] Size=36
// 16 [dref] Size=28 Version=0 Flags=0x000000 EntryCount=1
// 17 [url ] Size=12 Version=0 Flags=0x000001
// 18 [stbl] Size=115596
// 19 [stsd] Size=148 Version=0 Flags=0x000000 EntryCount=1
// 20 [avc1] Size=132 ... (use "-full avc1" to show all)
// 21 [avcC] Size=46 ... (use "-full avcC" to show all)
// 22 [stts] Size=24 Version=0 Flags=0x000000 EntryCount=1 Entries=[{SampleCount=14315 SampleDelta=1}]
// 23 [stss] Size=832 ... (use "-full stss" to show all)
// 24 [stsc] Size=28 Version=0 Flags=0x000000 EntryCount=1 Entries=[{FirstChunk=1 SamplesPerChunk=1 SampleDescriptionIndex=1}]
// 25 [stsz] Size=57280 ... (use "-full stsz" to show all)
// 26 [stco] Size=57276 ... (use "-full stco" to show all)
// 13 [vmhd] Size=20 Version=0 Flags=0x000001 Graphicsmode=0 Opcolor=[0, 0, 0]
// 14 [dinf] Size=36
// 15 [dref] Size=28 Version=0 Flags=0x000000 EntryCount=1
// 16 [url ] Size=12 Version=0 Flags=0x000001
// 17 [stbl] Size=115596
// 18 [stsd] Size=148 Version=0 Flags=0x000000 EntryCount=1
// 19 [avc1] Size=132 DataReferenceIndex=1 PreDefined=0 PreDefined2=[1179012432, 512, 512] Width=424 Height=240 Horizresolution=4718592 Vertresolution=4718592 FrameCount=1 Compressorname="libx264" Depth=24 PreDefined3=-1
// 20 [avcC] Size=46 ... (use "-full avcC" to show all)
// 21 [stts] Size=24 Version=0 Flags=0x000000 EntryCount=1 Entries=[{SampleCount=14315 SampleDelta=1}]
// 22 [stss] Size=832 ... (use "-full stss" to show all)
// 23 [stsc] Size=28 Version=0 Flags=0x000000 EntryCount=1 Entries=[{FirstChunk=1 SamplesPerChunk=1 SampleDescriptionIndex=1}]
// 24 [stsz] Size=57280 ... (use "-full stsz" to show all)
// 25 [stco] Size=57276 ... (use "-full stco" to show all)
// 26 [hdlr] Size=44 Version=0 Flags=0x000000 PreDefined=1684565106 HandlerType="url " Name="DataHandler"
// 27 [trak] Size=224196
// 28 [tkhd] Size=92 ... (use "-full tkhd" to show all)
// 29 [mdia] Size=224096
// 30 [mdhd] Size=32 Version=0 Flags=0x000000 CreationTimeV0=2082844800 ModificationTimeV0=2082844800 Timescale=48000 DurationV0=28628992 Language="```" PreDefined=0
// 30 [mdhd] Size=32 Version=0 Flags=0x000000 CreationTimeV0=2082844800 ModificationTimeV0=2082844800 Timescale=48000 DurationV0=28628992 Language="und" PreDefined=0
// 31 [hdlr] Size=45 Version=0 Flags=0x000000 PreDefined=1835560050 HandlerType="soun" Name="SoundHandler"
// 32 [minf] Size=224011
// 33 [hdlr] Size=44 Version=0 Flags=0x000000 PreDefined=1684565106 HandlerType="url " Name="DataHandler"
// 34 [smhd] Size=16 Version=0 Flags=0x000000 Balance=0
// 35 [dinf] Size=36
// 36 [dref] Size=28 Version=0 Flags=0x000000 EntryCount=1
// 37 [url ] Size=12 Version=0 Flags=0x000001
// 38 [stbl] Size=223907
// 39 [stsd] Size=147 Version=0 Flags=0x000000 EntryCount=1
// 40 [mp4a] Size=131 DataReferenceIndex=1 EntryVersion=1 ChannelCount=2 SampleSize=16 PreDefined=65534 SampleRate=3145728000 QuickTimeData=[0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2]
// 41 [wave] Size=79
// 42 [frma] Size=12 DataFormat="mp4a"
// 43 [mp4a] Size=12 QuickTimeData=[0x0, 0x0, 0x0, 0x0]
// 44 [esds] Size=39 ... (use "-full esds" to show all)
// 45 [0x00000000] (unsupported box type) Size=8 Data=[...] (use "-full 0x00000000" to show all)
// 46 [stts] Size=24 Version=0 Flags=0x000000 EntryCount=1 Entries=[{SampleCount=27958 SampleDelta=1024}]
// 47 [stsc] Size=28 Version=0 Flags=0x000000 EntryCount=1 Entries=[{FirstChunk=1 SamplesPerChunk=1 SampleDescriptionIndex=1}]
// 48 [stsz] Size=111852 ... (use "-full stsz" to show all)
// 49 [stco] Size=111848 ... (use "-full stco" to show all)
// 33 [smhd] Size=16 Version=0 Flags=0x000000 Balance=0
// 34 [dinf] Size=36
// 35 [dref] Size=28 Version=0 Flags=0x000000 EntryCount=1
// 36 [url ] Size=12 Version=0 Flags=0x000001
// 37 [stbl] Size=223907
// 38 [stsd] Size=147 Version=0 Flags=0x000000 EntryCount=1
// 39 [mp4a] Size=131 DataReferenceIndex=1 EntryVersion=1 ChannelCount=2 SampleSize=16 PreDefined=65534 SampleRate=48000 QuickTimeData=[0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2]
// 40 [wave] Size=79
// 41 [frma] Size=12 DataFormat="mp4a"
// 42 [mp4a] Size=12 QuickTimeData=[0x0, 0x0, 0x0, 0x0]
// 43 [esds] Size=39 ... (use "-full esds" to show all)
// 44 [0x00000000] (unsupported box type) Size=8 Data=[...] (use "-full 0x00000000" to show all)
// 45 [stts] Size=24 Version=0 Flags=0x000000 EntryCount=1 Entries=[{SampleCount=27958 SampleDelta=1024}]
// 46 [stsc] Size=28 Version=0 Flags=0x000000 EntryCount=1 Entries=[{FirstChunk=1 SamplesPerChunk=1 SampleDescriptionIndex=1}]
// 47 [stsz] Size=111852 ... (use "-full stsz" to show all)
// 48 [stco] Size=111848 ... (use "-full stco" to show all)
// 49 [hdlr] Size=44 Version=0 Flags=0x000000 PreDefined=1684565106 HandlerType="url " Name="DataHandler"
// 50 [udta] Size=156
// 51 [(c)enc] (unsupported box type) Size=23 Data=[...] (use "-full (c)enc" to show all)
// 52 [meta] Size=125 Version=0 Flags=0x000000
// 53 [hdlr] Size=33 Version=0 Flags=0x000000 PreDefined=0 HandlerType="mdta" Name=""
// 54 [keys] Size=43 Version=0 Flags=0x000000 EntryCount=1 Entries=[{KeySize=27 KeyNamespace="mdta" KeyValue="com.android.version"}]
// 55 [ilst] Size=37
// 56 [0x00000001] Size=29 Version=0 Flags=0x000000 ItemName="data" Data={DataType=UTF8 DataLang=0 Data="1.0.0"}

// this used to cause an infinite loop.
func TestReadBoxStructureZeroSize(t *testing.T) {
Expand Down
Binary file modified testdata/sample_qt.mp4
Binary file not shown.