Skip to content

Commit

Permalink
Merge pull request #159 from dtrejod/dt/keys-metadata
Browse files Browse the repository at this point in the history
improvement: Add support for keys and numbered ilst items BoxTypes.
  • Loading branch information
sunfish-shogi committed Jan 16, 2024
2 parents cd92953 + 848082d commit c058e0e
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 40 deletions.
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"`
}

// 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
}
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.

0 comments on commit c058e0e

Please sign in to comment.