diff --git a/box_info.go b/box_info.go index 402b418..1cb4b8a 100644 --- a/box_info.go +++ b/box_info.go @@ -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 diff --git a/box_types_metadata.go b/box_types_metadata.go index de21469..7baba22 100644 --- a/box_types_metadata.go +++ b/box_types_metadata.go @@ -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"` @@ -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"` +} + +// 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 ****************************/ + +// 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 +} diff --git a/box_types_metadata_test.go b/box_types_metadata_test.go index ebce386..97decaf 100644 --- a/box_types_metadata_test.go +++ b/box_types_metadata_test.go @@ -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) { diff --git a/mp4.go b/mp4.go index 6aa5b30..c8c55f7 100644 --- a/mp4.go +++ b/mp4.go @@ -1,6 +1,7 @@ package mp4 import ( + "encoding/binary" "errors" "fmt" "reflect" @@ -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]}) @@ -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 } diff --git a/read.go b/read.go index e4fadb1..7118d80 100644 --- a/read.go +++ b/read.go @@ -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 @@ -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 { diff --git a/read_test.go b/read_test.go index 9653380..72b4804 100644 --- a/read_test.go +++ b/read_test.go @@ -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() @@ -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() @@ -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() @@ -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) { diff --git a/testdata/sample_qt.mp4 b/testdata/sample_qt.mp4 index 3ec33ce..b4a07fc 100644 Binary files a/testdata/sample_qt.mp4 and b/testdata/sample_qt.mp4 differ