├── .gitignore ├── testdata ├── sample.mp4 ├── sample_qt.mp4 ├── sample_fragmented.mp4 ├── sample_init.enca.mp4 └── sample_init.encv.mp4 ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── other.md │ ├── question.md │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── test.yml ├── internal ├── bitio │ ├── bitio.go │ ├── write.go │ ├── write_test.go │ ├── read.go │ └── read_test.go └── util │ ├── io.go │ ├── string.go │ ├── io_test.go │ └── string_test.go ├── anytype.go ├── go.mod ├── box_types_3gpp.go ├── cmd └── mp4tool │ ├── internal │ ├── util │ │ └── util.go │ ├── psshdump │ │ ├── psshdump_test.go │ │ └── psshdump.go │ ├── extract │ │ ├── extract_test.go │ │ └── extract.go │ ├── edit │ │ └── edit.go │ ├── probe │ │ ├── probe_test.go │ │ └── probe.go │ ├── dump │ │ └── dump.go │ └── divide │ │ └── divide.go │ └── main.go ├── box_types_iso23001_5.go ├── box_types_etsi_ts_102_366.go ├── LICENSE ├── mp4_test.go ├── box_types_opus.go ├── box_types_av1.go ├── box_types_vp.go ├── write.go ├── box_test.go ├── box_types_iso23001_5_test.go ├── box_types_etsi_ts_102_366_test.go ├── box_types_3gpp_test.go ├── box_types_opus_test.go ├── extract.go ├── box_types_vp_test.go ├── box_types_av1_test.go ├── go.sum ├── box_types_iso14496_30.go ├── box_types_iso23001_7.go ├── string_test.go ├── box_types_iso14496_14.go ├── box_types_iso14496_30_test.go ├── write_test.go ├── mp4.go ├── box_info.go ├── box_info_test.go ├── box.go ├── box_types_iso14496_14_test.go ├── README.md ├── read.go ├── string.go ├── extract_test.go ├── field.go ├── box_types_metadata.go ├── box_types_iso23001_7_test.go ├── box_types_metadata_test.go ├── marshaller_test.go └── field_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /testdata/sample.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abema/go-mp4/HEAD/testdata/sample.mp4 -------------------------------------------------------------------------------- /testdata/sample_qt.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abema/go-mp4/HEAD/testdata/sample_qt.mp4 -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | @sunfish-shogi 2 | 3 | 4 | -------------------------------------------------------------------------------- /testdata/sample_fragmented.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abema/go-mp4/HEAD/testdata/sample_fragmented.mp4 -------------------------------------------------------------------------------- /testdata/sample_init.enca.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abema/go-mp4/HEAD/testdata/sample_init.enca.mp4 -------------------------------------------------------------------------------- /testdata/sample_init.encv.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abema/go-mp4/HEAD/testdata/sample_init.encv.mp4 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Others 3 | about: Others 4 | title: '' 5 | labels: '' 6 | assignees: sunfish-shogi 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Question about this project 4 | title: '' 5 | labels: 'question' 6 | assignees: sunfish-shogi 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: sunfish-shogi 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: sunfish-shogi 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /internal/bitio/bitio.go: -------------------------------------------------------------------------------- 1 | package bitio 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvalidAlignment = errors.New("invalid alignment") 7 | ErrDiscouragedReader = errors.New("discouraged reader implementation") 8 | ) 9 | -------------------------------------------------------------------------------- /anytype.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | type IAnyType interface { 4 | IBox 5 | SetType(BoxType) 6 | } 7 | 8 | type AnyTypeBox struct { 9 | Box 10 | Type BoxType 11 | } 12 | 13 | func (e *AnyTypeBox) GetType() BoxType { 14 | return e.Type 15 | } 16 | 17 | func (e *AnyTypeBox) SetType(boxType BoxType) { 18 | e.Type = boxType 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/abema/go-mp4 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/google/uuid v1.1.2 8 | github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e 9 | github.com/stretchr/testify v1.4.0 10 | github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365 11 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 12 | gopkg.in/src-d/go-billy.v4 v4.3.2 13 | gopkg.in/yaml.v2 v2.2.8 14 | ) 15 | -------------------------------------------------------------------------------- /internal/util/io.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | func ReadString(r io.Reader) (string, error) { 9 | b := make([]byte, 1) 10 | buf := bytes.NewBuffer(nil) 11 | for { 12 | if _, err := r.Read(b); err != nil { 13 | return "", err 14 | } 15 | if b[0] == 0 { 16 | return buf.String(), nil 17 | } 18 | buf.Write(b) 19 | } 20 | } 21 | 22 | func WriteString(w io.Writer, s string) error { 23 | if _, err := w.Write([]byte(s)); err != nil { 24 | return err 25 | } 26 | if _, err := w.Write([]byte{0}); err != nil { 27 | return err 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /box_types_3gpp.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | var udta3GppMetaBoxTypes = []BoxType{ 4 | StrToBoxType("titl"), 5 | StrToBoxType("dscp"), 6 | StrToBoxType("cprt"), 7 | StrToBoxType("perf"), 8 | StrToBoxType("auth"), 9 | StrToBoxType("gnre"), 10 | } 11 | 12 | func init() { 13 | for _, bt := range udta3GppMetaBoxTypes { 14 | AddAnyTypeBoxDefEx(&Udta3GppString{}, bt, isUnderUdta, 0) 15 | } 16 | } 17 | 18 | type Udta3GppString struct { 19 | AnyTypeBox 20 | FullBox `mp4:"0,extend"` 21 | Pad bool `mp4:"1,size=1,hidden"` 22 | Language [3]byte `mp4:"2,size=5,iso639-2"` // ISO-639-2/T language code 23 | Data []byte `mp4:"3,size=8,string"` 24 | } 25 | -------------------------------------------------------------------------------- /cmd/mp4tool/internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "github.com/abema/go-mp4" 4 | 5 | func ShouldHasNoChildren(boxType mp4.BoxType) bool { 6 | return boxType == mp4.BoxTypeEmsg() || 7 | boxType == mp4.BoxTypeEsds() || 8 | boxType == mp4.BoxTypeFtyp() || 9 | boxType == mp4.BoxTypePssh() || 10 | boxType == mp4.BoxTypeCtts() || 11 | boxType == mp4.BoxTypeCo64() || 12 | boxType == mp4.BoxTypeElst() || 13 | boxType == mp4.BoxTypeSbgp() || 14 | boxType == mp4.BoxTypeSdtp() || 15 | boxType == mp4.BoxTypeStco() || 16 | boxType == mp4.BoxTypeStsc() || 17 | boxType == mp4.BoxTypeStts() || 18 | boxType == mp4.BoxTypeStss() || 19 | boxType == mp4.BoxTypeStsz() || 20 | boxType == mp4.BoxTypeTfra() || 21 | boxType == mp4.BoxTypeTrun() 22 | } 23 | -------------------------------------------------------------------------------- /box_types_iso23001_5.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | /*************************** ipcm ****************************/ 4 | 5 | func BoxTypeIpcm() BoxType { return StrToBoxType("ipcm") } 6 | 7 | func init() { 8 | AddAnyTypeBoxDef(&AudioSampleEntry{}, BoxTypeIpcm()) 9 | } 10 | 11 | /*************************** fpcm ****************************/ 12 | 13 | func BoxTypeFpcm() BoxType { return StrToBoxType("fpcm") } 14 | 15 | func init() { 16 | AddAnyTypeBoxDef(&AudioSampleEntry{}, BoxTypeFpcm()) 17 | } 18 | 19 | /*************************** pcmC ****************************/ 20 | 21 | func BoxTypePcmC() BoxType { return StrToBoxType("pcmC") } 22 | 23 | func init() { 24 | AddBoxDef(&PcmC{}, 0, 1) 25 | } 26 | 27 | type PcmC struct { 28 | FullBox `mp4:"0,extend"` 29 | FormatFlags uint8 `mp4:"1,size=8"` 30 | PCMSampleSize uint8 `mp4:"1,size=8"` 31 | } 32 | 33 | func (PcmC) GetType() BoxType { 34 | return BoxTypePcmC() 35 | } 36 | -------------------------------------------------------------------------------- /internal/util/string.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "unicode" 7 | ) 8 | 9 | func FormatSignedFixedFloat1616(val int32) string { 10 | if val&0xffff == 0 { 11 | return strconv.Itoa(int(val >> 16)) 12 | } else { 13 | return strconv.FormatFloat(float64(val)/(1<<16), 'f', 5, 64) 14 | } 15 | } 16 | 17 | func FormatUnsignedFixedFloat1616(val uint32) string { 18 | if val&0xffff == 0 { 19 | return strconv.Itoa(int(val >> 16)) 20 | } else { 21 | return strconv.FormatFloat(float64(val)/(1<<16), 'f', 5, 64) 22 | } 23 | } 24 | 25 | func FormatSignedFixedFloat88(val int16) string { 26 | if val&0xff == 0 { 27 | return strconv.Itoa(int(val >> 8)) 28 | } else { 29 | return strconv.FormatFloat(float64(val)/(1<<8), 'f', 3, 32) 30 | } 31 | } 32 | 33 | func EscapeUnprintable(r rune) rune { 34 | if unicode.IsGraphic(r) { 35 | return r 36 | } 37 | return rune('.') 38 | } 39 | 40 | func EscapeUnprintables(src string) string { 41 | return strings.Map(EscapeUnprintable, src) 42 | } 43 | -------------------------------------------------------------------------------- /box_types_etsi_ts_102_366.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | /*************************** ac-3 ****************************/ 4 | 5 | // https://www.etsi.org/deliver/etsi_ts/102300_102399/102366/01.04.01_60/ts_102366v010401p.pdf 6 | 7 | func BoxTypeAC3() BoxType { return StrToBoxType("ac-3") } 8 | 9 | func init() { 10 | AddAnyTypeBoxDef(&AudioSampleEntry{}, BoxTypeAC3()) 11 | } 12 | 13 | /*************************** dac3 ****************************/ 14 | 15 | // https://www.etsi.org/deliver/etsi_ts/102300_102399/102366/01.04.01_60/ts_102366v010401p.pdf 16 | 17 | func BoxTypeDAC3() BoxType { return StrToBoxType("dac3") } 18 | 19 | func init() { 20 | AddBoxDef(&Dac3{}) 21 | } 22 | 23 | type Dac3 struct { 24 | Box 25 | Fscod uint8 `mp4:"0,size=2"` 26 | Bsid uint8 `mp4:"1,size=5"` 27 | Bsmod uint8 `mp4:"2,size=3"` 28 | Acmod uint8 `mp4:"3,size=3"` 29 | LfeOn uint8 `mp4:"4,size=1"` 30 | BitRateCode uint8 `mp4:"5,size=5"` 31 | Reserved uint8 `mp4:"6,size=5,const=0"` 32 | } 33 | 34 | func (Dac3) GetType() BoxType { 35 | return BoxTypeDAC3() 36 | } 37 | -------------------------------------------------------------------------------- /internal/util/io_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestReadString(t *testing.T) { 13 | r := bytes.NewReader([]byte{ 14 | 'f', 'i', 'r', 's', 't', 0, 15 | 's', 'e', 'c', 'o', 'n', 'd', 0, 16 | 't', 'h', 'i', 'r', 'd', 0, 17 | }) 18 | s, err := ReadString(r) 19 | require.NoError(t, err) 20 | assert.Equal(t, "first", s) 21 | s, err = ReadString(r) 22 | require.NoError(t, err) 23 | assert.Equal(t, "second", s) 24 | s, err = ReadString(r) 25 | require.NoError(t, err) 26 | assert.Equal(t, "third", s) 27 | _, err = ReadString(r) 28 | assert.Equal(t, io.EOF, err) 29 | } 30 | 31 | func TestWriteString(t *testing.T) { 32 | w := bytes.NewBuffer(nil) 33 | require.NoError(t, WriteString(w, "first")) 34 | require.NoError(t, WriteString(w, "second")) 35 | require.NoError(t, WriteString(w, "third")) 36 | assert.Equal(t, []byte{ 37 | 'f', 'i', 'r', 's', 't', 0, 38 | 's', 'e', 'c', 'o', 'n', 'd', 0, 39 | 't', 'h', 'i', 'r', 'd', 0, 40 | }, w.Bytes()) 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 AbemaTV 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest, macos-latest] 14 | go-version: ["1.23", "1.24", ">=1.25"] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3 18 | - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # ratchet:actions/setup-go@v3 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | check-latest: true 22 | - run: go vet ./... 23 | - run: go test "-coverprofile=coverage.txt" -covermode=atomic ./... 24 | - run: go install ./cmd/mp4tool 25 | - name: Upload Coverage Report 26 | if: ${{ github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.go-version == '1.23' }} 27 | env: 28 | COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} 29 | run: | 30 | go install github.com/mattn/goveralls@latest 31 | goveralls -coverprofile=coverage.txt 32 | -------------------------------------------------------------------------------- /mp4_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestBoxTypeString(t *testing.T) { 11 | assert.Equal(t, "1234", BoxType{'1', '2', '3', '4'}.String()) 12 | assert.Equal(t, "abcd", BoxType{'a', 'b', 'c', 'd'}.String()) 13 | assert.Equal(t, "xx x", BoxType{'x', 'x', ' ', 'x'}.String()) 14 | assert.Equal(t, "xx~x", BoxType{'x', 'x', '~', 'x'}.String()) 15 | assert.Equal(t, "xx(c)x", BoxType{'x', 'x', 0xa9, 'x'}.String()) 16 | assert.Equal(t, "0x7878ab78", BoxType{'x', 'x', 0xab, 'x'}.String()) 17 | } 18 | 19 | func TestIsSupported(t *testing.T) { 20 | assert.True(t, StrToBoxType("pssh").IsSupported(Context{})) 21 | assert.False(t, StrToBoxType("1234").IsSupported(Context{})) 22 | } 23 | 24 | func TestGetSupportedVersions(t *testing.T) { 25 | vers, err := BoxTypePssh().GetSupportedVersions(Context{}) 26 | require.NoError(t, err) 27 | assert.Equal(t, []uint8{0, 1}, vers) 28 | } 29 | 30 | func TestIsSupportedVersion(t *testing.T) { 31 | assert.True(t, BoxTypePssh().IsSupportedVersion(0, Context{})) 32 | assert.True(t, BoxTypePssh().IsSupportedVersion(1, Context{})) 33 | assert.False(t, BoxTypePssh().IsSupportedVersion(2, Context{})) 34 | } 35 | -------------------------------------------------------------------------------- /internal/bitio/write.go: -------------------------------------------------------------------------------- 1 | package bitio 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type Writer interface { 8 | io.Writer 9 | 10 | // alignment: 11 | // |-1-byte-block-|--------------|--------------|--------------| 12 | // |<-offset->|<-------------------width---------------------->| 13 | WriteBits(data []byte, width uint) error 14 | 15 | WriteBit(bit bool) error 16 | } 17 | 18 | type writer struct { 19 | writer io.Writer 20 | octet byte 21 | width uint 22 | } 23 | 24 | func NewWriter(w io.Writer) Writer { 25 | return &writer{writer: w} 26 | } 27 | 28 | func (w *writer) Write(p []byte) (n int, err error) { 29 | if w.width != 0 { 30 | return 0, ErrInvalidAlignment 31 | } 32 | return w.writer.Write(p) 33 | } 34 | 35 | func (w *writer) WriteBits(data []byte, width uint) error { 36 | length := uint(len(data)) * 8 37 | offset := length - width 38 | for i := offset; i < length; i++ { 39 | oi := i / 8 40 | if err := w.WriteBit((data[oi]>>(7-i%8))&0x01 != 0); err != nil { 41 | return err 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | func (w *writer) WriteBit(bit bool) error { 48 | if bit { 49 | w.octet |= 0x1 << (7 - w.width) 50 | } 51 | w.width++ 52 | 53 | if w.width == 8 { 54 | if _, err := w.writer.Write([]byte{w.octet}); err != nil { 55 | return err 56 | } 57 | w.octet = 0x00 58 | w.width = 0 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/bitio/write_test.go: -------------------------------------------------------------------------------- 1 | package bitio 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestWrite(t *testing.T) { 12 | buf := bytes.NewBuffer(nil) 13 | w := NewWriter(buf) 14 | 15 | // 1101,1010 16 | // ^^^ ^^^^ 17 | require.NoError(t, w.WriteBits([]byte{0xda}, 7)) 18 | 19 | // 0000,0111,0110,0011,1101,0101 20 | // ^ ^^^^ ^^^^ ^^^^ ^^^^ 21 | require.NoError(t, w.WriteBits([]byte{0x07, 0x63, 0xd5}, 17)) 22 | 23 | _, err := w.Write([]byte{0xa4, 0x6f}) 24 | require.NoError(t, err) 25 | 26 | // 0000,0111,0110,1001,1110,0011 27 | // ^ ^^^^ ^^^^ ^^^^ ^^^^ 28 | require.NoError(t, w.WriteBits([]byte{0x07, 0x69, 0xe3}, 17)) 29 | 30 | require.NoError(t, w.WriteBit(true)) 31 | require.NoError(t, w.WriteBit(false)) 32 | 33 | // 1111,0111 34 | // ^ ^^^^ 35 | require.NoError(t, w.WriteBits([]byte{0xf7}, 5)) 36 | 37 | assert.Equal(t, []byte{ 38 | 0xb5, 0x63, 0xd5, // 1011,0101,0110,0011,1101,0101 39 | 0xa4, 0x6f, 40 | 0xb4, 0xf1, 0xd7, // 1011,0100,1111,0001,1101,0111 41 | }, buf.Bytes()) 42 | } 43 | 44 | func TestWriteInvalidAlignment(t *testing.T) { 45 | w := NewWriter(bytes.NewBuffer(nil)) 46 | _, err := w.Write([]byte{0xa4, 0x6f}) 47 | require.NoError(t, err) 48 | require.NoError(t, w.WriteBits([]byte{0xda}, 7)) 49 | _, err = w.Write([]byte{0xa4, 0x6f}) 50 | require.Equal(t, ErrInvalidAlignment, err) 51 | } 52 | -------------------------------------------------------------------------------- /box_types_opus.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | /*************************** Opus ****************************/ 4 | 5 | // https://opus-codec.org/docs/opus_in_isobmff.html 6 | 7 | func BoxTypeOpus() BoxType { return StrToBoxType("Opus") } 8 | 9 | func init() { 10 | AddAnyTypeBoxDef(&AudioSampleEntry{}, BoxTypeOpus()) 11 | } 12 | 13 | /*************************** dOps ****************************/ 14 | 15 | // https://opus-codec.org/docs/opus_in_isobmff.html 16 | 17 | func BoxTypeDOps() BoxType { return StrToBoxType("dOps") } 18 | 19 | func init() { 20 | AddBoxDef(&DOps{}) 21 | } 22 | 23 | type DOps struct { 24 | Box 25 | Version uint8 `mp4:"0,size=8"` 26 | OutputChannelCount uint8 `mp4:"1,size=8"` 27 | PreSkip uint16 `mp4:"2,size=16"` 28 | InputSampleRate uint32 `mp4:"3,size=32"` 29 | OutputGain int16 `mp4:"4,size=16"` 30 | ChannelMappingFamily uint8 `mp4:"5,size=8"` 31 | StreamCount uint8 `mp4:"6,opt=dynamic,size=8"` 32 | CoupledCount uint8 `mp4:"7,opt=dynamic,size=8"` 33 | ChannelMapping []uint8 `mp4:"8,opt=dynamic,size=8,len=dynamic"` 34 | } 35 | 36 | func (DOps) GetType() BoxType { 37 | return BoxTypeDOps() 38 | } 39 | 40 | func (dops DOps) IsOptFieldEnabled(name string, ctx Context) bool { 41 | switch name { 42 | case "StreamCount", "CoupledCount", "ChannelMapping": 43 | return dops.ChannelMappingFamily != 0 44 | } 45 | return false 46 | } 47 | 48 | func (ops DOps) GetFieldLength(name string, ctx Context) uint { 49 | switch name { 50 | case "ChannelMapping": 51 | return uint(ops.OutputChannelCount) 52 | } 53 | return 0 54 | } 55 | -------------------------------------------------------------------------------- /box_types_av1.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | /*************************** av01 ****************************/ 4 | 5 | // https://aomediacodec.github.io/av1-isobmff 6 | 7 | func BoxTypeAv01() BoxType { return StrToBoxType("av01") } 8 | 9 | func init() { 10 | AddAnyTypeBoxDef(&VisualSampleEntry{}, BoxTypeAv01()) 11 | } 12 | 13 | /*************************** av1C ****************************/ 14 | 15 | // https://aomediacodec.github.io/av1-isobmff 16 | 17 | func BoxTypeAv1C() BoxType { return StrToBoxType("av1C") } 18 | 19 | func init() { 20 | AddBoxDef(&Av1C{}) 21 | } 22 | 23 | type Av1C struct { 24 | Box 25 | Marker uint8 `mp4:"0,size=1,const=1"` 26 | Version uint8 `mp4:"1,size=7,const=1"` 27 | SeqProfile uint8 `mp4:"2,size=3"` 28 | SeqLevelIdx0 uint8 `mp4:"3,size=5"` 29 | SeqTier0 uint8 `mp4:"4,size=1"` 30 | HighBitdepth uint8 `mp4:"5,size=1"` 31 | TwelveBit uint8 `mp4:"6,size=1"` 32 | Monochrome uint8 `mp4:"7,size=1"` 33 | ChromaSubsamplingX uint8 `mp4:"8,size=1"` 34 | ChromaSubsamplingY uint8 `mp4:"9,size=1"` 35 | ChromaSamplePosition uint8 `mp4:"10,size=2"` 36 | Reserved uint8 `mp4:"11,size=3,const=0"` 37 | InitialPresentationDelayPresent uint8 `mp4:"12,size=1"` 38 | InitialPresentationDelayMinusOne uint8 `mp4:"13,size=4"` 39 | ConfigOBUs []uint8 `mp4:"14,size=8"` 40 | } 41 | 42 | func (Av1C) GetType() BoxType { 43 | return BoxTypeAv1C() 44 | } 45 | -------------------------------------------------------------------------------- /box_types_vp.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | // https://www.webmproject.org/vp9/mp4/ 4 | 5 | /*************************** vp08 ****************************/ 6 | 7 | func BoxTypeVp08() BoxType { return StrToBoxType("vp08") } 8 | 9 | func init() { 10 | AddAnyTypeBoxDef(&VisualSampleEntry{}, BoxTypeVp08()) 11 | } 12 | 13 | /*************************** vp09 ****************************/ 14 | 15 | func BoxTypeVp09() BoxType { return StrToBoxType("vp09") } 16 | 17 | func init() { 18 | AddAnyTypeBoxDef(&VisualSampleEntry{}, BoxTypeVp09()) 19 | } 20 | 21 | /*************************** VpcC ****************************/ 22 | 23 | func BoxTypeVpcC() BoxType { return StrToBoxType("vpcC") } 24 | 25 | func init() { 26 | AddBoxDef(&VpcC{}) 27 | } 28 | 29 | type VpcC struct { 30 | FullBox `mp4:"0,extend"` 31 | Profile uint8 `mp4:"1,size=8"` 32 | Level uint8 `mp4:"2,size=8"` 33 | BitDepth uint8 `mp4:"3,size=4"` 34 | ChromaSubsampling uint8 `mp4:"4,size=3"` 35 | VideoFullRangeFlag uint8 `mp4:"5,size=1"` 36 | ColourPrimaries uint8 `mp4:"6,size=8"` 37 | TransferCharacteristics uint8 `mp4:"7,size=8"` 38 | MatrixCoefficients uint8 `mp4:"8,size=8"` 39 | CodecInitializationDataSize uint16 `mp4:"9,size=16"` 40 | CodecInitializationData []uint8 `mp4:"10,size=8,len=dynamic"` 41 | } 42 | 43 | func (VpcC) GetType() BoxType { 44 | return BoxTypeVpcC() 45 | } 46 | 47 | func (vpcc VpcC) GetFieldLength(name string, ctx Context) uint { 48 | switch name { 49 | case "CodecInitializationData": 50 | return uint(vpcc.CodecInitializationDataSize) 51 | } 52 | return 0 53 | } 54 | -------------------------------------------------------------------------------- /write.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | ) 7 | 8 | type Writer struct { 9 | writer io.WriteSeeker 10 | biStack []*BoxInfo 11 | } 12 | 13 | func NewWriter(w io.WriteSeeker) *Writer { 14 | return &Writer{ 15 | writer: w, 16 | } 17 | } 18 | 19 | func (w *Writer) Write(p []byte) (int, error) { 20 | return w.writer.Write(p) 21 | } 22 | 23 | func (w *Writer) Seek(offset int64, whence int) (int64, error) { 24 | return w.writer.Seek(offset, whence) 25 | } 26 | 27 | func (w *Writer) StartBox(bi *BoxInfo) (*BoxInfo, error) { 28 | bi, err := WriteBoxInfo(w.writer, bi) 29 | if err != nil { 30 | return nil, err 31 | } 32 | w.biStack = append(w.biStack, bi) 33 | return bi, nil 34 | } 35 | 36 | func (w *Writer) EndBox() (*BoxInfo, error) { 37 | bi := w.biStack[len(w.biStack)-1] 38 | w.biStack = w.biStack[:len(w.biStack)-1] 39 | end, err := w.writer.Seek(0, io.SeekCurrent) 40 | if err != nil { 41 | return nil, err 42 | } 43 | bi.Size = uint64(end) - bi.Offset 44 | if _, err = bi.SeekToStart(w.writer); err != nil { 45 | return nil, err 46 | } 47 | if bi2, err := WriteBoxInfo(w.writer, bi); err != nil { 48 | return nil, err 49 | } else if bi.HeaderSize != bi2.HeaderSize { 50 | return nil, errors.New("header size changed") 51 | } 52 | if _, err := w.writer.Seek(end, io.SeekStart); err != nil { 53 | return nil, err 54 | } 55 | return bi, nil 56 | } 57 | 58 | func (w *Writer) CopyBox(r io.ReadSeeker, bi *BoxInfo) error { 59 | if _, err := bi.SeekToStart(r); err != nil { 60 | return err 61 | } 62 | if n, err := io.CopyN(w, r, int64(bi.Size)); err != nil { 63 | return err 64 | } else if n != int64(bi.Size) { 65 | return errors.New("failed to copy box") 66 | } 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /box_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type mockBox struct { 11 | Type BoxType 12 | DynSizeMap map[string]uint 13 | DynLenMap map[string]uint 14 | DynOptMap map[string]bool 15 | IsPStringMap map[string]bool 16 | } 17 | 18 | func (m *mockBox) GetType() BoxType { 19 | return m.Type 20 | } 21 | 22 | func (m *mockBox) GetFieldSize(n string, ctx Context) uint { 23 | if s, ok := m.DynSizeMap[n]; !ok { 24 | panic(fmt.Errorf("invalid name of dynamic-size field: %s", n)) 25 | } else { 26 | return s 27 | } 28 | } 29 | 30 | func (m *mockBox) GetFieldLength(n string, ctx Context) uint { 31 | if l, ok := m.DynLenMap[n]; !ok { 32 | panic(fmt.Errorf("invalid name of dynamic-length field: %s", n)) 33 | } else { 34 | return l 35 | } 36 | } 37 | 38 | func (m *mockBox) IsOptFieldEnabled(n string, ctx Context) bool { 39 | if enabled, ok := m.DynOptMap[n]; !ok { 40 | panic(fmt.Errorf("invalid name of dynamic-opt field: %s", n)) 41 | } else { 42 | return enabled 43 | } 44 | } 45 | 46 | func (m *mockBox) IsPString(name string, bytes []byte, remainingSize uint64, ctx Context) bool { 47 | if b, ok := m.IsPStringMap[name]; ok { 48 | return b 49 | } 50 | return true 51 | } 52 | 53 | func TestFullBoxFlags(t *testing.T) { 54 | box := FullBox{} 55 | box.SetFlags(0x35ac68) 56 | assert.Equal(t, byte(0x35), box.Flags[0]) 57 | assert.Equal(t, byte(0xac), box.Flags[1]) 58 | assert.Equal(t, byte(0x68), box.Flags[2]) 59 | assert.Equal(t, uint32(0x35ac68), box.GetFlags()) 60 | 61 | box.AddFlag(0x030000) 62 | assert.Equal(t, uint32(0x37ac68), box.GetFlags()) 63 | 64 | box.RemoveFlag(0x000900) 65 | assert.Equal(t, uint32(0x37a468), box.GetFlags()) 66 | } 67 | -------------------------------------------------------------------------------- /cmd/mp4tool/internal/psshdump/psshdump_test.go: -------------------------------------------------------------------------------- 1 | package psshdump 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestPsshdump(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | file string 16 | options []string 17 | wants string 18 | }{ 19 | { 20 | name: "sample_init.encv.mp4", 21 | file: "../../../../testdata/sample_init.encv.mp4", 22 | wants: "0:\n" + 23 | " offset: 1307\n" + 24 | " size: 52\n" + 25 | " version: 1\n" + 26 | " flags: 0x000000\n" + 27 | " systemId: 1077efec-c0b2-4d02-ace3-3c1e52e2fb4b\n" + 28 | " dataSize: 0\n" + 29 | " base64: \"AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAEBI0VniavN7wEjRWeJq83vAAAAAA==\"\n" + 30 | "\n", 31 | }, 32 | { 33 | name: "sample_init.encv.mp4", 34 | file: "../../../../testdata/sample_init.enca.mp4", 35 | wants: "0:\n" + 36 | " offset: 1307\n" + 37 | " size: 52\n" + 38 | " version: 1\n" + 39 | " flags: 0x000000\n" + 40 | " systemId: 1077efec-c0b2-4d02-ace3-3c1e52e2fb4b\n" + 41 | " dataSize: 0\n" + 42 | " base64: \"AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAEBI0VniavN7wEjRWeJq83vAAAAAA==\"\n" + 43 | "\n", 44 | }, 45 | } 46 | for _, tc := range testCases { 47 | t.Run(tc.name, func(t *testing.T) { 48 | stdout := os.Stdout 49 | r, w, err := os.Pipe() 50 | require.NoError(t, err) 51 | defer func() { 52 | os.Stdout = stdout 53 | }() 54 | os.Stdout = w 55 | go func() { 56 | require.Zero(t, Main(append(tc.options, tc.file))) 57 | w.Close() 58 | }() 59 | b, err := io.ReadAll(r) 60 | require.NoError(t, err) 61 | assert.Equal(t, tc.wants, string(b)) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/util/string_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFormatSignedFixedFloat1616(t *testing.T) { 10 | assert.Equal(t, "1", FormatSignedFixedFloat1616(0x00010000)) 11 | assert.Equal(t, "1234", FormatSignedFixedFloat1616(0x04d20000)) 12 | assert.Equal(t, "-1234", FormatSignedFixedFloat1616(-0x04d20000)) 13 | assert.Equal(t, "1.50000", FormatSignedFixedFloat1616(0x00018000)) 14 | assert.Equal(t, "1234.56789", FormatSignedFixedFloat1616(0x04d29161)) 15 | assert.Equal(t, "-1234.56789", FormatSignedFixedFloat1616(-0x04d29161)) 16 | } 17 | 18 | func TestFormatUnsignedFixedFloat1616(t *testing.T) { 19 | assert.Equal(t, "1", FormatUnsignedFixedFloat1616(0x00010000)) 20 | assert.Equal(t, "1234", FormatUnsignedFixedFloat1616(0x04d20000)) 21 | assert.Equal(t, "1.50000", FormatUnsignedFixedFloat1616(0x00018000)) 22 | assert.Equal(t, "1234.56789", FormatUnsignedFixedFloat1616(0x04d29161)) 23 | assert.Equal(t, "65535.99998", FormatUnsignedFixedFloat1616(0xffffffff)) 24 | } 25 | 26 | func TestFormatSignedFixedFloat88(t *testing.T) { 27 | assert.Equal(t, "1", FormatSignedFixedFloat88(0x0100)) 28 | assert.Equal(t, "123", FormatSignedFixedFloat88(0x7b00)) 29 | assert.Equal(t, "-123", FormatSignedFixedFloat88(-0x7b00)) 30 | assert.Equal(t, "1.500", FormatSignedFixedFloat88(0x0180)) 31 | assert.Equal(t, "123.457", FormatSignedFixedFloat88(0x7b75)) 32 | assert.Equal(t, "-123.457", FormatSignedFixedFloat88(-0x7b75)) 33 | } 34 | 35 | func TestEscapeUnprintables(t *testing.T) { 36 | assert.Equal(t, ".ABC.あいう.", EscapeUnprintables(string([]byte{ 37 | 0x00, // NULL 38 | 0x41, 0x42, 0x43, // "ABC" 39 | 0x0a, // LF 40 | 0xe3, 0x81, 0x82, // "あ" 41 | 0xe3, 0x81, 0x84, // "い" 42 | 0xe3, 0x81, 0x86, // "う" 43 | 0x0d, // CR 44 | }))) 45 | } 46 | -------------------------------------------------------------------------------- /box_types_iso23001_5_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBoxTypesISO23001_5(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | src IImmutableBox 16 | dst IBox 17 | bin []byte 18 | str string 19 | ctx Context 20 | }{ 21 | { 22 | name: "pcmC", 23 | src: &PcmC{ 24 | FormatFlags: 1, 25 | PCMSampleSize: 32, 26 | }, 27 | dst: &PcmC{}, 28 | bin: []byte{0x0, 0x0, 0x0, 0x0, 0x1, 0x20}, 29 | str: `Version=0 Flags=0x000000 FormatFlags=0x1 PCMSampleSize=0x20`, 30 | }, 31 | } 32 | for _, tc := range testCases { 33 | t.Run(tc.name, func(t *testing.T) { 34 | // Marshal 35 | buf := bytes.NewBuffer(nil) 36 | n, err := Marshal(buf, tc.src, tc.ctx) 37 | require.NoError(t, err) 38 | assert.Equal(t, uint64(len(tc.bin)), n) 39 | assert.Equal(t, tc.bin, buf.Bytes()) 40 | 41 | // Unmarshal 42 | r := bytes.NewReader(tc.bin) 43 | n, err = Unmarshal(r, uint64(len(tc.bin)), tc.dst, tc.ctx) 44 | require.NoError(t, err) 45 | assert.Equal(t, uint64(buf.Len()), n) 46 | assert.Equal(t, tc.src, tc.dst) 47 | s, err := r.Seek(0, io.SeekCurrent) 48 | require.NoError(t, err) 49 | assert.Equal(t, int64(buf.Len()), s) 50 | 51 | // UnmarshalAny 52 | dst, n, err := UnmarshalAny(bytes.NewReader(tc.bin), tc.src.GetType(), uint64(len(tc.bin)), tc.ctx) 53 | require.NoError(t, err) 54 | assert.Equal(t, uint64(buf.Len()), n) 55 | assert.Equal(t, tc.src, dst) 56 | s, err = r.Seek(0, io.SeekCurrent) 57 | require.NoError(t, err) 58 | assert.Equal(t, int64(buf.Len()), s) 59 | 60 | // Stringify 61 | str, err := Stringify(tc.src, tc.ctx) 62 | require.NoError(t, err) 63 | assert.Equal(t, tc.str, str) 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cmd/mp4tool/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/abema/go-mp4/cmd/mp4tool/internal/divide" 8 | "github.com/abema/go-mp4/cmd/mp4tool/internal/dump" 9 | "github.com/abema/go-mp4/cmd/mp4tool/internal/edit" 10 | "github.com/abema/go-mp4/cmd/mp4tool/internal/extract" 11 | "github.com/abema/go-mp4/cmd/mp4tool/internal/probe" 12 | "github.com/abema/go-mp4/cmd/mp4tool/internal/psshdump" 13 | ) 14 | 15 | func main() { 16 | args := os.Args[1:] 17 | 18 | if len(args) == 0 { 19 | printUsage() 20 | os.Exit(1) 21 | } 22 | 23 | switch args[0] { 24 | case "help": 25 | printUsage() 26 | case "dump": 27 | os.Exit(dump.Main(args[1:])) 28 | case "psshdump": 29 | os.Exit(psshdump.Main(args[1:])) 30 | case "probe": 31 | os.Exit(probe.Main(args[1:])) 32 | case "extract": 33 | os.Exit(extract.Main(args[1:])) 34 | case "alpha": 35 | os.Exit(alpha(args[1:])) 36 | default: 37 | printUsage() 38 | os.Exit(1) 39 | } 40 | } 41 | 42 | func alpha(args []string) int { 43 | if len(args) < 1 { 44 | printUsage() 45 | return 1 46 | } 47 | 48 | switch args[0] { 49 | case "edit": 50 | return edit.Main(args[1:]) 51 | case "divide": 52 | return divide.Main(args[1:]) 53 | default: 54 | printUsage() 55 | return 1 56 | } 57 | } 58 | 59 | func printUsage() { 60 | fmt.Fprintf(os.Stderr, "USAGE: mp4tool COMMAND_NAME [ARGS]\n") 61 | fmt.Fprintln(os.Stderr) 62 | fmt.Fprintln(os.Stderr, "COMMAND_NAME:") 63 | fmt.Fprintln(os.Stderr, " dump : display box tree as human readable format") 64 | fmt.Fprintln(os.Stderr, " psshdump : display pssh box attributes") 65 | fmt.Fprintln(os.Stderr, " probe : probe and summarize mp4 file status") 66 | fmt.Fprintln(os.Stderr, " extract : extract specific box") 67 | fmt.Fprintln(os.Stderr, " alpha edit") 68 | fmt.Fprintln(os.Stderr, " alpha divide") 69 | } 70 | -------------------------------------------------------------------------------- /cmd/mp4tool/internal/psshdump/psshdump.go: -------------------------------------------------------------------------------- 1 | package psshdump 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | mp4 "github.com/abema/go-mp4" 10 | "github.com/sunfish-shogi/bufseekio" 11 | ) 12 | 13 | func Main(args []string) int { 14 | if len(args) < 1 { 15 | println("USAGE: mp4tool psshdump INPUT.mp4") 16 | return 1 17 | } 18 | 19 | if err := dump(args[0]); err != nil { 20 | fmt.Println("Error:", err) 21 | return 1 22 | } 23 | return 0 24 | } 25 | 26 | func dump(inputFilePath string) error { 27 | inputFile, err := os.Open(inputFilePath) 28 | if err != nil { 29 | return err 30 | } 31 | defer inputFile.Close() 32 | 33 | r := bufseekio.NewReadSeeker(inputFile, 1024, 4) 34 | 35 | bs, err := mp4.ExtractBoxesWithPayload(r, nil, []mp4.BoxPath{ 36 | {mp4.BoxTypeMoov(), mp4.BoxTypePssh()}, 37 | {mp4.BoxTypeMoof(), mp4.BoxTypePssh()}, 38 | }) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | for i := range bs { 44 | pssh := bs[i].Payload.(*mp4.Pssh) 45 | 46 | var sysid string 47 | for i, v := range pssh.SystemID { 48 | sysid += fmt.Sprintf("%02x", v) 49 | if i == 3 || i == 5 || i == 7 || i == 9 { 50 | sysid += "-" 51 | } 52 | } 53 | 54 | if _, err := bs[i].Info.SeekToStart(r); err != nil { 55 | return err 56 | } 57 | rawData := make([]byte, bs[i].Info.Size) 58 | if _, err := io.ReadFull(r, rawData); err != nil { 59 | return err 60 | } 61 | 62 | fmt.Printf("%d:\n", i) 63 | fmt.Printf(" offset: %d\n", bs[i].Info.Offset) 64 | fmt.Printf(" size: %d\n", bs[i].Info.Size) 65 | fmt.Printf(" version: %d\n", pssh.Version) 66 | fmt.Printf(" flags: 0x%x\n", pssh.Flags) 67 | fmt.Printf(" systemId: %s\n", sysid) 68 | fmt.Printf(" dataSize: %d\n", pssh.DataSize) 69 | fmt.Printf(" base64: \"%s\"\n", base64.StdEncoding.EncodeToString(rawData)) 70 | fmt.Println() 71 | } 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /cmd/mp4tool/internal/extract/extract_test.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestExtract(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | file string 16 | boxType string 17 | expectedSize int 18 | }{ 19 | { 20 | name: "sample.mp4/ftyp", 21 | file: "../../../../testdata/sample.mp4", 22 | boxType: "ftyp", 23 | expectedSize: 32, 24 | }, 25 | { 26 | name: "sample.mp4/mdhd", 27 | file: "../../../../testdata/sample.mp4", 28 | boxType: "mdhd", 29 | expectedSize: 64, // = 32 (1st trak) + 32 (2nd trak) 30 | }, 31 | { 32 | name: "sample_fragmented.mp4/trun", 33 | file: "../../../../testdata/sample_fragmented.mp4", 34 | boxType: "trun", 35 | expectedSize: 452, 36 | }, 37 | } 38 | for _, tc := range testCases { 39 | t.Run(tc.name, func(t *testing.T) { 40 | stdout := os.Stdout 41 | r, w, err := os.Pipe() 42 | require.NoError(t, err) 43 | defer func() { 44 | os.Stdout = stdout 45 | }() 46 | os.Stdout = w 47 | go func() { 48 | require.Zero(t, Main([]string{tc.boxType, tc.file})) 49 | w.Close() 50 | }() 51 | b, err := io.ReadAll(r) 52 | require.NoError(t, err) 53 | assert.Equal(t, tc.expectedSize, len(b)) 54 | assert.Equal(t, tc.boxType, string(b[4:8])) 55 | }) 56 | } 57 | } 58 | 59 | func TestValidation(t *testing.T) { 60 | // valid 61 | require.Zero(t, Main([]string{"xxxx", "../../../../testdata/sample.mp4"})) 62 | 63 | // invalid 64 | require.NotZero(t, Main([]string{})) 65 | require.NotZero(t, Main([]string{"xxxx"})) 66 | require.NotZero(t, Main([]string{"xxxxx", "../../../../testdata/sample.mp4"})) 67 | require.NotZero(t, Main([]string{"xxxx", "not_found.mp4"})) 68 | } 69 | -------------------------------------------------------------------------------- /box_types_etsi_ts_102_366_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBoxTypesETSI_TS_102_366(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | src IImmutableBox 16 | dst IBox 17 | bin []byte 18 | str string 19 | ctx Context 20 | }{ 21 | { 22 | name: "dac3", 23 | src: &Dac3{ 24 | Fscod: 0, 25 | Bsid: 8, 26 | Acmod: 7, 27 | LfeOn: 1, 28 | BitRateCode: 0x7, 29 | }, 30 | dst: &Dac3{}, 31 | bin: []byte{0x10, 0x3c, 0xe0}, 32 | str: `Fscod=0x0 Bsid=0x8 Bsmod=0x0 Acmod=0x7 LfeOn=0x1 BitRateCode=0x7`, 33 | }, 34 | } 35 | for _, tc := range testCases { 36 | t.Run(tc.name, func(t *testing.T) { 37 | // Marshal 38 | buf := bytes.NewBuffer(nil) 39 | n, err := Marshal(buf, tc.src, tc.ctx) 40 | require.NoError(t, err) 41 | assert.Equal(t, uint64(len(tc.bin)), n) 42 | assert.Equal(t, tc.bin, buf.Bytes()) 43 | 44 | // Unmarshal 45 | r := bytes.NewReader(tc.bin) 46 | n, err = Unmarshal(r, uint64(len(tc.bin)), tc.dst, tc.ctx) 47 | require.NoError(t, err) 48 | assert.Equal(t, uint64(buf.Len()), n) 49 | assert.Equal(t, tc.src, tc.dst) 50 | s, err := r.Seek(0, io.SeekCurrent) 51 | require.NoError(t, err) 52 | assert.Equal(t, int64(buf.Len()), s) 53 | 54 | // UnmarshalAny 55 | dst, n, err := UnmarshalAny(bytes.NewReader(tc.bin), tc.src.GetType(), uint64(len(tc.bin)), tc.ctx) 56 | require.NoError(t, err) 57 | assert.Equal(t, uint64(buf.Len()), n) 58 | assert.Equal(t, tc.src, dst) 59 | s, err = r.Seek(0, io.SeekCurrent) 60 | require.NoError(t, err) 61 | assert.Equal(t, int64(buf.Len()), s) 62 | 63 | // Stringify 64 | str, err := Stringify(tc.src, tc.ctx) 65 | require.NoError(t, err) 66 | assert.Equal(t, tc.str, str) 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /cmd/mp4tool/internal/extract/extract.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/abema/go-mp4" 10 | "github.com/abema/go-mp4/cmd/mp4tool/internal/util" 11 | "github.com/sunfish-shogi/bufseekio" 12 | ) 13 | 14 | const ( 15 | blockSize = 128 * 1024 16 | blockHistorySize = 4 17 | ) 18 | 19 | func Main(args []string) int { 20 | flagSet := flag.NewFlagSet("extract", flag.ExitOnError) 21 | flagSet.Usage = func() { 22 | println("USAGE: mp4tool extract [OPTIONS] BOX_TYPE INPUT.mp4") 23 | flagSet.PrintDefaults() 24 | } 25 | flagSet.Parse(args) 26 | 27 | if len(flagSet.Args()) < 2 { 28 | flagSet.Usage() 29 | return 1 30 | } 31 | 32 | boxType := flagSet.Args()[0] 33 | inputPath := flagSet.Args()[1] 34 | 35 | if len(boxType) != 4 { 36 | println("Error:", "invalid argument:", boxType) 37 | println("BOX_TYPE must be 4 characters.") 38 | return 1 39 | } 40 | 41 | input, err := os.Open(inputPath) 42 | if err != nil { 43 | fmt.Println("Error:", err) 44 | return 1 45 | } 46 | defer input.Close() 47 | 48 | r := bufseekio.NewReadSeeker(input, blockSize, blockHistorySize) 49 | if err := extract(r, mp4.StrToBoxType(boxType)); err != nil { 50 | fmt.Println("Error:", err) 51 | return 1 52 | } 53 | return 0 54 | } 55 | 56 | func extract(r io.ReadSeeker, boxType mp4.BoxType) error { 57 | _, err := mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) { 58 | if h.BoxInfo.Type == boxType { 59 | h.BoxInfo.SeekToStart(r) 60 | if _, err := io.CopyN(os.Stdout, r, int64(h.BoxInfo.Size)); err != nil { 61 | return nil, err 62 | } 63 | } 64 | if !h.BoxInfo.IsSupportedType() { 65 | return nil, nil 66 | } 67 | if h.BoxInfo.Size >= 256 && util.ShouldHasNoChildren(h.BoxInfo.Type) { 68 | return nil, nil 69 | } 70 | _, err := h.Expand() 71 | if err == mp4.ErrUnsupportedBoxVersion { 72 | return nil, nil 73 | } 74 | return nil, err 75 | }) 76 | return err 77 | } 78 | -------------------------------------------------------------------------------- /box_types_3gpp_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBoxTypes3GPP(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | src IImmutableBox 16 | dst IBox 17 | bin []byte 18 | str string 19 | ctx Context 20 | }{ 21 | { 22 | name: "udta 3gpp string", 23 | src: &Udta3GppString{ 24 | AnyTypeBox: AnyTypeBox{Type: StrToBoxType("titl")}, 25 | Language: [3]byte{0x5, 0xe, 0x7}, 26 | Data: []byte("SING"), 27 | }, 28 | dst: &Udta3GppString{ 29 | AnyTypeBox: AnyTypeBox{Type: StrToBoxType("titl")}, 30 | }, 31 | bin: []byte{ 32 | 0, // version 33 | 0x00, 0x00, 0x00, // flags 34 | 0x15, 0xc7, // language 35 | 0x53, 0x49, 0x4e, 0x47, // data 36 | }, 37 | str: `Version=0 Flags=0x000000 Language="eng" Data="SING"`, 38 | ctx: Context{UnderUdta: true}, 39 | }, 40 | } 41 | for _, tc := range testCases { 42 | t.Run(tc.name, func(t *testing.T) { 43 | // Marshal 44 | buf := bytes.NewBuffer(nil) 45 | n, err := Marshal(buf, tc.src, tc.ctx) 46 | require.NoError(t, err) 47 | assert.Equal(t, uint64(len(tc.bin)), n) 48 | assert.Equal(t, tc.bin, buf.Bytes()) 49 | 50 | // Unmarshal 51 | r := bytes.NewReader(tc.bin) 52 | n, err = Unmarshal(r, uint64(len(tc.bin)), tc.dst, tc.ctx) 53 | require.NoError(t, err) 54 | assert.Equal(t, uint64(buf.Len()), n) 55 | assert.Equal(t, tc.src, tc.dst) 56 | s, err := r.Seek(0, io.SeekCurrent) 57 | require.NoError(t, err) 58 | assert.Equal(t, int64(buf.Len()), s) 59 | 60 | // UnmarshalAny 61 | dst, n, err := UnmarshalAny(bytes.NewReader(tc.bin), tc.src.GetType(), uint64(len(tc.bin)), tc.ctx) 62 | require.NoError(t, err) 63 | assert.Equal(t, uint64(buf.Len()), n) 64 | assert.Equal(t, tc.src, dst) 65 | s, err = r.Seek(0, io.SeekCurrent) 66 | require.NoError(t, err) 67 | assert.Equal(t, int64(buf.Len()), s) 68 | 69 | // Stringify 70 | str, err := Stringify(tc.src, tc.ctx) 71 | require.NoError(t, err) 72 | assert.Equal(t, tc.str, str) 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/bitio/read.go: -------------------------------------------------------------------------------- 1 | package bitio 2 | 3 | import "io" 4 | 5 | type Reader interface { 6 | io.Reader 7 | 8 | // alignment: 9 | // |-1-byte-block-|--------------|--------------|--------------| 10 | // |<-offset->|<-------------------width---------------------->| 11 | ReadBits(width uint) (data []byte, err error) 12 | 13 | ReadBit() (bit bool, err error) 14 | } 15 | 16 | type ReadSeeker interface { 17 | Reader 18 | io.Seeker 19 | } 20 | 21 | type reader struct { 22 | reader io.Reader 23 | octet byte 24 | width uint 25 | } 26 | 27 | func NewReader(r io.Reader) Reader { 28 | return &reader{reader: r} 29 | } 30 | 31 | func (r *reader) Read(p []byte) (n int, err error) { 32 | if r.width != 0 { 33 | return 0, ErrInvalidAlignment 34 | } 35 | return r.reader.Read(p) 36 | } 37 | 38 | func (r *reader) ReadBits(size uint) ([]byte, error) { 39 | bytes := (size + 7) / 8 40 | data := make([]byte, bytes) 41 | offset := (bytes * 8) - (size) 42 | 43 | for i := uint(0); i < size; i++ { 44 | bit, err := r.ReadBit() 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | byteIdx := (offset + i) / 8 50 | bitIdx := 7 - (offset+i)%8 51 | if bit { 52 | data[byteIdx] |= 0x1 << bitIdx 53 | } 54 | } 55 | 56 | return data, nil 57 | } 58 | 59 | func (r *reader) ReadBit() (bool, error) { 60 | if r.width == 0 { 61 | buf := make([]byte, 1) 62 | if n, err := r.reader.Read(buf); err != nil { 63 | return false, err 64 | } else if n != 1 { 65 | return false, ErrDiscouragedReader 66 | } 67 | r.octet = buf[0] 68 | r.width = 8 69 | } 70 | 71 | r.width-- 72 | return (r.octet>>r.width)&0x01 != 0, nil 73 | } 74 | 75 | type readSeeker struct { 76 | reader 77 | seeker io.Seeker 78 | } 79 | 80 | func NewReadSeeker(r io.ReadSeeker) ReadSeeker { 81 | return &readSeeker{ 82 | reader: reader{reader: r}, 83 | seeker: r, 84 | } 85 | } 86 | 87 | func (r *readSeeker) Seek(offset int64, whence int) (int64, error) { 88 | if whence == io.SeekCurrent && r.reader.width != 0 { 89 | return 0, ErrInvalidAlignment 90 | } 91 | n, err := r.seeker.Seek(offset, whence) 92 | if err != nil { 93 | return n, err 94 | } 95 | r.reader.width = 0 96 | return n, nil 97 | } 98 | -------------------------------------------------------------------------------- /box_types_opus_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBoxTypesOpus(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | src IImmutableBox 16 | dst IBox 17 | bin []byte 18 | str string 19 | ctx Context 20 | }{ 21 | { 22 | name: "dOps", 23 | src: &DOps{ 24 | OutputChannelCount: 2, 25 | PreSkip: 312, 26 | InputSampleRate: 48000, 27 | OutputGain: 0, 28 | ChannelMappingFamily: 2, 29 | StreamCount: 1, 30 | CoupledCount: 1, 31 | ChannelMapping: []uint8{1, 2}, 32 | }, 33 | dst: &DOps{}, 34 | bin: []byte{ 35 | 0x00, 0x02, 0x01, 0x38, 0x00, 0x00, 0xbb, 0x80, 36 | 0x00, 0x00, 0x02, 0x01, 0x01, 0x01, 0x02, 37 | }, 38 | str: `Version=0 OutputChannelCount=0x2 PreSkip=312 InputSampleRate=48000 OutputGain=0 ChannelMappingFamily=0x2 StreamCount=0x1 CoupledCount=0x1 ChannelMapping=[0x1, 0x2]`, 39 | }, 40 | } 41 | for _, tc := range testCases { 42 | t.Run(tc.name, func(t *testing.T) { 43 | // Marshal 44 | buf := bytes.NewBuffer(nil) 45 | n, err := Marshal(buf, tc.src, tc.ctx) 46 | require.NoError(t, err) 47 | assert.Equal(t, uint64(len(tc.bin)), n) 48 | assert.Equal(t, tc.bin, buf.Bytes()) 49 | 50 | // Unmarshal 51 | r := bytes.NewReader(tc.bin) 52 | n, err = Unmarshal(r, uint64(len(tc.bin)), tc.dst, tc.ctx) 53 | require.NoError(t, err) 54 | assert.Equal(t, uint64(buf.Len()), n) 55 | assert.Equal(t, tc.src, tc.dst) 56 | s, err := r.Seek(0, io.SeekCurrent) 57 | require.NoError(t, err) 58 | assert.Equal(t, int64(buf.Len()), s) 59 | 60 | // UnmarshalAny 61 | dst, n, err := UnmarshalAny(bytes.NewReader(tc.bin), tc.src.GetType(), uint64(len(tc.bin)), tc.ctx) 62 | require.NoError(t, err) 63 | assert.Equal(t, uint64(buf.Len()), n) 64 | assert.Equal(t, tc.src, dst) 65 | s, err = r.Seek(0, io.SeekCurrent) 66 | require.NoError(t, err) 67 | assert.Equal(t, int64(buf.Len()), s) 68 | 69 | // Stringify 70 | str, err := Stringify(tc.src, tc.ctx) 71 | require.NoError(t, err) 72 | assert.Equal(t, tc.str, str) 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /extract.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | ) 7 | 8 | type BoxInfoWithPayload struct { 9 | Info BoxInfo 10 | Payload IBox 11 | } 12 | 13 | func ExtractBoxWithPayload(r io.ReadSeeker, parent *BoxInfo, path BoxPath) ([]*BoxInfoWithPayload, error) { 14 | return ExtractBoxesWithPayload(r, parent, []BoxPath{path}) 15 | } 16 | 17 | func ExtractBoxesWithPayload(r io.ReadSeeker, parent *BoxInfo, paths []BoxPath) ([]*BoxInfoWithPayload, error) { 18 | bis, err := ExtractBoxes(r, parent, paths) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | bs := make([]*BoxInfoWithPayload, 0, len(bis)) 24 | for _, bi := range bis { 25 | if _, err := bi.SeekToPayload(r); err != nil { 26 | return nil, err 27 | } 28 | 29 | var ctx Context 30 | if parent != nil { 31 | ctx = parent.Context 32 | } 33 | box, _, err := UnmarshalAny(r, bi.Type, bi.Size-bi.HeaderSize, ctx) 34 | if err != nil { 35 | return nil, err 36 | } 37 | bs = append(bs, &BoxInfoWithPayload{ 38 | Info: *bi, 39 | Payload: box, 40 | }) 41 | } 42 | return bs, nil 43 | } 44 | 45 | func ExtractBox(r io.ReadSeeker, parent *BoxInfo, path BoxPath) ([]*BoxInfo, error) { 46 | return ExtractBoxes(r, parent, []BoxPath{path}) 47 | } 48 | 49 | func ExtractBoxes(r io.ReadSeeker, parent *BoxInfo, paths []BoxPath) ([]*BoxInfo, error) { 50 | if len(paths) == 0 { 51 | return nil, nil 52 | } 53 | 54 | for i := range paths { 55 | if len(paths[i]) == 0 { 56 | return nil, errors.New("box path must not be empty") 57 | } 58 | } 59 | 60 | boxes := make([]*BoxInfo, 0, 8) 61 | 62 | handler := func(handle *ReadHandle) (interface{}, error) { 63 | path := handle.Path 64 | if parent != nil { 65 | path = path[1:] 66 | } 67 | if handle.BoxInfo.Type == BoxTypeAny() { 68 | return nil, nil 69 | } 70 | fm, m := matchPath(paths, path) 71 | if m { 72 | boxes = append(boxes, &handle.BoxInfo) 73 | } 74 | 75 | if fm { 76 | if _, err := handle.Expand(); err != nil { 77 | return nil, err 78 | } 79 | } 80 | return nil, nil 81 | } 82 | 83 | if parent != nil { 84 | _, err := ReadBoxStructureFromInternal(r, parent, handler) 85 | return boxes, err 86 | } 87 | _, err := ReadBoxStructure(r, handler) 88 | return boxes, err 89 | } 90 | 91 | func matchPath(paths []BoxPath, path BoxPath) (forwardMatch bool, match bool) { 92 | for i := range paths { 93 | fm, m := path.compareWith(paths[i]) 94 | forwardMatch = forwardMatch || fm 95 | match = match || m 96 | } 97 | return 98 | } 99 | -------------------------------------------------------------------------------- /box_types_vp_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBoxTypesVp(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | src IImmutableBox 16 | dst IBox 17 | bin []byte 18 | str string 19 | ctx Context 20 | }{ 21 | { 22 | name: "vpcC", 23 | src: &VpcC{ 24 | FullBox: FullBox{ 25 | Version: 1, 26 | }, 27 | Profile: 1, 28 | Level: 50, 29 | BitDepth: 10, 30 | ChromaSubsampling: 3, 31 | VideoFullRangeFlag: 1, 32 | ColourPrimaries: 0, 33 | TransferCharacteristics: 1, 34 | MatrixCoefficients: 10, 35 | CodecInitializationDataSize: 3, 36 | CodecInitializationData: []byte{5, 4, 3}, 37 | }, 38 | dst: &VpcC{}, 39 | bin: []byte{ 40 | 0x01, 0x00, 0x00, 0x00, 0x01, 0x32, 0xa7, 0x00, 41 | 0x01, 0x0a, 0x00, 0x03, 0x05, 0x04, 0x03, 42 | }, 43 | str: `Version=1 Flags=0x000000 Profile=0x1 Level=0x32 BitDepth=0xa ChromaSubsampling=0x3 VideoFullRangeFlag=0x1 ColourPrimaries=0x0 TransferCharacteristics=0x1 MatrixCoefficients=0xa CodecInitializationDataSize=3 CodecInitializationData=[0x5, 0x4, 0x3]`, 44 | }, 45 | } 46 | for _, tc := range testCases { 47 | t.Run(tc.name, func(t *testing.T) { 48 | // Marshal 49 | buf := bytes.NewBuffer(nil) 50 | n, err := Marshal(buf, tc.src, tc.ctx) 51 | require.NoError(t, err) 52 | assert.Equal(t, uint64(len(tc.bin)), n) 53 | assert.Equal(t, tc.bin, buf.Bytes()) 54 | 55 | // Unmarshal 56 | r := bytes.NewReader(tc.bin) 57 | n, err = Unmarshal(r, uint64(len(tc.bin)), tc.dst, tc.ctx) 58 | require.NoError(t, err) 59 | assert.Equal(t, uint64(buf.Len()), n) 60 | assert.Equal(t, tc.src, tc.dst) 61 | s, err := r.Seek(0, io.SeekCurrent) 62 | require.NoError(t, err) 63 | assert.Equal(t, int64(buf.Len()), s) 64 | 65 | // UnmarshalAny 66 | dst, n, err := UnmarshalAny(bytes.NewReader(tc.bin), tc.src.GetType(), uint64(len(tc.bin)), tc.ctx) 67 | require.NoError(t, err) 68 | assert.Equal(t, uint64(buf.Len()), n) 69 | assert.Equal(t, tc.src, dst) 70 | s, err = r.Seek(0, io.SeekCurrent) 71 | require.NoError(t, err) 72 | assert.Equal(t, int64(buf.Len()), s) 73 | 74 | // Stringify 75 | str, err := Stringify(tc.src, tc.ctx) 76 | require.NoError(t, err) 77 | assert.Equal(t, tc.str, str) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /box_types_av1_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBoxTypesAV1(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | src IImmutableBox 16 | dst IBox 17 | bin []byte 18 | str string 19 | ctx Context 20 | }{ 21 | { 22 | name: "Av1C", 23 | src: &Av1C{ 24 | Marker: 1, 25 | Version: 1, 26 | SeqProfile: 2, 27 | SeqLevelIdx0: 1, 28 | SeqTier0: 1, 29 | HighBitdepth: 1, 30 | TwelveBit: 0, 31 | Monochrome: 0, 32 | ChromaSubsamplingX: 1, 33 | ChromaSubsamplingY: 1, 34 | ChromaSamplePosition: 0, 35 | ConfigOBUs: []byte{ 36 | 0x08, 0x00, 0x00, 0x00, 0x42, 0xa7, 0xbf, 0xe4, 37 | 0x60, 0x0d, 0x00, 0x40, 38 | }, 39 | }, 40 | dst: &Av1C{}, 41 | bin: []byte{ 42 | 0x81, 0x41, 0xcc, 0x00, 0x08, 0x00, 0x00, 0x00, 43 | 0x42, 0xa7, 0xbf, 0xe4, 0x60, 0x0d, 0x00, 0x40, 44 | }, 45 | str: `SeqProfile=0x2 SeqLevelIdx0=0x1 SeqTier0=0x1 HighBitdepth=0x1 TwelveBit=0x0 Monochrome=0x0 ChromaSubsamplingX=0x1 ChromaSubsamplingY=0x1 ChromaSamplePosition=0x0 InitialPresentationDelayPresent=0x0 InitialPresentationDelayMinusOne=0x0 ConfigOBUs=[0x8, 0x0, 0x0, 0x0, 0x42, 0xa7, 0xbf, 0xe4, 0x60, 0xd, 0x0, 0x40]`, 46 | }, 47 | } 48 | for _, tc := range testCases { 49 | t.Run(tc.name, func(t *testing.T) { 50 | // Marshal 51 | buf := bytes.NewBuffer(nil) 52 | n, err := Marshal(buf, tc.src, tc.ctx) 53 | require.NoError(t, err) 54 | assert.Equal(t, uint64(len(tc.bin)), n) 55 | assert.Equal(t, tc.bin, buf.Bytes()) 56 | 57 | // Unmarshal 58 | r := bytes.NewReader(tc.bin) 59 | n, err = Unmarshal(r, uint64(len(tc.bin)), tc.dst, tc.ctx) 60 | require.NoError(t, err) 61 | assert.Equal(t, uint64(buf.Len()), n) 62 | assert.Equal(t, tc.src, tc.dst) 63 | s, err := r.Seek(0, io.SeekCurrent) 64 | require.NoError(t, err) 65 | assert.Equal(t, int64(buf.Len()), s) 66 | 67 | // UnmarshalAny 68 | dst, n, err := UnmarshalAny(bytes.NewReader(tc.bin), tc.src.GetType(), uint64(len(tc.bin)), tc.ctx) 69 | require.NoError(t, err) 70 | assert.Equal(t, uint64(buf.Len()), n) 71 | assert.Equal(t, tc.src, dst) 72 | s, err = r.Seek(0, io.SeekCurrent) 73 | require.NoError(t, err) 74 | assert.Equal(t, int64(buf.Len()), s) 75 | 76 | // Stringify 77 | str, err := Stringify(tc.src, tc.ctx) 78 | require.NoError(t, err) 79 | assert.Equal(t, tc.str, str) 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 6 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 7 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 8 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 9 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 10 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 11 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw= 14 | github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 19 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 20 | github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365 h1:uy0xSeqOW3InVwbgqDkE00h2InGiJ1jp5BLu4k0ax8o= 21 | github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I= 22 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= 24 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 26 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 29 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 30 | gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= 31 | gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= 32 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 33 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 34 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 35 | -------------------------------------------------------------------------------- /box_types_iso14496_30.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | /*********************** WebVTT Sample Entry ****************************/ 4 | 5 | func BoxTypeVttC() BoxType { return StrToBoxType("vttC") } 6 | func BoxTypeVlab() BoxType { return StrToBoxType("vlab") } 7 | func BoxTypeWvtt() BoxType { return StrToBoxType("wvtt") } 8 | 9 | func init() { 10 | AddBoxDef(&WebVTTConfigurationBox{}) 11 | AddBoxDef(&WebVTTSourceLabelBox{}) 12 | AddAnyTypeBoxDef(&WVTTSampleEntry{}, BoxTypeWvtt()) 13 | } 14 | 15 | type WebVTTConfigurationBox struct { 16 | Box 17 | Config string `mp4:"0,boxstring"` 18 | } 19 | 20 | func (WebVTTConfigurationBox) GetType() BoxType { 21 | return BoxTypeVttC() 22 | } 23 | 24 | type WebVTTSourceLabelBox struct { 25 | Box 26 | SourceLabel string `mp4:"0,boxstring"` 27 | } 28 | 29 | func (WebVTTSourceLabelBox) GetType() BoxType { 30 | return BoxTypeVlab() 31 | } 32 | 33 | type WVTTSampleEntry struct { 34 | SampleEntry `mp4:"0,extend"` 35 | } 36 | 37 | /*********************** WebVTT Sample Format ****************************/ 38 | 39 | func BoxTypeVttc() BoxType { return StrToBoxType("vttc") } 40 | func BoxTypeVsid() BoxType { return StrToBoxType("vsid") } 41 | func BoxTypeCtim() BoxType { return StrToBoxType("ctim") } 42 | func BoxTypeIden() BoxType { return StrToBoxType("iden") } 43 | func BoxTypeSttg() BoxType { return StrToBoxType("sttg") } 44 | func BoxTypePayl() BoxType { return StrToBoxType("payl") } 45 | func BoxTypeVtte() BoxType { return StrToBoxType("vtte") } 46 | func BoxTypeVtta() BoxType { return StrToBoxType("vtta") } 47 | 48 | func init() { 49 | AddBoxDef(&VTTCueBox{}) 50 | AddBoxDef(&CueSourceIDBox{}) 51 | AddBoxDef(&CueTimeBox{}) 52 | AddBoxDef(&CueIDBox{}) 53 | AddBoxDef(&CueSettingsBox{}) 54 | AddBoxDef(&CuePayloadBox{}) 55 | AddBoxDef(&VTTEmptyCueBox{}) 56 | AddBoxDef(&VTTAdditionalTextBox{}) 57 | } 58 | 59 | type VTTCueBox struct { 60 | Box 61 | } 62 | 63 | func (VTTCueBox) GetType() BoxType { 64 | return BoxTypeVttc() 65 | } 66 | 67 | type CueSourceIDBox struct { 68 | Box 69 | SourceId uint32 `mp4:"0,size=32"` 70 | } 71 | 72 | func (CueSourceIDBox) GetType() BoxType { 73 | return BoxTypeVsid() 74 | } 75 | 76 | type CueTimeBox struct { 77 | Box 78 | CueCurrentTime string `mp4:"0,boxstring"` 79 | } 80 | 81 | func (CueTimeBox) GetType() BoxType { 82 | return BoxTypeCtim() 83 | } 84 | 85 | type CueIDBox struct { 86 | Box 87 | CueId string `mp4:"0,boxstring"` 88 | } 89 | 90 | func (CueIDBox) GetType() BoxType { 91 | return BoxTypeIden() 92 | } 93 | 94 | type CueSettingsBox struct { 95 | Box 96 | Settings string `mp4:"0,boxstring"` 97 | } 98 | 99 | func (CueSettingsBox) GetType() BoxType { 100 | return BoxTypeSttg() 101 | } 102 | 103 | type CuePayloadBox struct { 104 | Box 105 | CueText string `mp4:"0,boxstring"` 106 | } 107 | 108 | func (CuePayloadBox) GetType() BoxType { 109 | return BoxTypePayl() 110 | } 111 | 112 | type VTTEmptyCueBox struct { 113 | Box 114 | } 115 | 116 | func (VTTEmptyCueBox) GetType() BoxType { 117 | return BoxTypeVtte() 118 | } 119 | 120 | type VTTAdditionalTextBox struct { 121 | Box 122 | CueAdditionalText string `mp4:"0,boxstring"` 123 | } 124 | 125 | func (VTTAdditionalTextBox) GetType() BoxType { 126 | return BoxTypeVtta() 127 | } 128 | -------------------------------------------------------------------------------- /cmd/mp4tool/internal/edit/edit.go: -------------------------------------------------------------------------------- 1 | package edit 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "math" 7 | "os" 8 | "strings" 9 | 10 | "github.com/abema/go-mp4" 11 | "github.com/sunfish-shogi/bufseekio" 12 | ) 13 | 14 | const UNoValue = math.MaxUint64 15 | 16 | type Values struct { 17 | BaseMediaDecodeTime uint64 18 | } 19 | 20 | type Boxes []string 21 | 22 | func (b Boxes) Exists(boxType string) bool { 23 | for _, t := range b { 24 | if t == boxType { 25 | return true 26 | } 27 | } 28 | return false 29 | } 30 | 31 | type Config struct { 32 | values Values 33 | dropBoxes Boxes 34 | } 35 | 36 | var config Config 37 | 38 | func Main(args []string) int { 39 | flagSet := flag.NewFlagSet("edit", flag.ExitOnError) 40 | flagSet.Uint64Var(&config.values.BaseMediaDecodeTime, "base_media_decode_time", UNoValue, "set new value to base_media_decode_time") 41 | dropBoxes := flagSet.String("drop", "", "drop boxes") 42 | flagSet.Usage = func() { 43 | println("USAGE: mp4tool edit [OPTIONS] INPUT.mp4 OUTPUT.mp4") 44 | flagSet.PrintDefaults() 45 | } 46 | flagSet.Parse(args) 47 | 48 | if len(flagSet.Args()) < 2 { 49 | flagSet.Usage() 50 | return 1 51 | } 52 | 53 | config.dropBoxes = strings.Split(*dropBoxes, ",") 54 | 55 | inputPath := flagSet.Args()[0] 56 | outputPath := flagSet.Args()[1] 57 | 58 | err := editFile(inputPath, outputPath) 59 | if err != nil { 60 | fmt.Println("Error:", err) 61 | return 1 62 | } 63 | return 0 64 | } 65 | 66 | func editFile(inputPath, outputPath string) error { 67 | inputFile, err := os.Open(inputPath) 68 | if err != nil { 69 | return err 70 | } 71 | defer inputFile.Close() 72 | 73 | outputFile, err := os.Create(outputPath) 74 | if err != nil { 75 | return err 76 | } 77 | defer outputFile.Close() 78 | 79 | r := bufseekio.NewReadSeeker(inputFile, 128*1024, 4) 80 | w := mp4.NewWriter(outputFile) 81 | _, err = mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) { 82 | if config.dropBoxes.Exists(h.BoxInfo.Type.String()) { 83 | // drop 84 | return uint64(0), nil 85 | } 86 | 87 | if !h.BoxInfo.IsSupportedType() || h.BoxInfo.Type == mp4.BoxTypeMdat() { 88 | // copy all data 89 | return nil, w.CopyBox(r, &h.BoxInfo) 90 | } 91 | 92 | // write header 93 | _, err := w.StartBox(&h.BoxInfo) 94 | if err != nil { 95 | return nil, err 96 | } 97 | // read payload 98 | box, _, err := h.ReadPayload() 99 | if err != nil { 100 | return nil, err 101 | } 102 | // edit some fields 103 | switch h.BoxInfo.Type { 104 | case mp4.BoxTypeTfdt(): 105 | tfdt := box.(*mp4.Tfdt) 106 | if config.values.BaseMediaDecodeTime != UNoValue { 107 | if tfdt.GetVersion() == 0 { 108 | tfdt.BaseMediaDecodeTimeV0 = uint32(config.values.BaseMediaDecodeTime) 109 | } else { 110 | tfdt.BaseMediaDecodeTimeV1 = config.values.BaseMediaDecodeTime 111 | } 112 | } 113 | } 114 | // write payload 115 | if _, err := mp4.Marshal(w, box, h.BoxInfo.Context); err != nil { 116 | return nil, err 117 | } 118 | // expand all of offsprings 119 | if _, err := h.Expand(); err != nil { 120 | return nil, err 121 | } 122 | // rewrite box size 123 | _, err = w.EndBox() 124 | return nil, err 125 | }) 126 | return err 127 | } 128 | -------------------------------------------------------------------------------- /box_types_iso23001_7.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | /*************************** pssh ****************************/ 11 | 12 | func BoxTypePssh() BoxType { return StrToBoxType("pssh") } 13 | 14 | func init() { 15 | AddBoxDef(&Pssh{}, 0, 1) 16 | } 17 | 18 | // Pssh is ISOBMFF pssh box type 19 | type Pssh struct { 20 | FullBox `mp4:"0,extend"` 21 | SystemID [16]byte `mp4:"1,size=8,uuid"` 22 | KIDCount uint32 `mp4:"2,size=32,nver=0"` 23 | KIDs []PsshKID `mp4:"3,nver=0,len=dynamic,size=128"` 24 | DataSize int32 `mp4:"4,size=32"` 25 | Data []byte `mp4:"5,size=8,len=dynamic"` 26 | } 27 | 28 | type PsshKID struct { 29 | KID [16]byte `mp4:"0,size=8,uuid"` 30 | } 31 | 32 | // GetFieldLength returns length of dynamic field 33 | func (pssh *Pssh) GetFieldLength(name string, ctx Context) uint { 34 | switch name { 35 | case "KIDs": 36 | return uint(pssh.KIDCount) 37 | case "Data": 38 | return uint(pssh.DataSize) 39 | } 40 | panic(fmt.Errorf("invalid name of dynamic-length field: boxType=pssh fieldName=%s", name)) 41 | } 42 | 43 | // StringifyField returns field value as string 44 | func (pssh *Pssh) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) { 45 | switch name { 46 | case "KIDs": 47 | buf := bytes.NewBuffer(nil) 48 | buf.WriteString("[") 49 | for i, e := range pssh.KIDs { 50 | if i != 0 { 51 | buf.WriteString(", ") 52 | } 53 | buf.WriteString(uuid.UUID(e.KID).String()) 54 | } 55 | buf.WriteString("]") 56 | return buf.String(), true 57 | 58 | default: 59 | return "", false 60 | } 61 | } 62 | 63 | // GetType returns the BoxType 64 | func (*Pssh) GetType() BoxType { 65 | return BoxTypePssh() 66 | } 67 | 68 | /*************************** tenc ****************************/ 69 | 70 | func BoxTypeTenc() BoxType { return StrToBoxType("tenc") } 71 | 72 | func init() { 73 | AddBoxDef(&Tenc{}, 0, 1) 74 | } 75 | 76 | // Tenc is ISOBMFF tenc box type 77 | type Tenc struct { 78 | FullBox `mp4:"0,extend"` 79 | Reserved uint8 `mp4:"1,size=8,dec"` 80 | DefaultCryptByteBlock uint8 `mp4:"2,size=4,dec"` // always 0 on version 0 81 | DefaultSkipByteBlock uint8 `mp4:"3,size=4,dec"` // always 0 on version 0 82 | DefaultIsProtected uint8 `mp4:"4,size=8,dec"` 83 | DefaultPerSampleIVSize uint8 `mp4:"5,size=8,dec"` 84 | DefaultKID [16]byte `mp4:"6,size=8,uuid"` 85 | DefaultConstantIVSize uint8 `mp4:"7,size=8,opt=dynamic,dec"` 86 | DefaultConstantIV []byte `mp4:"8,size=8,opt=dynamic,len=dynamic"` 87 | } 88 | 89 | func (tenc *Tenc) IsOptFieldEnabled(name string, ctx Context) bool { 90 | switch name { 91 | case "DefaultConstantIVSize", "DefaultConstantIV": 92 | return tenc.DefaultIsProtected == 1 && tenc.DefaultPerSampleIVSize == 0 93 | } 94 | return false 95 | } 96 | 97 | func (tenc *Tenc) GetFieldLength(name string, ctx Context) uint { 98 | switch name { 99 | case "DefaultConstantIV": 100 | return uint(tenc.DefaultConstantIVSize) 101 | } 102 | panic(fmt.Errorf("invalid name of dynamic-length field: boxType=tenc fieldName=%s", name)) 103 | } 104 | 105 | // GetType returns the BoxType 106 | func (*Tenc) GetType() BoxType { 107 | return BoxTypeTenc() 108 | } 109 | -------------------------------------------------------------------------------- /string_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestStringify(t *testing.T) { 11 | type inner struct { 12 | Uint64 uint64 `mp4:"0,size=64,hex"` 13 | } 14 | 15 | type testBox struct { 16 | AnyTypeBox 17 | FullBox `mp4:"0,extend"` 18 | String string `mp4:"1,string"` 19 | Int32 int32 `mp4:"2,size=32"` 20 | Int32Hex int32 `mp4:"3,size=32,hex"` 21 | Int32HexMinus int32 `mp4:"4,size=32,hex"` 22 | Uint32 uint32 `mp4:"5,size=32"` 23 | Bytes []byte `mp4:"6,size=8,string"` 24 | Ptr *inner `mp4:"7"` 25 | PtrEx *inner `mp4:"8,extend"` 26 | Struct inner `mp4:"9"` 27 | StructEx inner `mp4:"10,extend"` 28 | Array [7]byte `mp4:"11,size=8,string"` 29 | Bool bool `mp4:"12,size=1"` 30 | UUID [16]byte `mp4:"13,size=8,uuid"` 31 | NotSorted15 uint8 `mp4:"15,size=8,dec"` 32 | NotSorted16 uint8 `mp4:"16,size=8,dec"` 33 | NotSorted14 uint8 `mp4:"14,size=8,dec"` 34 | } 35 | boxType := StrToBoxType("test") 36 | AddAnyTypeBoxDef(&testBox{}, boxType) 37 | 38 | box := testBox{ 39 | AnyTypeBox: AnyTypeBox{ 40 | Type: boxType, 41 | }, 42 | FullBox: FullBox{ 43 | Version: 0, 44 | Flags: [3]byte{0x00, 0x00, 0x00}, 45 | }, 46 | String: "abema.tv", 47 | Int32: -1234567890, 48 | Int32Hex: 0x12345678, 49 | Int32HexMinus: -0x12345678, 50 | Uint32: 1234567890, 51 | Bytes: []byte{'A', 'B', 'E', 'M', 'A', 0x00, 'T', 'V'}, 52 | Ptr: &inner{ 53 | Uint64: 0x1234567890, 54 | }, 55 | PtrEx: &inner{ 56 | Uint64: 0x1234567890, 57 | }, 58 | Struct: inner{ 59 | Uint64: 0x1234567890, 60 | }, 61 | StructEx: inner{ 62 | Uint64: 0x1234567890, 63 | }, 64 | Array: [7]byte{'f', 'o', 'o', 0x00, 'b', 'a', 'r'}, 65 | Bool: true, 66 | UUID: [16]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}, 67 | NotSorted15: 15, 68 | NotSorted16: 16, 69 | NotSorted14: 14, 70 | } 71 | 72 | str, err := StringifyWithIndent(&box, " ", Context{}) 73 | require.NoError(t, err) 74 | assert.Equal(t, ` Version=0`+"\n"+ 75 | ` Flags=0x000000`+"\n"+ 76 | ` String="abema.tv"`+"\n"+ 77 | ` Int32=-1234567890`+"\n"+ 78 | ` Int32Hex=0x12345678`+"\n"+ 79 | ` Int32HexMinus=-0x12345678`+"\n"+ 80 | ` Uint32=1234567890`+"\n"+ 81 | ` Bytes="ABEMA.TV"`+"\n"+ 82 | ` Ptr={`+"\n"+ 83 | ` Uint64=0x1234567890`+"\n"+ 84 | ` }`+"\n"+ 85 | ` Uint64=0x1234567890`+"\n"+ 86 | ` Struct={`+"\n"+ 87 | ` Uint64=0x1234567890`+"\n"+ 88 | ` }`+"\n"+ 89 | ` Uint64=0x1234567890`+"\n"+ 90 | ` Array="foo.bar"`+"\n"+ 91 | ` Bool=true`+"\n"+ 92 | ` UUID=01234567-89ab-cdef-0123-456789abcdef`+"\n"+ 93 | ` NotSorted14=14`+"\n"+ 94 | ` NotSorted15=15`+"\n"+ 95 | ` NotSorted16=16`+"\n", str) 96 | 97 | str, err = Stringify(&box, Context{}) 98 | require.NoError(t, err) 99 | assert.Equal(t, `Version=0 Flags=0x000000 String="abema.tv" Int32=-1234567890 Int32Hex=0x12345678 Int32HexMinus=-0x12345678 Uint32=1234567890 Bytes="ABEMA.TV" Ptr={Uint64=0x1234567890} Uint64=0x1234567890 Struct={Uint64=0x1234567890} Uint64=0x1234567890 Array="foo.bar" Bool=true UUID=01234567-89ab-cdef-0123-456789abcdef NotSorted14=14 NotSorted15=15 NotSorted16=16`, str) 100 | } 101 | -------------------------------------------------------------------------------- /cmd/mp4tool/internal/probe/probe_test.go: -------------------------------------------------------------------------------- 1 | package probe 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestProbe(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | file string 16 | options []string 17 | wants string 18 | }{ 19 | { 20 | name: "sample.mp4 no-options", 21 | file: "../../../../testdata/sample.mp4", 22 | wants: sampleMP4JSONOutput, 23 | }, 24 | { 25 | name: "sample.mp4 format-json", 26 | file: "../../../../testdata/sample.mp4", 27 | options: []string{"-format", "json"}, 28 | wants: sampleMP4JSONOutput, 29 | }, 30 | { 31 | name: "sample.mp4 format-json", 32 | file: "../../../../testdata/sample.mp4", 33 | options: []string{"-format", "yaml"}, 34 | wants: sampleMP4YamlOutput, 35 | }, 36 | } 37 | for _, tc := range testCases { 38 | stdout := os.Stdout 39 | r, w, err := os.Pipe() 40 | require.NoError(t, err) 41 | defer func() { 42 | os.Stdout = stdout 43 | }() 44 | os.Stdout = w 45 | go func() { 46 | require.Zero(t, Main(append(tc.options, tc.file))) 47 | w.Close() 48 | }() 49 | b, err := io.ReadAll(r) 50 | require.NoError(t, err) 51 | assert.Equal(t, tc.wants, string(b)) 52 | } 53 | } 54 | 55 | var sampleMP4JSONOutput = "" + 56 | `{` + "\n" + 57 | ` "MajorBrand": "isom",` + "\n" + 58 | ` "MinorVersion": 512,` + "\n" + 59 | ` "CompatibleBrands": [` + "\n" + 60 | ` "isom",` + "\n" + 61 | ` "iso2",` + "\n" + 62 | ` "avc1",` + "\n" + 63 | ` "mp41"` + "\n" + 64 | ` ],` + "\n" + 65 | ` "FastStart": false,` + "\n" + 66 | ` "Timescale": 1000,` + "\n" + 67 | ` "Duration": 1024,` + "\n" + 68 | ` "DurationSeconds": 1.024,` + "\n" + 69 | ` "Tracks": [` + "\n" + 70 | ` {` + "\n" + 71 | ` "TrackID": 1,` + "\n" + 72 | ` "Timescale": 10240,` + "\n" + 73 | ` "Duration": 10240,` + "\n" + 74 | ` "DurationSeconds": 1,` + "\n" + 75 | ` "Codec": "avc1.64000C",` + "\n" + 76 | ` "Encrypted": false,` + "\n" + 77 | ` "Width": 320,` + "\n" + 78 | ` "Height": 180,` + "\n" + 79 | ` "SampleNum": 10,` + "\n" + 80 | ` "ChunkNum": 9,` + "\n" + 81 | ` "IDRFrameNum": 1,` + "\n" + 82 | ` "Bitrate": 40336,` + "\n" + 83 | ` "MaxBitrate": 40336` + "\n" + 84 | ` },` + "\n" + 85 | ` {` + "\n" + 86 | ` "TrackID": 2,` + "\n" + 87 | ` "Timescale": 44100,` + "\n" + 88 | ` "Duration": 45124,` + "\n" + 89 | ` "DurationSeconds": 1.02322,` + "\n" + 90 | ` "Codec": "mp4a.40.2",` + "\n" + 91 | ` "Encrypted": false,` + "\n" + 92 | ` "SampleNum": 44,` + "\n" + 93 | ` "ChunkNum": 9,` + "\n" + 94 | ` "Bitrate": 10570,` + "\n" + 95 | ` "MaxBitrate": 10632` + "\n" + 96 | ` }` + "\n" + 97 | ` ]` + "\n" + 98 | `}` + "\n" 99 | 100 | var sampleMP4YamlOutput = "" + 101 | `major_brand: isom` + "\n" + 102 | `minor_version: 512` + "\n" + 103 | `compatible_brands:` + "\n" + 104 | `- isom` + "\n" + 105 | `- iso2` + "\n" + 106 | `- avc1` + "\n" + 107 | `- mp41` + "\n" + 108 | `fast_start: false` + "\n" + 109 | `timescale: 1000` + "\n" + 110 | `duration: 1024` + "\n" + 111 | `duration_seconds: 1.024` + "\n" + 112 | `tracks:` + "\n" + 113 | `- track_id: 1` + "\n" + 114 | ` timescale: 10240` + "\n" + 115 | ` duration: 10240` + "\n" + 116 | ` duration_seconds: 1` + "\n" + 117 | ` codec: avc1.64000C` + "\n" + 118 | ` encrypted: false` + "\n" + 119 | ` width: 320` + "\n" + 120 | ` height: 180` + "\n" + 121 | ` sample_num: 10` + "\n" + 122 | ` chunk_num: 9` + "\n" + 123 | ` idr_frame_num: 1` + "\n" + 124 | ` bitrate: 40336` + "\n" + 125 | ` max_bitrate: 40336` + "\n" + 126 | `- track_id: 2` + "\n" + 127 | ` timescale: 44100` + "\n" + 128 | ` duration: 45124` + "\n" + 129 | ` duration_seconds: 1.02322` + "\n" + 130 | ` codec: mp4a.40.2` + "\n" + 131 | ` encrypted: false` + "\n" + 132 | ` sample_num: 44` + "\n" + 133 | ` chunk_num: 9` + "\n" + 134 | ` bitrate: 10570` + "\n" + 135 | ` max_bitrate: 10632` + "\n" 136 | -------------------------------------------------------------------------------- /box_types_iso14496_14.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import "fmt" 4 | 5 | /*************************** esds ****************************/ 6 | 7 | // https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html 8 | 9 | func BoxTypeEsds() BoxType { return StrToBoxType("esds") } 10 | 11 | func init() { 12 | AddBoxDef(&Esds{}, 0) 13 | } 14 | 15 | const ( 16 | ESDescrTag = 0x03 17 | DecoderConfigDescrTag = 0x04 18 | DecSpecificInfoTag = 0x05 19 | SLConfigDescrTag = 0x06 20 | ) 21 | 22 | // Esds is ES descripter box 23 | type Esds struct { 24 | FullBox `mp4:"0,extend"` 25 | Descriptors []Descriptor `mp4:"1,array"` 26 | } 27 | 28 | // GetType returns the BoxType 29 | func (*Esds) GetType() BoxType { 30 | return BoxTypeEsds() 31 | } 32 | 33 | type Descriptor struct { 34 | BaseCustomFieldObject 35 | Tag int8 `mp4:"0,size=8"` // must be 0x03 36 | Size uint32 `mp4:"1,varint"` 37 | ESDescriptor *ESDescriptor `mp4:"2,extend,opt=dynamic"` 38 | DecoderConfigDescriptor *DecoderConfigDescriptor `mp4:"3,extend,opt=dynamic"` 39 | Data []byte `mp4:"4,size=8,opt=dynamic,len=dynamic"` 40 | } 41 | 42 | // GetFieldLength returns length of dynamic field 43 | func (ds *Descriptor) GetFieldLength(name string, ctx Context) uint { 44 | switch name { 45 | case "Data": 46 | return uint(ds.Size) 47 | } 48 | panic(fmt.Errorf("invalid name of dynamic-length field: boxType=esds fieldName=%s", name)) 49 | } 50 | 51 | func (ds *Descriptor) IsOptFieldEnabled(name string, ctx Context) bool { 52 | switch ds.Tag { 53 | case ESDescrTag: 54 | return name == "ESDescriptor" 55 | case DecoderConfigDescrTag: 56 | return name == "DecoderConfigDescriptor" 57 | default: 58 | return name == "Data" 59 | } 60 | } 61 | 62 | // StringifyField returns field value as string 63 | func (ds *Descriptor) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) { 64 | switch name { 65 | case "Tag": 66 | switch ds.Tag { 67 | case ESDescrTag: 68 | return "ESDescr", true 69 | case DecoderConfigDescrTag: 70 | return "DecoderConfigDescr", true 71 | case DecSpecificInfoTag: 72 | return "DecSpecificInfo", true 73 | case SLConfigDescrTag: 74 | return "SLConfigDescr", true 75 | default: 76 | return "", false 77 | } 78 | default: 79 | return "", false 80 | } 81 | } 82 | 83 | type ESDescriptor struct { 84 | BaseCustomFieldObject 85 | ESID uint16 `mp4:"0,size=16"` 86 | StreamDependenceFlag bool `mp4:"1,size=1"` 87 | UrlFlag bool `mp4:"2,size=1"` 88 | OcrStreamFlag bool `mp4:"3,size=1"` 89 | StreamPriority int8 `mp4:"4,size=5"` 90 | DependsOnESID uint16 `mp4:"5,size=16,opt=dynamic"` 91 | URLLength uint8 `mp4:"6,size=8,opt=dynamic"` 92 | URLString []byte `mp4:"7,size=8,len=dynamic,opt=dynamic,string"` 93 | OCRESID uint16 `mp4:"8,size=16,opt=dynamic"` 94 | } 95 | 96 | func (esds *ESDescriptor) GetFieldLength(name string, ctx Context) uint { 97 | switch name { 98 | case "URLString": 99 | return uint(esds.URLLength) 100 | } 101 | panic(fmt.Errorf("invalid name of dynamic-length field: boxType=ESDescriptor fieldName=%s", name)) 102 | } 103 | 104 | func (esds *ESDescriptor) IsOptFieldEnabled(name string, ctx Context) bool { 105 | switch name { 106 | case "DependsOnESID": 107 | return esds.StreamDependenceFlag 108 | case "URLLength", "URLString": 109 | return esds.UrlFlag 110 | case "OCRESID": 111 | return esds.OcrStreamFlag 112 | default: 113 | return false 114 | } 115 | } 116 | 117 | type DecoderConfigDescriptor struct { 118 | BaseCustomFieldObject 119 | ObjectTypeIndication byte `mp4:"0,size=8"` 120 | StreamType int8 `mp4:"1,size=6"` 121 | UpStream bool `mp4:"2,size=1"` 122 | Reserved bool `mp4:"3,size=1"` 123 | BufferSizeDB uint32 `mp4:"4,size=24"` 124 | MaxBitrate uint32 `mp4:"5,size=32"` 125 | AvgBitrate uint32 `mp4:"6,size=32"` 126 | } 127 | -------------------------------------------------------------------------------- /box_types_iso14496_30_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBoxTypesISO14496_30(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | src IImmutableBox 16 | dst IBox 17 | bin []byte 18 | str string 19 | ctx Context 20 | }{ 21 | { 22 | name: "vttC", 23 | src: &WebVTTConfigurationBox{ 24 | Config: "WEBVTT\n", 25 | }, 26 | dst: &WebVTTConfigurationBox{}, 27 | bin: []byte{'W', 'E', 'B', 'V', 'T', 'T', '\n'}, 28 | str: `Config="WEBVTT."`, 29 | }, 30 | { 31 | name: "vlab", 32 | src: &WebVTTSourceLabelBox{ 33 | SourceLabel: "Source", 34 | }, 35 | dst: &WebVTTSourceLabelBox{}, 36 | bin: []byte{'S', 'o', 'u', 'r', 'c', 'e'}, 37 | str: `SourceLabel="Source"`, 38 | }, 39 | { 40 | name: "wvtt", 41 | src: &WVTTSampleEntry{ 42 | SampleEntry: SampleEntry{ 43 | AnyTypeBox: AnyTypeBox{Type: StrToBoxType("wvtt")}, 44 | DataReferenceIndex: 0x1234, 45 | }, 46 | }, 47 | dst: &WVTTSampleEntry{SampleEntry: SampleEntry{AnyTypeBox: AnyTypeBox{Type: StrToBoxType("wvtt")}}}, 48 | bin: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x12, 0x34}, 49 | str: `DataReferenceIndex=4660`, 50 | }, 51 | { 52 | name: "vttc", 53 | src: &VTTCueBox{}, 54 | dst: &VTTCueBox{}, 55 | bin: []byte(nil), 56 | str: ``, 57 | }, 58 | { 59 | name: "vsid", 60 | src: &CueSourceIDBox{ 61 | SourceId: 0, 62 | }, 63 | dst: &CueSourceIDBox{}, 64 | bin: []byte{0, 0, 0, 0}, 65 | str: `SourceId=0`, 66 | }, 67 | { 68 | name: "ctim", 69 | src: &CueTimeBox{ 70 | CueCurrentTime: "00:00:00.000", 71 | }, 72 | dst: &CueTimeBox{}, 73 | bin: []byte{'0', '0', ':', '0', '0', ':', '0', '0', '.', '0', '0', '0'}, 74 | str: `CueCurrentTime="00:00:00.000"`, 75 | }, 76 | { 77 | name: "iden", 78 | src: &CueIDBox{ 79 | CueId: "example_id", 80 | }, 81 | dst: &CueIDBox{}, 82 | bin: []byte{'e', 'x', 'a', 'm', 'p', 'l', 'e', '_', 'i', 'd'}, 83 | str: `CueId="example_id"`, 84 | }, 85 | { 86 | name: "sttg", 87 | src: &CueSettingsBox{ 88 | Settings: "line=0", 89 | }, 90 | dst: &CueSettingsBox{}, 91 | bin: []byte{'l', 'i', 'n', 'e', '=', '0'}, 92 | str: `Settings="line=0"`, 93 | }, 94 | { 95 | name: "payl", 96 | src: &CuePayloadBox{ 97 | CueText: "sample", 98 | }, 99 | dst: &CuePayloadBox{}, 100 | bin: []byte{'s', 'a', 'm', 'p', 'l', 'e'}, 101 | str: `CueText="sample"`, 102 | }, 103 | { 104 | name: "vtte", 105 | src: &VTTEmptyCueBox{}, 106 | dst: &VTTEmptyCueBox{}, 107 | bin: []byte(nil), 108 | str: ``, 109 | }, 110 | { 111 | name: "vtta", 112 | src: &VTTAdditionalTextBox{ 113 | CueAdditionalText: "test", 114 | }, 115 | dst: &VTTAdditionalTextBox{}, 116 | bin: []byte{'t', 'e', 's', 't'}, 117 | str: `CueAdditionalText="test"`, 118 | }, 119 | } 120 | 121 | for _, tc := range testCases { 122 | t.Run(tc.name, func(t *testing.T) { 123 | // Marshal 124 | buf := bytes.NewBuffer(nil) 125 | n, err := Marshal(buf, tc.src, tc.ctx) 126 | require.NoError(t, err) 127 | assert.Equal(t, uint64(len(tc.bin)), n) 128 | assert.Equal(t, tc.bin, buf.Bytes()) 129 | 130 | // Unmarshal 131 | r := bytes.NewReader(tc.bin) 132 | n, err = Unmarshal(r, uint64(len(tc.bin)), tc.dst, tc.ctx) 133 | require.NoError(t, err) 134 | assert.Equal(t, uint64(buf.Len()), n) 135 | assert.Equal(t, tc.src, tc.dst) 136 | s, err := r.Seek(0, io.SeekCurrent) 137 | require.NoError(t, err) 138 | assert.Equal(t, int64(buf.Len()), s) 139 | 140 | // UnmarshalAny 141 | dst, n, err := UnmarshalAny(bytes.NewReader(tc.bin), tc.src.GetType(), uint64(len(tc.bin)), tc.ctx) 142 | require.NoError(t, err) 143 | assert.Equal(t, uint64(buf.Len()), n) 144 | assert.Equal(t, tc.src, dst) 145 | s, err = r.Seek(0, io.SeekCurrent) 146 | require.NoError(t, err) 147 | assert.Equal(t, int64(buf.Len()), s) 148 | 149 | // Stringify 150 | str, err := Stringify(tc.src, tc.ctx) 151 | require.NoError(t, err) 152 | assert.Equal(t, tc.str, str) 153 | }) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /write_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "gopkg.in/src-d/go-billy.v4/memfs" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestWriter(t *testing.T) { 15 | output, err := memfs.New().Create("output.mp4") 16 | require.NoError(t, err) 17 | defer output.Close() 18 | w := NewWriter(output) 19 | 20 | // start ftyp 21 | bi, err := w.StartBox(&BoxInfo{Type: BoxTypeFtyp()}) 22 | require.NoError(t, err) 23 | assert.Equal(t, uint64(0), bi.Offset) 24 | assert.Equal(t, uint64(8), bi.Size) 25 | 26 | ftyp := &Ftyp{ 27 | MajorBrand: [4]byte{'a', 'b', 'e', 'm'}, 28 | MinorVersion: 0x12345678, 29 | CompatibleBrands: []CompatibleBrandElem{ 30 | {CompatibleBrand: [4]byte{'a', 'b', 'c', 'd'}}, 31 | {CompatibleBrand: [4]byte{'e', 'f', 'g', 'h'}}, 32 | }, 33 | } 34 | _, err = Marshal(w, ftyp, Context{}) 35 | require.NoError(t, err) 36 | 37 | // end ftyp 38 | bi, err = w.EndBox() 39 | require.NoError(t, err) 40 | assert.Equal(t, uint64(0), bi.Offset) 41 | assert.Equal(t, uint64(24), bi.Size) 42 | 43 | // start moov 44 | bi, err = w.StartBox(&BoxInfo{Type: BoxTypeMoov()}) 45 | require.NoError(t, err) 46 | assert.Equal(t, uint64(24), bi.Offset) 47 | assert.Equal(t, uint64(8), bi.Size) 48 | 49 | // copy 50 | err = w.CopyBox(bytes.NewReader([]byte{ 51 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 52 | 0x00, 0x00, 0x00, 0x0a, 53 | 'u', 'd', 't', 'a', 54 | 0x01, 0x02, 0x03, 0x04, 55 | 0x05, 0x06, 0x07, 0x08, 56 | }), &BoxInfo{Offset: 6, Size: 15}) 57 | require.NoError(t, err) 58 | 59 | // start trak 60 | bi, err = w.StartBox(&BoxInfo{Type: BoxTypeTrak()}) 61 | require.NoError(t, err) 62 | assert.Equal(t, uint64(47), bi.Offset) 63 | assert.Equal(t, uint64(8), bi.Size) 64 | 65 | // start tkhd 66 | bi, err = w.StartBox(&BoxInfo{Type: BoxTypeTkhd()}) 67 | require.NoError(t, err) 68 | assert.Equal(t, uint64(55), bi.Offset) 69 | assert.Equal(t, uint64(8), bi.Size) 70 | 71 | _, err = Marshal(w, &Tkhd{ 72 | CreationTimeV0: 1, 73 | ModificationTimeV0: 2, 74 | TrackID: 3, 75 | DurationV0: 4, 76 | Layer: 5, 77 | AlternateGroup: 6, 78 | Volume: 7, 79 | Width: 8, 80 | Height: 9, 81 | }, Context{}) 82 | require.NoError(t, err) 83 | 84 | // end tkhd 85 | bi, err = w.EndBox() 86 | require.NoError(t, err) 87 | assert.Equal(t, uint64(55), bi.Offset) 88 | assert.Equal(t, uint64(92), bi.Size) 89 | 90 | // end trak 91 | bi, err = w.EndBox() 92 | require.NoError(t, err) 93 | assert.Equal(t, uint64(47), bi.Offset) 94 | assert.Equal(t, uint64(100), bi.Size) 95 | 96 | // end moov 97 | bi, err = w.EndBox() 98 | require.NoError(t, err) 99 | assert.Equal(t, uint64(24), bi.Offset) 100 | assert.Equal(t, uint64(123), bi.Size) 101 | 102 | // update ftyp 103 | n, err := w.Seek(8, io.SeekStart) 104 | require.NoError(t, err) 105 | assert.Equal(t, int64(8), n) 106 | ftyp.CompatibleBrands[1].CompatibleBrand = [4]byte{'E', 'F', 'G', 'H'} 107 | _, err = Marshal(w, ftyp, Context{}) 108 | require.NoError(t, err) 109 | 110 | _, err = output.Seek(0, io.SeekStart) 111 | require.NoError(t, err) 112 | bin, err := io.ReadAll(output) 113 | require.NoError(t, err) 114 | assert.Equal(t, []byte{ 115 | // ftyp 116 | 0x00, 0x00, 0x00, 0x18, // size 117 | 'f', 't', 'y', 'p', // type 118 | 'a', 'b', 'e', 'm', // major brand 119 | 0x12, 0x34, 0x56, 0x78, // minor version 120 | 'a', 'b', 'c', 'd', // compatible brand 121 | 'E', 'F', 'G', 'H', // compatible brand 122 | // moov 123 | 0x00, 0x00, 0x00, 0x7b, // size 124 | 'm', 'o', 'o', 'v', // type 125 | // udta (copy) 126 | 0x00, 0x00, 0x00, 0x0a, 127 | 'u', 'd', 't', 'a', 128 | 0x01, 0x02, 0x03, 0x04, 129 | 0x05, 0x06, 0x07, 130 | // trak 131 | 0x00, 0x00, 0x00, 0x64, // size 132 | 't', 'r', 'a', 'k', // type 133 | // tkhd 134 | 0x00, 0x00, 0x00, 0x5c, // size 135 | 't', 'k', 'h', 'd', // type 136 | 0, // version 137 | 0x00, 0x00, 0x00, // flags 138 | 0x00, 0x00, 0x00, 0x01, // creation time 139 | 0x00, 0x00, 0x00, 0x02, // modification time 140 | 0x00, 0x00, 0x00, 0x03, // track ID 141 | 0x00, 0x00, 0x00, 0x00, // reserved 142 | 0x00, 0x00, 0x00, 0x04, // duration 143 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 144 | 0x00, 0x05, // layer 145 | 0x00, 0x06, // alternate group 146 | 0x00, 0x07, // volume 147 | 0x00, 0x00, // reserved 148 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 149 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 150 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // matrix 151 | 0x00, 0x00, 0x00, 0x08, // width 152 | 0x00, 0x00, 0x00, 0x09, // height 153 | }, bin) 154 | } 155 | -------------------------------------------------------------------------------- /mp4.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | var ErrBoxInfoNotFound = errors.New("box info not found") 12 | 13 | // BoxType is mpeg box type 14 | type BoxType [4]byte 15 | 16 | func StrToBoxType(code string) BoxType { 17 | if len(code) != 4 { 18 | panic(fmt.Errorf("invalid box type id length: [%s]", code)) 19 | } 20 | return BoxType{code[0], code[1], code[2], code[3]} 21 | } 22 | 23 | // Uint32ToBoxType returns a new BoxType from the provied uint32 24 | func Uint32ToBoxType(i uint32) BoxType { 25 | b := make([]byte, 4) 26 | binary.BigEndian.PutUint32(b, i) 27 | return BoxType{b[0], b[1], b[2], b[3]} 28 | } 29 | 30 | func (boxType BoxType) String() string { 31 | if isPrintable(boxType[0]) && isPrintable(boxType[1]) && isPrintable(boxType[2]) && isPrintable(boxType[3]) { 32 | s := string([]byte{boxType[0], boxType[1], boxType[2], boxType[3]}) 33 | s = strings.ReplaceAll(s, string([]byte{0xa9}), "(c)") 34 | return s 35 | } 36 | return fmt.Sprintf("0x%02x%02x%02x%02x", boxType[0], boxType[1], boxType[2], boxType[3]) 37 | } 38 | 39 | func isASCII(c byte) bool { 40 | return c >= 0x20 && c <= 0x7e 41 | } 42 | 43 | func isPrintable(c byte) bool { 44 | return isASCII(c) || c == 0xa9 45 | } 46 | 47 | func (lhs BoxType) MatchWith(rhs BoxType) bool { 48 | if lhs == boxTypeAny || rhs == boxTypeAny { 49 | return true 50 | } 51 | return lhs == rhs 52 | } 53 | 54 | var boxTypeAny = BoxType{0x00, 0x00, 0x00, 0x00} 55 | 56 | func BoxTypeAny() BoxType { 57 | return boxTypeAny 58 | } 59 | 60 | type boxDef struct { 61 | dataType reflect.Type 62 | versions []uint8 63 | isTarget func(Context) bool 64 | fields []*field 65 | } 66 | 67 | var boxMap = make(map[BoxType][]boxDef, 64) 68 | 69 | func AddBoxDef(payload IBox, versions ...uint8) { 70 | boxMap[payload.GetType()] = append(boxMap[payload.GetType()], boxDef{ 71 | dataType: reflect.TypeOf(payload).Elem(), 72 | versions: versions, 73 | fields: buildFields(payload), 74 | }) 75 | } 76 | 77 | func AddBoxDefEx(payload IBox, isTarget func(Context) bool, versions ...uint8) { 78 | boxMap[payload.GetType()] = append(boxMap[payload.GetType()], boxDef{ 79 | dataType: reflect.TypeOf(payload).Elem(), 80 | versions: versions, 81 | isTarget: isTarget, 82 | fields: buildFields(payload), 83 | }) 84 | } 85 | 86 | func AddAnyTypeBoxDef(payload IAnyType, boxType BoxType, versions ...uint8) { 87 | boxMap[boxType] = append(boxMap[boxType], boxDef{ 88 | dataType: reflect.TypeOf(payload).Elem(), 89 | versions: versions, 90 | fields: buildFields(payload), 91 | }) 92 | } 93 | 94 | func AddAnyTypeBoxDefEx(payload IAnyType, boxType BoxType, isTarget func(Context) bool, versions ...uint8) { 95 | boxMap[boxType] = append(boxMap[boxType], boxDef{ 96 | dataType: reflect.TypeOf(payload).Elem(), 97 | versions: versions, 98 | isTarget: isTarget, 99 | fields: buildFields(payload), 100 | }) 101 | } 102 | 103 | var itemBoxFields = buildFields(&Item{}) 104 | 105 | func (boxType BoxType) getBoxDef(ctx Context) *boxDef { 106 | boxDefs := boxMap[boxType] 107 | for i := len(boxDefs) - 1; i >= 0; i-- { 108 | boxDef := &boxDefs[i] 109 | if boxDef.isTarget == nil || boxDef.isTarget(ctx) { 110 | return boxDef 111 | } 112 | } 113 | if ctx.UnderIlst { 114 | typeID := int(binary.BigEndian.Uint32(boxType[:])) 115 | if typeID >= 1 && typeID <= ctx.QuickTimeKeysMetaEntryCount { 116 | return &boxDef{ 117 | dataType: reflect.TypeOf(Item{}), 118 | isTarget: isIlstMetaContainer, 119 | fields: itemBoxFields, 120 | } 121 | } 122 | } 123 | return nil 124 | } 125 | 126 | func (boxType BoxType) IsSupported(ctx Context) bool { 127 | return boxType.getBoxDef(ctx) != nil 128 | } 129 | 130 | func (boxType BoxType) New(ctx Context) (IBox, error) { 131 | boxDef := boxType.getBoxDef(ctx) 132 | if boxDef == nil { 133 | return nil, ErrBoxInfoNotFound 134 | } 135 | 136 | box, ok := reflect.New(boxDef.dataType).Interface().(IBox) 137 | if !ok { 138 | return nil, fmt.Errorf("box type not implements IBox interface: %s", boxType.String()) 139 | } 140 | 141 | anyTypeBox, ok := box.(IAnyType) 142 | if ok { 143 | anyTypeBox.SetType(boxType) 144 | } 145 | 146 | return box, nil 147 | } 148 | 149 | func (boxType BoxType) GetSupportedVersions(ctx Context) ([]uint8, error) { 150 | boxDef := boxType.getBoxDef(ctx) 151 | if boxDef == nil { 152 | return nil, ErrBoxInfoNotFound 153 | } 154 | return boxDef.versions, nil 155 | } 156 | 157 | func (boxType BoxType) IsSupportedVersion(ver uint8, ctx Context) bool { 158 | boxDef := boxType.getBoxDef(ctx) 159 | if boxDef == nil { 160 | return false 161 | } 162 | if len(boxDef.versions) == 0 { 163 | return true 164 | } 165 | for _, sver := range boxDef.versions { 166 | if ver == sver { 167 | return true 168 | } 169 | } 170 | return false 171 | } 172 | -------------------------------------------------------------------------------- /box_info.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "math" 9 | ) 10 | 11 | type Context struct { 12 | // IsQuickTimeCompatible represents whether ftyp.compatible_brands contains "qt ". 13 | IsQuickTimeCompatible bool 14 | 15 | // QuickTimeKeysMetaEntryCount the expected number of items under the ilst box as observed from the keys box 16 | QuickTimeKeysMetaEntryCount int 17 | 18 | // UnderWave represents whether current box is under the wave box. 19 | UnderWave bool 20 | 21 | // UnderIlst represents whether current box is under the ilst box. 22 | UnderIlst bool 23 | 24 | // UnderIlstMeta represents whether current box is under the metadata box under the ilst box. 25 | UnderIlstMeta bool 26 | 27 | // UnderIlstFreeMeta represents whether current box is under "----" box. 28 | UnderIlstFreeMeta bool 29 | 30 | // UnderUdta represents whether current box is under the udta box. 31 | UnderUdta bool 32 | } 33 | 34 | // BoxInfo has common infomations of box 35 | type BoxInfo struct { 36 | // Offset specifies an offset of the box in a file. 37 | Offset uint64 38 | 39 | // Size specifies size(bytes) of box. 40 | Size uint64 41 | 42 | // HeaderSize specifies size(bytes) of common fields which are defined as "Box" class member at ISO/IEC 14496-12. 43 | HeaderSize uint64 44 | 45 | // Type specifies box type which is represented by 4 characters. 46 | Type BoxType 47 | 48 | // ExtendToEOF is set true when Box.size is zero. It means that end of box equals to end of file. 49 | ExtendToEOF bool 50 | 51 | // Context would be set by ReadBoxStructure, not ReadBoxInfo. 52 | Context 53 | } 54 | 55 | func (bi *BoxInfo) IsSupportedType() bool { 56 | return bi.Type.IsSupported(bi.Context) 57 | } 58 | 59 | const ( 60 | SmallHeaderSize = 8 61 | LargeHeaderSize = 16 62 | ) 63 | 64 | // WriteBoxInfo writes common fields which are defined as "Box" class member at ISO/IEC 14496-12. 65 | // This function ignores bi.Offset and returns BoxInfo which contains real Offset and recalculated Size/HeaderSize. 66 | func WriteBoxInfo(w io.WriteSeeker, bi *BoxInfo) (*BoxInfo, error) { 67 | offset, err := w.Seek(0, io.SeekCurrent) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | var data []byte 73 | if bi.ExtendToEOF { 74 | data = make([]byte, SmallHeaderSize) 75 | } else if bi.Size <= math.MaxUint32 && bi.HeaderSize != LargeHeaderSize { 76 | data = make([]byte, SmallHeaderSize) 77 | binary.BigEndian.PutUint32(data, uint32(bi.Size)) 78 | } else { 79 | data = make([]byte, LargeHeaderSize) 80 | binary.BigEndian.PutUint32(data, 1) 81 | binary.BigEndian.PutUint64(data[SmallHeaderSize:], bi.Size) 82 | } 83 | data[4] = bi.Type[0] 84 | data[5] = bi.Type[1] 85 | data[6] = bi.Type[2] 86 | data[7] = bi.Type[3] 87 | 88 | if _, err := w.Write(data); err != nil { 89 | return nil, err 90 | } 91 | 92 | return &BoxInfo{ 93 | Offset: uint64(offset), 94 | Size: bi.Size - bi.HeaderSize + uint64(len(data)), 95 | HeaderSize: uint64(len(data)), 96 | Type: bi.Type, 97 | ExtendToEOF: bi.ExtendToEOF, 98 | }, nil 99 | } 100 | 101 | // ReadBoxInfo reads common fields which are defined as "Box" class member at ISO/IEC 14496-12. 102 | func ReadBoxInfo(r io.ReadSeeker) (*BoxInfo, error) { 103 | offset, err := r.Seek(0, io.SeekCurrent) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | bi := &BoxInfo{ 109 | Offset: uint64(offset), 110 | } 111 | 112 | // read 8 bytes 113 | buf := bytes.NewBuffer(make([]byte, 0, SmallHeaderSize)) 114 | if _, err := io.CopyN(buf, r, SmallHeaderSize); err != nil { 115 | return nil, err 116 | } 117 | bi.HeaderSize += SmallHeaderSize 118 | 119 | // pick size and type 120 | data := buf.Bytes() 121 | bi.Size = uint64(binary.BigEndian.Uint32(data)) 122 | bi.Type = BoxType{data[4], data[5], data[6], data[7]} 123 | 124 | if bi.Size == 0 { 125 | // box extends to end of file 126 | offsetEOF, err := r.Seek(0, io.SeekEnd) 127 | if err != nil { 128 | return nil, err 129 | } 130 | bi.Size = uint64(offsetEOF) - bi.Offset 131 | bi.ExtendToEOF = true 132 | if _, err := bi.SeekToPayload(r); err != nil { 133 | return nil, err 134 | } 135 | } else if bi.Size == 1 { 136 | // read more 8 bytes 137 | buf.Reset() 138 | if _, err := io.CopyN(buf, r, LargeHeaderSize-SmallHeaderSize); err != nil { 139 | return nil, err 140 | } 141 | bi.HeaderSize += LargeHeaderSize - SmallHeaderSize 142 | bi.Size = binary.BigEndian.Uint64(buf.Bytes()) 143 | } 144 | 145 | if bi.Size == 0 { 146 | return nil, fmt.Errorf("invalid size") 147 | } 148 | 149 | return bi, nil 150 | } 151 | 152 | func (bi *BoxInfo) SeekToStart(s io.Seeker) (int64, error) { 153 | return s.Seek(int64(bi.Offset), io.SeekStart) 154 | } 155 | 156 | func (bi *BoxInfo) SeekToPayload(s io.Seeker) (int64, error) { 157 | return s.Seek(int64(bi.Offset+bi.HeaderSize), io.SeekStart) 158 | } 159 | 160 | func (bi *BoxInfo) SeekToEnd(s io.Seeker) (int64, error) { 161 | return s.Seek(int64(bi.Offset+bi.Size), io.SeekStart) 162 | } 163 | -------------------------------------------------------------------------------- /internal/bitio/read_test.go: -------------------------------------------------------------------------------- 1 | package bitio 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestRead(t *testing.T) { 13 | buf := bytes.NewReader([]byte{ 14 | 0xb5, 0x63, 0xd5, // 1011,0101,0110,0011,1101,0101 15 | 0xa4, 0x6f, 16 | 0xb4, 0xf1, 0xd7, // 1011,0100,1111,0001,1101,0111 17 | }) 18 | r := NewReader(buf) 19 | var data []byte 20 | var err error 21 | var bit bool 22 | 23 | // 0101,1010 24 | // ^^^ ^^^^ 25 | data, err = r.ReadBits(7) 26 | require.NoError(t, err) 27 | require.Equal(t, []byte{0x5a}, data) 28 | 29 | // 0000,0001,0110,0011,1101,0101 30 | // ^ ^^^^ ^^^^ ^^^^ ^^^^ 31 | data, err = r.ReadBits(17) 32 | require.NoError(t, err) 33 | require.Equal(t, []byte{0x01, 0x63, 0xd5}, data) 34 | 35 | data = make([]byte, 2) 36 | n, err := r.Read(data) 37 | require.NoError(t, err) 38 | require.Equal(t, 2, n) 39 | assert.Equal(t, []byte{0xa4, 0x6f}, data) 40 | 41 | // 0000,0001,0110,1001,1110,0011 42 | // ^ ^^^^ ^^^^ ^^^^ ^^^^ 43 | data, err = r.ReadBits(17) 44 | require.NoError(t, err) 45 | require.Equal(t, []byte{0x01, 0x69, 0xe3}, data) 46 | 47 | bit, err = r.ReadBit() 48 | require.NoError(t, err) 49 | assert.True(t, bit) 50 | bit, err = r.ReadBit() 51 | require.NoError(t, err) 52 | assert.False(t, bit) 53 | 54 | // 0001,0111 55 | // ^ ^^^^ 56 | data, err = r.ReadBits(5) 57 | require.NoError(t, err) 58 | require.Equal(t, []byte{0x17}, data) 59 | } 60 | 61 | func TestReadBits(t *testing.T) { 62 | testCases := []struct { 63 | name string 64 | octet byte 65 | width uint 66 | input []byte 67 | size uint 68 | err bool 69 | expectedData []byte 70 | }{ 71 | { 72 | name: "no width", 73 | input: []byte{0x6c, 0xa5}, 74 | size: 10, 75 | expectedData: []byte{0x01, 0xb2}, 76 | }, 77 | { 78 | name: "width 3", 79 | octet: 0x6c, 80 | width: 3, 81 | input: []byte{0xa5}, 82 | size: 10, 83 | expectedData: []byte{0x02, 0x52}, 84 | }, 85 | { 86 | name: "reach to end of box", 87 | input: []byte{0x6c, 0xa5}, 88 | size: 16, 89 | expectedData: []byte{0x6c, 0xa5}, 90 | }, 91 | { 92 | name: "overrun", 93 | input: []byte{0x6c, 0xa5}, 94 | size: 17, 95 | err: true, 96 | }, 97 | } 98 | for _, tc := range testCases { 99 | t.Run(tc.name, func(t *testing.T) { 100 | r := NewReader(bytes.NewReader(tc.input)) 101 | require.Zero(t, r.(*reader).octet) 102 | require.Zero(t, r.(*reader).width) 103 | r.(*reader).octet = tc.octet 104 | r.(*reader).width = tc.width 105 | data, err := r.ReadBits(tc.size) 106 | if tc.err { 107 | require.Error(t, err) 108 | return 109 | } 110 | require.NoError(t, err) 111 | assert.Equal(t, tc.expectedData, data) 112 | }) 113 | } 114 | } 115 | 116 | func TestReadBit(t *testing.T) { 117 | r := NewReader(bytes.NewReader([]byte{0x6c, 0xa5})).(*reader) 118 | outputs := []struct { 119 | bit bool 120 | octet byte 121 | }{ 122 | {bit: false, octet: 0x6c}, 123 | {bit: true, octet: 0x6c}, 124 | {bit: true, octet: 0x6c}, 125 | {bit: false, octet: 0x6c}, 126 | {bit: true, octet: 0x6c}, 127 | {bit: true, octet: 0x6c}, 128 | {bit: false, octet: 0x6c}, 129 | {bit: false, octet: 0x6c}, 130 | {bit: true, octet: 0xa5}, 131 | {bit: false, octet: 0xa5}, 132 | {bit: true, octet: 0xa5}, 133 | {bit: false, octet: 0xa5}, 134 | {bit: false, octet: 0xa5}, 135 | {bit: true, octet: 0xa5}, 136 | {bit: false, octet: 0xa5}, 137 | {bit: true, octet: 0xa5}, 138 | } 139 | for _, o := range outputs { 140 | bit, err := r.ReadBit() 141 | require.NoError(t, err) 142 | assert.Equal(t, o.bit, bit) 143 | assert.Equal(t, o.octet, r.octet) 144 | } 145 | _, err := r.ReadBits(1) 146 | require.Error(t, err) 147 | } 148 | 149 | func TestReadInvalidAlignment(t *testing.T) { 150 | r := NewReader(bytes.NewReader([]byte{0x6c, 0x82, 0x41, 0x35, 0x71, 0xa4, 0xcd, 0x9f})) 151 | _, err := r.Read(make([]byte, 2)) 152 | require.NoError(t, err) 153 | _, err = r.ReadBits(3) 154 | require.NoError(t, err) 155 | _, err = r.Read(make([]byte, 2)) 156 | assert.Equal(t, ErrInvalidAlignment, err) 157 | } 158 | 159 | func TestSeekInvalidAlignment(t *testing.T) { 160 | r := NewReadSeeker(bytes.NewReader([]byte{0x6c, 0x82, 0x41, 0x35, 0x71, 0xa4, 0xcd, 0x9f})) 161 | 162 | _, err := r.Seek(2, io.SeekCurrent) 163 | require.NoError(t, err) 164 | 165 | data, err := r.ReadBits(3) 166 | require.NoError(t, err) 167 | require.Equal(t, []byte{0x02}, data) 168 | 169 | // When the head is not on 8 bits block border, SeekCurrent fails. 170 | _, err = r.Seek(2, io.SeekCurrent) 171 | assert.Equal(t, ErrInvalidAlignment, err) 172 | 173 | // SeekStart always succeeds. 174 | _, err = r.Seek(0, io.SeekStart) 175 | assert.NoError(t, err) 176 | 177 | data, err = r.ReadBits(3) 178 | require.NoError(t, err) 179 | require.Equal(t, []byte{0x03}, data) 180 | } 181 | -------------------------------------------------------------------------------- /cmd/mp4tool/internal/probe/probe.go: -------------------------------------------------------------------------------- 1 | package probe 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/abema/go-mp4" 11 | "github.com/sunfish-shogi/bufseekio" 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | func Main(args []string) int { 16 | flagSet := flag.NewFlagSet("fragment", flag.ExitOnError) 17 | format := flagSet.String("format", "json", "output format (yaml|json)") 18 | flagSet.Usage = func() { 19 | println("USAGE: mp4tool beta probe [OPTIONS] INPUT.mp4") 20 | flagSet.PrintDefaults() 21 | } 22 | flagSet.Parse(args) 23 | 24 | if len(flagSet.Args()) < 1 { 25 | flagSet.Usage() 26 | return 1 27 | } 28 | 29 | ipath := flagSet.Args()[0] 30 | input, err := os.Open(ipath) 31 | if err != nil { 32 | fmt.Println("Failed to open the input file:", err) 33 | return 1 34 | } 35 | defer input.Close() 36 | 37 | r := bufseekio.NewReadSeeker(input, 1024, 4) 38 | rep, err := buildReport(r) 39 | if err != nil { 40 | fmt.Println("Error:", err) 41 | return 1 42 | } 43 | switch *format { 44 | case "json": 45 | enc := json.NewEncoder(os.Stdout) 46 | enc.SetIndent("", " ") 47 | err = enc.Encode(rep) 48 | default: 49 | err = yaml.NewEncoder(os.Stdout).Encode(rep) 50 | } 51 | if err != nil { 52 | fmt.Println("Error:", err) 53 | return 1 54 | } 55 | return 0 56 | } 57 | 58 | type report struct { 59 | MajorBrand string `yaml:"major_brand"` 60 | MinorVersion uint32 `yaml:"minor_version"` 61 | CompatibleBrands []string `yaml:"compatible_brands"` 62 | FastStart bool `yaml:"fast_start"` 63 | Timescale uint32 `yaml:"timescale"` 64 | Duration uint64 `yaml:"duration"` 65 | DurationSeconds float32 `yaml:"duration_seconds"` 66 | Tracks []*track `yaml:"tracks"` 67 | } 68 | 69 | type track struct { 70 | TrackID uint32 `yaml:"track_id"` 71 | Timescale uint32 `yaml:"timescale"` 72 | Duration uint64 `yaml:"duration"` 73 | DurationSeconds float32 `yaml:"duration_seconds"` 74 | Codec string `yaml:"codec"` 75 | Encrypted bool `yaml:"encrypted"` 76 | Width uint16 `json:",omitempty" yaml:"width,omitempty"` 77 | Height uint16 `json:",omitempty" yaml:"height,omitempty"` 78 | SampleNum int `json:",omitempty" yaml:"sample_num,omitempty"` 79 | ChunkNum int `json:",omitempty" yaml:"chunk_num,omitempty"` 80 | IDRFrameNum int `json:",omitempty" yaml:"idr_frame_num,omitempty"` 81 | Bitrate uint64 `json:",omitempty" yaml:"bitrate,omitempty"` 82 | MaxBitrate uint64 `json:",omitempty" yaml:"max_bitrate,omitempty"` 83 | } 84 | 85 | func buildReport(r io.ReadSeeker) (*report, error) { 86 | info, err := mp4.Probe(r) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | rep := &report{ 92 | MajorBrand: string(info.MajorBrand[:]), 93 | MinorVersion: info.MinorVersion, 94 | CompatibleBrands: make([]string, 0, len(info.CompatibleBrands)), 95 | FastStart: info.FastStart, 96 | Timescale: info.Timescale, 97 | Duration: info.Duration, 98 | DurationSeconds: float32(info.Duration) / float32(info.Timescale), 99 | Tracks: make([]*track, 0, len(info.Tracks)), 100 | } 101 | for _, brand := range info.CompatibleBrands { 102 | rep.CompatibleBrands = append(rep.CompatibleBrands, string(brand[:])) 103 | } 104 | for _, tr := range info.Tracks { 105 | bitrate := tr.Samples.GetBitrate(tr.Timescale) 106 | maxBitrate := tr.Samples.GetMaxBitrate(tr.Timescale, uint64(tr.Timescale)) 107 | if bitrate == 0 || maxBitrate == 0 { 108 | bitrate = info.Segments.GetBitrate(tr.TrackID, tr.Timescale) 109 | maxBitrate = info.Segments.GetMaxBitrate(tr.TrackID, tr.Timescale) 110 | } 111 | t := &track{ 112 | TrackID: tr.TrackID, 113 | Timescale: tr.Timescale, 114 | Duration: tr.Duration, 115 | DurationSeconds: float32(tr.Duration) / float32(tr.Timescale), 116 | Encrypted: tr.Encrypted, 117 | Bitrate: bitrate, 118 | MaxBitrate: maxBitrate, 119 | SampleNum: len(tr.Samples), 120 | ChunkNum: len(tr.Chunks), 121 | } 122 | switch tr.Codec { 123 | case mp4.CodecAVC1: 124 | if tr.AVC != nil { 125 | t.Codec = fmt.Sprintf("avc1.%02X%02X%02X", 126 | tr.AVC.Profile, 127 | tr.AVC.ProfileCompatibility, 128 | tr.AVC.Level, 129 | ) 130 | t.Width = tr.AVC.Width 131 | t.Height = tr.AVC.Height 132 | } else { 133 | t.Codec = "avc1" 134 | } 135 | idxs, err := mp4.FindIDRFrames(r, tr) 136 | if err != nil { 137 | return nil, err 138 | } 139 | t.IDRFrameNum = len(idxs) 140 | case mp4.CodecMP4A: 141 | if tr.MP4A == nil || tr.MP4A.OTI == 0 { 142 | t.Codec = "mp4a" 143 | } else if tr.MP4A.AudOTI == 0 { 144 | t.Codec = fmt.Sprintf("mp4a.%X", tr.MP4A.OTI) 145 | } else { 146 | t.Codec = fmt.Sprintf("mp4a.%X.%d", tr.MP4A.OTI, tr.MP4A.AudOTI) 147 | } 148 | default: 149 | t.Codec = "unknown" 150 | } 151 | rep.Tracks = append(rep.Tracks, t) 152 | } 153 | return rep, nil 154 | } 155 | -------------------------------------------------------------------------------- /box_info_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/orcaman/writerseeker" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestWriteBoxInfo(t *testing.T) { 14 | type testCase struct { 15 | name string 16 | pre []byte 17 | bi *BoxInfo 18 | hasError bool 19 | expectedBI *BoxInfo 20 | expectedBytes []byte 21 | } 22 | 23 | testCases := []testCase{ 24 | { 25 | name: "small-size", 26 | bi: &BoxInfo{ 27 | Size: 0x12345, 28 | HeaderSize: 8, 29 | Type: StrToBoxType("test"), 30 | }, 31 | expectedBI: &BoxInfo{ 32 | Size: 0x12345, 33 | HeaderSize: 8, 34 | Type: StrToBoxType("test"), 35 | }, 36 | expectedBytes: []byte{ 37 | 0x00, 0x01, 0x23, 0x45, 38 | 't', 'e', 's', 't', 39 | }, 40 | }, 41 | { 42 | name: "large-size", 43 | bi: &BoxInfo{ 44 | Size: 0x123456789abc, 45 | HeaderSize: 8, 46 | Type: StrToBoxType("test"), 47 | }, 48 | expectedBI: &BoxInfo{ 49 | Size: 0x123456789abc + 8, 50 | HeaderSize: 16, 51 | Type: StrToBoxType("test"), 52 | }, 53 | expectedBytes: []byte{ 54 | 0x00, 0x00, 0x00, 0x01, 55 | 't', 'e', 's', 't', 56 | 0x00, 0x00, 0x12, 0x34, 57 | 0x56, 0x78, 0x9a, 0xbc, 58 | }, 59 | }, 60 | { 61 | name: "extend to eof", 62 | bi: &BoxInfo{ 63 | Size: 0x123, 64 | HeaderSize: 8, 65 | Type: StrToBoxType("test"), 66 | ExtendToEOF: true, 67 | }, 68 | expectedBI: &BoxInfo{ 69 | Size: 0x123, 70 | HeaderSize: 8, 71 | Type: StrToBoxType("test"), 72 | ExtendToEOF: true, 73 | }, 74 | expectedBytes: []byte{ 75 | 0x00, 0x00, 0x00, 0x00, 76 | 't', 'e', 's', 't', 77 | }, 78 | }, 79 | { 80 | name: "with offset", 81 | pre: []byte{0x00, 0x00, 0x00}, 82 | bi: &BoxInfo{ 83 | Size: 0x12345, 84 | HeaderSize: 8, 85 | Type: StrToBoxType("test"), 86 | }, 87 | expectedBI: &BoxInfo{ 88 | Offset: 3, 89 | Size: 0x12345, 90 | HeaderSize: 8, 91 | Type: StrToBoxType("test"), 92 | }, 93 | expectedBytes: []byte{ 94 | 0x00, 0x00, 0x00, // pre inserted 95 | 0x00, 0x01, 0x23, 0x45, 96 | 't', 'e', 's', 't', 97 | }, 98 | }, 99 | } 100 | 101 | for _, c := range testCases { 102 | t.Run(c.name, func(t *testing.T) { 103 | w := &writerseeker.WriterSeeker{} 104 | _, err := w.Write(c.pre) 105 | require.NoError(t, err) 106 | bi, err := WriteBoxInfo(w, c.bi) 107 | if !c.hasError { 108 | require.NoError(t, err) 109 | assert.Equal(t, c.expectedBI, bi) 110 | b, err := io.ReadAll(w.Reader()) 111 | require.NoError(t, err) 112 | assert.Equal(t, c.expectedBytes, b) 113 | } else { 114 | assert.Error(t, err) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | func TestReadBoxInfo(t *testing.T) { 121 | testCases := []struct { 122 | name string 123 | buf []byte 124 | seek int64 125 | hasError bool 126 | expected *BoxInfo 127 | }{ 128 | { 129 | name: "no offset", 130 | buf: []byte{ 131 | 0x00, 0x01, 0x23, 0x45, 132 | 't', 'e', 's', 't', 133 | }, 134 | expected: &BoxInfo{ 135 | Size: 0x12345, 136 | HeaderSize: 8, 137 | Type: StrToBoxType("test"), 138 | }, 139 | }, 140 | { 141 | name: "has offset", 142 | buf: []byte{ 143 | 0x00, 0x00, 144 | 0x00, 0x01, 0x23, 0x45, 145 | 't', 'e', 's', 't', 146 | }, 147 | seek: 2, 148 | expected: &BoxInfo{ 149 | Offset: 2, 150 | Size: 0x12345, 151 | HeaderSize: 8, 152 | Type: StrToBoxType("test"), 153 | }, 154 | }, 155 | { 156 | name: "large-size", 157 | buf: []byte{ 158 | 0x00, 0x00, 0x00, 0x01, 159 | 't', 'e', 's', 't', 160 | 0x01, 0x23, 0x45, 0x67, 161 | 0x89, 0xab, 0xcd, 0xef, 162 | }, 163 | expected: &BoxInfo{ 164 | Size: 0x123456789abcdef, 165 | HeaderSize: 16, 166 | Type: StrToBoxType("test"), 167 | }, 168 | }, 169 | { 170 | name: "extend to eof", 171 | buf: []byte{ 172 | 0x00, 0x00, 0x00, 0x00, 173 | 't', 'e', 's', 't', 174 | 0x00, 0x00, 0x00, 0x00, 175 | 0x00, 0x00, 0x00, 0x00, 176 | 0x00, 0x00, 0x00, 0x00, 177 | }, 178 | expected: &BoxInfo{ 179 | Size: 20, 180 | HeaderSize: 8, 181 | Type: StrToBoxType("test"), 182 | ExtendToEOF: true, 183 | }, 184 | }, 185 | { 186 | name: "end-of-file", 187 | buf: []byte{ 188 | 0x00, 0x01, 0x23, 0x45, 189 | 't', 'e', 's', 190 | }, 191 | hasError: true, 192 | }, 193 | } 194 | 195 | for _, c := range testCases { 196 | t.Run(c.name, func(t *testing.T) { 197 | buf := bytes.NewReader(c.buf) 198 | buf.Seek(c.seek, io.SeekStart) 199 | bi, err := ReadBoxInfo(buf) 200 | if !c.hasError { 201 | require.NoError(t, err) 202 | assert.Equal(t, c.expected, bi) 203 | } else { 204 | assert.Error(t, err) 205 | } 206 | }) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /cmd/mp4tool/internal/dump/dump.go: -------------------------------------------------------------------------------- 1 | package dump 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | 11 | "github.com/abema/go-mp4" 12 | "github.com/abema/go-mp4/cmd/mp4tool/internal/util" 13 | "github.com/sunfish-shogi/bufseekio" 14 | "golang.org/x/term" 15 | ) 16 | 17 | const ( 18 | indentSize = 2 19 | blockSize = 128 * 1024 20 | blockHistorySize = 4 21 | ) 22 | 23 | var terminalWidth = 180 24 | 25 | func init() { 26 | if width, _, err := term.GetSize(int(os.Stdin.Fd())); err == nil { 27 | terminalWidth = width 28 | } 29 | } 30 | 31 | func Main(args []string) int { 32 | flagSet := flag.NewFlagSet("dump", flag.ExitOnError) 33 | full := flagSet.String("full", "", "Show full content of specified box types\nFor example: -full free,ctts,stts") 34 | showAll := flagSet.Bool("a", false, "Show full content of boxes excepting mdat, free and styp") 35 | mdat := flagSet.Bool("mdat", false, "Deprecated: use \"-full mdat\"") 36 | free := flagSet.Bool("free", false, "Deprecated: use \"-full free,styp\"") 37 | offset := flagSet.Bool("offset", false, "Show offset of box") 38 | hex := flagSet.Bool("hex", false, "Use hex for size and offset") 39 | flagSet.Usage = func() { 40 | println("USAGE: mp4tool dump [OPTIONS] INPUT.mp4") 41 | flagSet.PrintDefaults() 42 | } 43 | flagSet.Parse(args) 44 | 45 | if len(flagSet.Args()) < 1 { 46 | flagSet.Usage() 47 | return 1 48 | } 49 | 50 | fpath := flagSet.Args()[0] 51 | 52 | fmap := make(map[string]struct{}) 53 | for _, tname := range strings.Split(*full, ",") { 54 | fmap[tname] = struct{}{} 55 | } 56 | if *mdat { 57 | fmap["mdat"] = struct{}{} 58 | } 59 | if *free { 60 | fmap["free"] = struct{}{} 61 | fmap["skip"] = struct{}{} 62 | } 63 | 64 | m := &mp4dump{ 65 | full: fmap, 66 | showAll: *showAll, 67 | offset: *offset, 68 | hex: *hex, 69 | } 70 | err := m.dumpFile(fpath) 71 | if err != nil { 72 | fmt.Println("Error:", err) 73 | return 1 74 | } 75 | return 0 76 | } 77 | 78 | type mp4dump struct { 79 | full map[string]struct{} 80 | showAll bool 81 | offset bool 82 | hex bool 83 | } 84 | 85 | func (m *mp4dump) dumpFile(fpath string) error { 86 | file, err := os.Open(fpath) 87 | if err != nil { 88 | return err 89 | } 90 | defer file.Close() 91 | 92 | return m.dump(bufseekio.NewReadSeeker(file, blockSize, blockHistorySize)) 93 | } 94 | 95 | func (m *mp4dump) dump(r io.ReadSeeker) error { 96 | _, err := mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) { 97 | line := bytes.NewBuffer(make([]byte, 0, terminalWidth)) 98 | 99 | printIndent(line, len(h.Path)-1) 100 | 101 | fmt.Fprintf(line, "[%s]", h.BoxInfo.Type.String()) 102 | if !h.BoxInfo.IsSupportedType() { 103 | fmt.Fprintf(line, " (unsupported box type)") 104 | } 105 | sizeFormat := "%d" 106 | if m.hex { 107 | sizeFormat = "0x%x" 108 | } 109 | if m.offset { 110 | fmt.Fprintf(line, " Offset="+sizeFormat, h.BoxInfo.Offset) 111 | } 112 | fmt.Fprintf(line, " Size="+sizeFormat, h.BoxInfo.Size) 113 | 114 | _, full := m.full[h.BoxInfo.Type.String()] 115 | if !full && 116 | (h.BoxInfo.Type == mp4.BoxTypeMdat() || 117 | h.BoxInfo.Type == mp4.BoxTypeFree() || 118 | h.BoxInfo.Type == mp4.BoxTypeSkip()) { 119 | fmt.Fprintf(line, " Data=[...] (use \"-full %s\" to show all)", h.BoxInfo.Type) 120 | fmt.Println(line.String()) 121 | return nil, nil 122 | } 123 | full = full || m.showAll 124 | 125 | // supported box type 126 | if h.BoxInfo.IsSupportedType() { 127 | if !full && h.BoxInfo.Size-h.BoxInfo.HeaderSize >= 64 && 128 | util.ShouldHasNoChildren(h.BoxInfo.Type) { 129 | fmt.Fprintf(line, " ... (use \"-full %s\" to show all)", h.BoxInfo.Type) 130 | fmt.Println(line.String()) 131 | return nil, nil 132 | } 133 | 134 | box, _, err := h.ReadPayload() 135 | if err != mp4.ErrUnsupportedBoxVersion { 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | str, err := mp4.Stringify(box, h.BoxInfo.Context) 141 | if err != nil { 142 | return nil, err 143 | } 144 | if !full && line.Len()+len(str)+2 > terminalWidth { 145 | fmt.Fprintf(line, " ... (use \"-full %s\" to show all)", h.BoxInfo.Type) 146 | } else if str != "" { 147 | fmt.Fprintf(line, " %s", str) 148 | } 149 | 150 | fmt.Println(line.String()) 151 | _, err = h.Expand() 152 | return nil, err 153 | } 154 | fmt.Fprintf(line, " (unsupported box version)") 155 | } 156 | 157 | // unsupported box type 158 | if full { 159 | buf := bytes.NewBuffer(make([]byte, 0, h.BoxInfo.Size-h.BoxInfo.HeaderSize)) 160 | if _, err := h.ReadData(buf); err != nil { 161 | return nil, err 162 | } 163 | fmt.Fprintf(line, " Data=[") 164 | for i, d := range buf.Bytes() { 165 | if i != 0 { 166 | fmt.Fprintf(line, " ") 167 | } 168 | fmt.Fprintf(line, "0x%02x", d) 169 | } 170 | fmt.Fprintf(line, "]") 171 | } else { 172 | fmt.Fprintf(line, " Data=[...] (use \"-full %s\" to show all)", h.BoxInfo.Type) 173 | } 174 | fmt.Println(line.String()) 175 | return nil, nil 176 | }) 177 | return err 178 | } 179 | 180 | func printIndent(w io.Writer, depth int) { 181 | for i := 0; i < depth*indentSize; i++ { 182 | fmt.Fprintf(w, " ") 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /box.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "math" 7 | 8 | "github.com/abema/go-mp4/internal/bitio" 9 | ) 10 | 11 | const LengthUnlimited = math.MaxUint32 12 | 13 | type ICustomFieldObject interface { 14 | // GetFieldSize returns size of dynamic field 15 | GetFieldSize(name string, ctx Context) uint 16 | 17 | // GetFieldLength returns length of dynamic field 18 | GetFieldLength(name string, ctx Context) uint 19 | 20 | // IsOptFieldEnabled check whether if the optional field is enabled 21 | IsOptFieldEnabled(name string, ctx Context) bool 22 | 23 | // StringifyField returns field value as string 24 | StringifyField(name string, indent string, depth int, ctx Context) (string, bool) 25 | 26 | IsPString(name string, bytes []byte, remainingSize uint64, ctx Context) bool 27 | 28 | BeforeUnmarshal(r io.ReadSeeker, size uint64, ctx Context) (n uint64, override bool, err error) 29 | 30 | OnReadField(name string, r bitio.ReadSeeker, leftBits uint64, ctx Context) (rbits uint64, override bool, err error) 31 | 32 | OnWriteField(name string, w bitio.Writer, ctx Context) (wbits uint64, override bool, err error) 33 | } 34 | 35 | type BaseCustomFieldObject struct { 36 | } 37 | 38 | // GetFieldSize returns size of dynamic field 39 | func (box *BaseCustomFieldObject) GetFieldSize(string, Context) uint { 40 | panic(errors.New("GetFieldSize not implemented")) 41 | } 42 | 43 | // GetFieldLength returns length of dynamic field 44 | func (box *BaseCustomFieldObject) GetFieldLength(string, Context) uint { 45 | panic(errors.New("GetFieldLength not implemented")) 46 | } 47 | 48 | // IsOptFieldEnabled check whether if the optional field is enabled 49 | func (box *BaseCustomFieldObject) IsOptFieldEnabled(string, Context) bool { 50 | return false 51 | } 52 | 53 | // StringifyField returns field value as string 54 | func (box *BaseCustomFieldObject) StringifyField(string, string, int, Context) (string, bool) { 55 | return "", false 56 | } 57 | 58 | func (*BaseCustomFieldObject) IsPString(name string, bytes []byte, remainingSize uint64, ctx Context) bool { 59 | return true 60 | } 61 | 62 | func (*BaseCustomFieldObject) BeforeUnmarshal(io.ReadSeeker, uint64, Context) (uint64, bool, error) { 63 | return 0, false, nil 64 | } 65 | 66 | func (*BaseCustomFieldObject) OnReadField(string, bitio.ReadSeeker, uint64, Context) (uint64, bool, error) { 67 | return 0, false, nil 68 | } 69 | 70 | func (*BaseCustomFieldObject) OnWriteField(string, bitio.Writer, Context) (uint64, bool, error) { 71 | return 0, false, nil 72 | } 73 | 74 | // IImmutableBox is common interface of box 75 | type IImmutableBox interface { 76 | ICustomFieldObject 77 | 78 | // GetVersion returns the box version 79 | GetVersion() uint8 80 | 81 | // GetFlags returns the flags 82 | GetFlags() uint32 83 | 84 | // CheckFlag checks the flag status 85 | CheckFlag(uint32) bool 86 | 87 | // GetType returns the BoxType 88 | GetType() BoxType 89 | } 90 | 91 | // IBox is common interface of box 92 | type IBox interface { 93 | IImmutableBox 94 | 95 | // SetVersion sets the box version 96 | SetVersion(uint8) 97 | 98 | // SetFlags sets the flags 99 | SetFlags(uint32) 100 | 101 | // AddFlag adds the flag 102 | AddFlag(uint32) 103 | 104 | // RemoveFlag removes the flag 105 | RemoveFlag(uint32) 106 | } 107 | 108 | type Box struct { 109 | BaseCustomFieldObject 110 | } 111 | 112 | // GetVersion returns the box version 113 | func (box *Box) GetVersion() uint8 { 114 | return 0 115 | } 116 | 117 | // SetVersion sets the box version 118 | func (box *Box) SetVersion(uint8) { 119 | } 120 | 121 | // GetFlags returns the flags 122 | func (box *Box) GetFlags() uint32 { 123 | return 0x000000 124 | } 125 | 126 | // CheckFlag checks the flag status 127 | func (box *Box) CheckFlag(flag uint32) bool { 128 | return true 129 | } 130 | 131 | // SetFlags sets the flags 132 | func (box *Box) SetFlags(uint32) { 133 | } 134 | 135 | // AddFlag adds the flag 136 | func (box *Box) AddFlag(flag uint32) { 137 | } 138 | 139 | // RemoveFlag removes the flag 140 | func (box *Box) RemoveFlag(flag uint32) { 141 | } 142 | 143 | // FullBox is ISOBMFF FullBox 144 | type FullBox struct { 145 | BaseCustomFieldObject 146 | Version uint8 `mp4:"0,size=8"` 147 | Flags [3]byte `mp4:"1,size=8"` 148 | } 149 | 150 | // GetVersion returns the box version 151 | func (box *FullBox) GetVersion() uint8 { 152 | return box.Version 153 | } 154 | 155 | // SetVersion sets the box version 156 | func (box *FullBox) SetVersion(version uint8) { 157 | box.Version = version 158 | } 159 | 160 | // GetFlags returns the flags 161 | func (box *FullBox) GetFlags() uint32 { 162 | flag := uint32(box.Flags[0]) << 16 163 | flag ^= uint32(box.Flags[1]) << 8 164 | flag ^= uint32(box.Flags[2]) 165 | return flag 166 | } 167 | 168 | // CheckFlag checks the flag status 169 | func (box *FullBox) CheckFlag(flag uint32) bool { 170 | return box.GetFlags()&flag != 0 171 | } 172 | 173 | // SetFlags sets the flags 174 | func (box *FullBox) SetFlags(flags uint32) { 175 | box.Flags[0] = byte(flags >> 16) 176 | box.Flags[1] = byte(flags >> 8) 177 | box.Flags[2] = byte(flags) 178 | } 179 | 180 | // AddFlag adds the flag 181 | func (box *FullBox) AddFlag(flag uint32) { 182 | box.SetFlags(box.GetFlags() | flag) 183 | } 184 | 185 | // RemoveFlag removes the flag 186 | func (box *FullBox) RemoveFlag(flag uint32) { 187 | box.SetFlags(box.GetFlags() & (^flag)) 188 | } 189 | -------------------------------------------------------------------------------- /box_types_iso14496_14_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBoxTypesISO14496_14(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | src IImmutableBox 16 | dst IBox 17 | bin []byte 18 | str string 19 | ctx Context 20 | }{ 21 | { 22 | name: "esds", 23 | src: &Esds{ 24 | FullBox: FullBox{ 25 | Version: 0, 26 | Flags: [3]byte{0x00, 0x00, 0x00}, 27 | }, 28 | Descriptors: []Descriptor{ 29 | { 30 | Tag: ESDescrTag, 31 | Size: 0x1234567, 32 | ESDescriptor: &ESDescriptor{ 33 | ESID: 0x1234, 34 | StreamDependenceFlag: true, 35 | UrlFlag: false, 36 | OcrStreamFlag: true, 37 | StreamPriority: 0x03, 38 | DependsOnESID: 0x2345, 39 | OCRESID: 0x3456, 40 | }, 41 | }, 42 | { 43 | Tag: ESDescrTag, 44 | Size: 0x1234567, 45 | ESDescriptor: &ESDescriptor{ 46 | ESID: 0x1234, 47 | StreamDependenceFlag: false, 48 | UrlFlag: true, 49 | OcrStreamFlag: false, 50 | StreamPriority: 0x03, 51 | URLLength: 11, 52 | URLString: []byte("http://hoge"), 53 | }, 54 | }, 55 | { 56 | Tag: DecoderConfigDescrTag, 57 | Size: 0x1234567, 58 | DecoderConfigDescriptor: &DecoderConfigDescriptor{ 59 | ObjectTypeIndication: 0x12, 60 | StreamType: 0x15, 61 | UpStream: true, 62 | Reserved: false, 63 | BufferSizeDB: 0x123456, 64 | MaxBitrate: 0x12345678, 65 | AvgBitrate: 0x23456789, 66 | }, 67 | }, 68 | { 69 | Tag: DecSpecificInfoTag, 70 | Size: 0x03, 71 | Data: []byte{0x11, 0x22, 0x33}, 72 | }, 73 | { 74 | Tag: SLConfigDescrTag, 75 | Size: 0x05, 76 | Data: []byte{0x11, 0x22, 0x33, 0x44, 0x55}, 77 | }, 78 | }, 79 | }, 80 | dst: &Esds{}, 81 | bin: []byte{ 82 | 0, // version 83 | 0x00, 0x00, 0x00, // flags 84 | // 85 | 0x03, // tag 86 | 0x89, 0x8d, 0x8a, 0x67, // size (varint) 87 | 0x12, 0x34, // esid 88 | 0xa3, // flags & streamPriority 89 | 0x23, 0x45, // dependsOnESID 90 | 0x34, 0x56, // ocresid 91 | // 92 | 0x03, // tag 93 | 0x89, 0x8d, 0x8a, 0x67, // size (varint) 94 | 0x12, 0x34, // esid 95 | 0x43, // flags & streamPriority 96 | 11, // urlLength 97 | 'h', 't', 't', 'p', ':', '/', '/', 'h', 'o', 'g', 'e', // urlString 98 | // 99 | 0x04, // tag 100 | 0x89, 0x8d, 0x8a, 0x67, // size (varint) 101 | 0x12, // objectTypeIndication 102 | 0x56, // streamType & upStream & reserved 103 | 0x12, 0x34, 0x56, // bufferSizeDB 104 | 0x12, 0x34, 0x56, 0x78, // maxBitrate 105 | 0x23, 0x45, 0x67, 0x89, // avgBitrate 106 | // 107 | 0x05, // tag 108 | 0x80, 0x80, 0x80, 0x03, // size (varint) 109 | 0x11, 0x22, 0x33, // data 110 | // 111 | 0x06, // tag 112 | 0x80, 0x80, 0x80, 0x05, // size (varint) 113 | 0x11, 0x22, 0x33, 0x44, 0x55, // data 114 | }, 115 | str: `Version=0 Flags=0x000000 Descriptors=[` + 116 | `{Tag=ESDescr Size=19088743 ESID=4660 StreamDependenceFlag=true UrlFlag=false OcrStreamFlag=true StreamPriority=3 DependsOnESID=9029 OCRESID=13398}, ` + 117 | `{Tag=ESDescr Size=19088743 ESID=4660 StreamDependenceFlag=false UrlFlag=true OcrStreamFlag=false StreamPriority=3 URLLength=0xb URLString="http://hoge"}, ` + 118 | `{Tag=DecoderConfigDescr Size=19088743 ObjectTypeIndication=0x12 StreamType=21 UpStream=true Reserved=false BufferSizeDB=1193046 MaxBitrate=305419896 AvgBitrate=591751049}, ` + 119 | "{Tag=DecSpecificInfo Size=3 Data=[0x11, 0x22, 0x33]}, " + 120 | "{Tag=SLConfigDescr Size=5 Data=[0x11, 0x22, 0x33, 0x44, 0x55]}]", 121 | }, 122 | } 123 | for _, tc := range testCases { 124 | t.Run(tc.name, func(t *testing.T) { 125 | // Marshal 126 | buf := bytes.NewBuffer(nil) 127 | n, err := Marshal(buf, tc.src, tc.ctx) 128 | require.NoError(t, err) 129 | assert.Equal(t, uint64(len(tc.bin)), n) 130 | assert.Equal(t, tc.bin, buf.Bytes()) 131 | 132 | // Unmarshal 133 | r := bytes.NewReader(tc.bin) 134 | n, err = Unmarshal(r, uint64(len(tc.bin)), tc.dst, tc.ctx) 135 | require.NoError(t, err) 136 | assert.Equal(t, uint64(buf.Len()), n) 137 | assert.Equal(t, tc.src, tc.dst) 138 | s, err := r.Seek(0, io.SeekCurrent) 139 | require.NoError(t, err) 140 | assert.Equal(t, int64(buf.Len()), s) 141 | 142 | // UnmarshalAny 143 | dst, n, err := UnmarshalAny(bytes.NewReader(tc.bin), tc.src.GetType(), uint64(len(tc.bin)), tc.ctx) 144 | require.NoError(t, err) 145 | assert.Equal(t, uint64(buf.Len()), n) 146 | assert.Equal(t, tc.src, dst) 147 | s, err = r.Seek(0, io.SeekCurrent) 148 | require.NoError(t, err) 149 | assert.Equal(t, int64(buf.Len()), s) 150 | 151 | // Stringify 152 | str, err := Stringify(tc.src, tc.ctx) 153 | require.NoError(t, err) 154 | assert.Equal(t, tc.str, str) 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-mp4 2 | ------ 3 | 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/abema/go-mp4.svg)](https://pkg.go.dev/github.com/abema/go-mp4) 5 | ![Test](https://github.com/abema/go-mp4/actions/workflows/test.yml/badge.svg) 6 | [![Coverage Status](https://coveralls.io/repos/github/abema/go-mp4/badge.svg)](https://coveralls.io/github/abema/go-mp4) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/abema/go-mp4)](https://goreportcard.com/report/github.com/abema/go-mp4) 8 | 9 | go-mp4 is Go library which provides low-level I/O interfaces of MP4. 10 | This library supports you to parse or build any MP4 boxes(atoms) directly. 11 | 12 | go-mp4 provides very flexible interfaces for reading boxes. 13 | If you want to read only specific parts of MP4 file, this library extracts those boxes via io.ReadSeeker interface. 14 | 15 | On the other hand, this library is not suitable for complex data conversions. 16 | 17 | ## Integration with your Go application 18 | 19 | ### Reading 20 | 21 | Using [ReadBoxStructure](https://pkg.go.dev/github.com/abema/go-mp4#ReadBoxStructure) or [ReadBoxStructureFromInternal](https://pkg.go.dev/github.com/abema/go-mp4#ReadBoxStructureFromInternal), you can scan box(atom) tree by depth-first order. 22 | 23 | ```go 24 | // expand all boxes 25 | _, err := mp4.ReadBoxStructure(file, func(h *mp4.ReadHandle) (interface{}, error) { 26 | fmt.Println("depth", len(h.Path)) 27 | 28 | // Box Type (e.g. "mdhd", "tfdt", "mdat") 29 | fmt.Println("type", h.BoxInfo.Type.String()) 30 | 31 | // Box Size 32 | fmt.Println("size", h.BoxInfo.Size) 33 | 34 | if h.BoxInfo.IsSupportedType() { 35 | // Payload 36 | box, _, err := h.ReadPayload() 37 | if err != nil { 38 | return nil, err 39 | } 40 | str, err := mp4.Stringify(box, h.BoxInfo.Context) 41 | if err != nil { 42 | return nil, err 43 | } 44 | fmt.Println("payload", str) 45 | 46 | // Expands children 47 | return h.Expand() 48 | } 49 | return nil, nil 50 | }) 51 | ``` 52 | 53 | [ExtractBox](https://pkg.go.dev/github.com/abema/go-mp4#ExtractBox), [ExtractBoxes](https://pkg.go.dev/github.com/abema/go-mp4#ExtractBoxes), [ExtractBoxWithPayload](https://pkg.go.dev/github.com/abema/go-mp4#ExtractBoxWithPayload), [ExtractBoxesWithPayload](https://pkg.go.dev/github.com/abema/go-mp4#ExtractBoxesWithPayload), and [Probe](https://pkg.go.dev/github.com/abema/go-mp4#Probe) are wrapper functions of ReadBoxStructure. 54 | 55 | ```go 56 | // extract specific boxes 57 | boxes, err := mp4.ExtractBoxWithPayload(file, nil, mp4.BoxPath{mp4.BoxTypeMoov(), mp4.BoxTypeTrak(), mp4.BoxTypeTkhd()}) 58 | if err != nil { 59 | : 60 | } 61 | for _, box := range boxes { 62 | tkhd := box.Payload.(*mp4.Tkhd) 63 | fmt.Println("track ID:", tkhd.TrackID) 64 | } 65 | ``` 66 | 67 | ```go 68 | // get basic informations 69 | info, err := mp4.Probe(bufseekio.NewReadSeeker(file, 1024, 4)) 70 | if err != nil { 71 | : 72 | } 73 | fmt.Println("track num:", len(info.Tracks)) 74 | ``` 75 | 76 | ### Writing 77 | 78 | Writer helps you to write box tree. 79 | The following sample code edits emsg box and writes to another file. 80 | 81 | ```go 82 | r := bufseekio.NewReadSeeker(inputFile, 128*1024, 4) 83 | w := mp4.NewWriter(outputFile) 84 | _, err = mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) { 85 | switch h.BoxInfo.Type { 86 | case mp4.BoxTypeEmsg(): 87 | // write box size and box type 88 | _, err := w.StartBox(&h.BoxInfo) 89 | if err != nil { 90 | return nil, err 91 | } 92 | // read payload 93 | box, _, err := h.ReadPayload() 94 | if err != nil { 95 | return nil, err 96 | } 97 | // update MessageData 98 | emsg := box.(*mp4.Emsg) 99 | emsg.MessageData = []byte("hello world") 100 | // write box playload 101 | if _, err := mp4.Marshal(w, emsg, h.BoxInfo.Context); err != nil { 102 | return nil, err 103 | } 104 | // rewrite box size 105 | _, err = w.EndBox() 106 | return nil, err 107 | default: 108 | // copy all 109 | return nil, w.CopyBox(r, &h.BoxInfo) 110 | } 111 | }) 112 | ``` 113 | 114 | Please note that the above sample code doesn't work for some MP4 files. 115 | If your MP4 files include specific box types (ex. stco, mfra), you should update those when offsets of mdat box changed. 116 | Next sample code adds an metadata box and updates chunk offsets in stco boxes. 117 | 118 | [Sample Code: Insert Metadata and Update stco box](https://gist.github.com/sunfish-shogi/cccde016a38c66d32c07a0234368804e) 119 | 120 | ### User-defined Boxes 121 | 122 | You can create additional box definition as follows: 123 | 124 | ```go 125 | func BoxTypeXxxx() BoxType { return mp4.StrToBoxType("xxxx") } 126 | 127 | func init() { 128 | mp4.AddBoxDef(&Xxxx{}, 0) 129 | } 130 | 131 | type Xxxx struct { 132 | FullBox `mp4:"0,extend"` 133 | UI32 uint32 `mp4:"1,size=32"` 134 | ByteArray []byte `mp4:"2,size=8,len=dynamic"` 135 | } 136 | 137 | func (*Xxxx) GetType() BoxType { 138 | return BoxTypeXxxx() 139 | } 140 | ``` 141 | 142 | ### Buffering 143 | 144 | go-mp4 has no buffering feature for I/O. 145 | If you should reduce Read function calls, you can wrap the io.ReadSeeker by [bufseekio](https://github.com/sunfish-shogi/bufseekio). 146 | 147 | ## Command Line Tool 148 | 149 | Install mp4tool as follows: 150 | 151 | ```sh 152 | go install github.com/abema/go-mp4/cmd/mp4tool@latest 153 | 154 | mp4tool -help 155 | ``` 156 | 157 | For example, `mp4tool dump MP4_FILE_NAME` command prints MP4 box tree as follows: 158 | 159 | ``` 160 | [moof] Size=504 161 | [mfhd] Size=16 Version=0 Flags=0x000000 SequenceNumber=1 162 | [traf] Size=480 163 | [tfhd] Size=28 Version=0 Flags=0x020038 TrackID=1 DefaultSampleDuration=9000 DefaultSampleSize=33550 DefaultSampleFlags=0x1010000 164 | [tfdt] Size=20 Version=1 Flags=0x000000 BaseMediaDecodeTimeV1=0 165 | [trun] Size=424 ... (use -a option to show all) 166 | [mdat] Size=44569 Data=[...] (use -mdat option to expand) 167 | ``` 168 | -------------------------------------------------------------------------------- /read.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | type BoxPath []BoxType 10 | 11 | func (lhs BoxPath) compareWith(rhs BoxPath) (forwardMatch bool, match bool) { 12 | if len(lhs) > len(rhs) { 13 | return false, false 14 | } 15 | for i := 0; i < len(lhs); i++ { 16 | if !lhs[i].MatchWith(rhs[i]) { 17 | return false, false 18 | } 19 | } 20 | if len(lhs) < len(rhs) { 21 | return true, false 22 | } 23 | return false, true 24 | } 25 | 26 | type ReadHandle struct { 27 | Params []interface{} 28 | BoxInfo BoxInfo 29 | Path BoxPath 30 | ReadPayload func() (box IBox, n uint64, err error) 31 | ReadData func(io.Writer) (n uint64, err error) 32 | Expand func(params ...interface{}) (vals []interface{}, err error) 33 | } 34 | 35 | type ReadHandler func(handle *ReadHandle) (val interface{}, err error) 36 | 37 | func ReadBoxStructure(r io.ReadSeeker, handler ReadHandler, params ...interface{}) ([]interface{}, error) { 38 | if _, err := r.Seek(0, io.SeekStart); err != nil { 39 | return nil, err 40 | } 41 | return readBoxStructure(r, 0, true, nil, Context{}, handler, params) 42 | } 43 | 44 | func ReadBoxStructureFromInternal(r io.ReadSeeker, bi *BoxInfo, handler ReadHandler, params ...interface{}) (interface{}, error) { 45 | return readBoxStructureFromInternal(r, bi, nil, handler, params) 46 | } 47 | 48 | func readBoxStructureFromInternal(r io.ReadSeeker, bi *BoxInfo, path BoxPath, handler ReadHandler, params []interface{}) (interface{}, error) { 49 | if _, err := bi.SeekToPayload(r); err != nil { 50 | return nil, err 51 | } 52 | 53 | // check comatible-brands 54 | if len(path) == 0 && bi.Type == BoxTypeFtyp() { 55 | var ftyp Ftyp 56 | if _, err := Unmarshal(r, bi.Size-bi.HeaderSize, &ftyp, bi.Context); err != nil { 57 | return nil, err 58 | } 59 | if ftyp.HasCompatibleBrand(BrandQT()) { 60 | bi.IsQuickTimeCompatible = true 61 | } 62 | if _, err := bi.SeekToPayload(r); err != nil { 63 | return nil, err 64 | } 65 | } 66 | 67 | // parse numbered ilst items after keys box by saving EntryCount field to context 68 | if bi.Type == BoxTypeKeys() { 69 | var keys Keys 70 | if _, err := Unmarshal(r, bi.Size-bi.HeaderSize, &keys, bi.Context); err != nil { 71 | return nil, err 72 | } 73 | bi.QuickTimeKeysMetaEntryCount = int(keys.EntryCount) 74 | if _, err := bi.SeekToPayload(r); err != nil { 75 | return nil, err 76 | } 77 | } 78 | 79 | ctx := bi.Context 80 | if bi.Type == BoxTypeWave() { 81 | ctx.UnderWave = true 82 | } else if bi.Type == BoxTypeIlst() { 83 | ctx.UnderIlst = true 84 | } else if bi.UnderIlst && !bi.UnderIlstMeta && IsIlstMetaBoxType(bi.Type) { 85 | ctx.UnderIlstMeta = true 86 | if bi.Type == StrToBoxType("----") { 87 | ctx.UnderIlstFreeMeta = true 88 | } 89 | } else if bi.Type == BoxTypeUdta() { 90 | ctx.UnderUdta = true 91 | } 92 | 93 | newPath := make(BoxPath, len(path)+1) 94 | copy(newPath, path) 95 | newPath[len(path)] = bi.Type 96 | 97 | h := &ReadHandle{ 98 | Params: params, 99 | BoxInfo: *bi, 100 | Path: newPath, 101 | } 102 | 103 | var childrenOffset uint64 104 | 105 | h.ReadPayload = func() (IBox, uint64, error) { 106 | if _, err := bi.SeekToPayload(r); err != nil { 107 | return nil, 0, err 108 | } 109 | 110 | box, n, err := UnmarshalAny(r, bi.Type, bi.Size-bi.HeaderSize, bi.Context) 111 | if err != nil { 112 | return nil, 0, err 113 | } 114 | childrenOffset = bi.Offset + bi.HeaderSize + n 115 | return box, n, nil 116 | } 117 | 118 | h.ReadData = func(w io.Writer) (uint64, error) { 119 | if _, err := bi.SeekToPayload(r); err != nil { 120 | return 0, err 121 | } 122 | 123 | size := bi.Size - bi.HeaderSize 124 | if _, err := io.CopyN(w, r, int64(size)); err != nil { 125 | return 0, err 126 | } 127 | return size, nil 128 | } 129 | 130 | h.Expand = func(params ...interface{}) ([]interface{}, error) { 131 | if childrenOffset == 0 { 132 | if _, err := bi.SeekToPayload(r); err != nil { 133 | return nil, err 134 | } 135 | 136 | _, n, err := UnmarshalAny(r, bi.Type, bi.Size-bi.HeaderSize, bi.Context) 137 | if err != nil { 138 | return nil, err 139 | } 140 | childrenOffset = bi.Offset + bi.HeaderSize + n 141 | } else { 142 | if _, err := r.Seek(int64(childrenOffset), io.SeekStart); err != nil { 143 | return nil, err 144 | } 145 | } 146 | 147 | childrenSize := bi.Offset + bi.Size - childrenOffset 148 | return readBoxStructure(r, childrenSize, false, newPath, ctx, handler, params) 149 | } 150 | 151 | if val, err := handler(h); err != nil { 152 | return nil, err 153 | } else if _, err := bi.SeekToEnd(r); err != nil { 154 | return nil, err 155 | } else { 156 | return val, nil 157 | } 158 | } 159 | 160 | func readBoxStructure(r io.ReadSeeker, totalSize uint64, isRoot bool, path BoxPath, ctx Context, handler ReadHandler, params []interface{}) ([]interface{}, error) { 161 | vals := make([]interface{}, 0, 8) 162 | 163 | for isRoot || totalSize >= SmallHeaderSize { 164 | bi, err := ReadBoxInfo(r) 165 | if isRoot && err == io.EOF { 166 | return vals, nil 167 | } else if err != nil { 168 | return nil, err 169 | } 170 | 171 | if !isRoot && bi.Size > totalSize { 172 | return nil, fmt.Errorf("too large box size: type=%s, size=%d, actualBufSize=%d", bi.Type.String(), bi.Size, totalSize) 173 | } 174 | totalSize -= bi.Size 175 | 176 | bi.Context = ctx 177 | 178 | val, err := readBoxStructureFromInternal(r, bi, path, handler, params) 179 | if err != nil { 180 | return nil, err 181 | } 182 | vals = append(vals, val) 183 | 184 | if bi.IsQuickTimeCompatible { 185 | ctx.IsQuickTimeCompatible = true 186 | } 187 | 188 | // preserve keys entry count on context for subsequent ilst number item box 189 | if bi.Type == BoxTypeKeys() { 190 | ctx.QuickTimeKeysMetaEntryCount = bi.QuickTimeKeysMetaEntryCount 191 | } 192 | } 193 | 194 | if totalSize != 0 && !ctx.IsQuickTimeCompatible { 195 | return nil, errors.New("unexpected EOF") 196 | } 197 | 198 | return vals, nil 199 | } 200 | -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "strconv" 9 | 10 | "github.com/abema/go-mp4/internal/util" 11 | ) 12 | 13 | type stringifier struct { 14 | buf *bytes.Buffer 15 | src IImmutableBox 16 | indent string 17 | ctx Context 18 | } 19 | 20 | func Stringify(src IImmutableBox, ctx Context) (string, error) { 21 | return StringifyWithIndent(src, "", ctx) 22 | } 23 | 24 | func StringifyWithIndent(src IImmutableBox, indent string, ctx Context) (string, error) { 25 | boxDef := src.GetType().getBoxDef(ctx) 26 | if boxDef == nil { 27 | return "", ErrBoxInfoNotFound 28 | } 29 | 30 | v := reflect.ValueOf(src).Elem() 31 | 32 | m := &stringifier{ 33 | buf: bytes.NewBuffer(nil), 34 | src: src, 35 | indent: indent, 36 | ctx: ctx, 37 | } 38 | 39 | err := m.stringifyStruct(v, boxDef.fields, 0, true) 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | return m.buf.String(), nil 45 | } 46 | 47 | func (m *stringifier) stringify(v reflect.Value, fi *fieldInstance, depth int) error { 48 | switch v.Type().Kind() { 49 | case reflect.Ptr: 50 | return m.stringifyPtr(v, fi, depth) 51 | case reflect.Struct: 52 | return m.stringifyStruct(v, fi.children, depth, fi.is(fieldExtend)) 53 | case reflect.Array: 54 | return m.stringifyArray(v, fi, depth) 55 | case reflect.Slice: 56 | return m.stringifySlice(v, fi, depth) 57 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 58 | return m.stringifyInt(v, fi, depth) 59 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 60 | return m.stringifyUint(v, fi, depth) 61 | case reflect.Bool: 62 | return m.stringifyBool(v, depth) 63 | case reflect.String: 64 | return m.stringifyString(v, depth) 65 | default: 66 | return fmt.Errorf("unsupported type: %s", v.Type().Kind()) 67 | } 68 | } 69 | 70 | func (m *stringifier) stringifyPtr(v reflect.Value, fi *fieldInstance, depth int) error { 71 | return m.stringify(v.Elem(), fi, depth) 72 | } 73 | 74 | func (m *stringifier) stringifyStruct(v reflect.Value, fs []*field, depth int, extended bool) error { 75 | if !extended { 76 | m.buf.WriteString("{") 77 | if m.indent != "" { 78 | m.buf.WriteString("\n") 79 | } 80 | depth++ 81 | } 82 | 83 | for _, f := range fs { 84 | fi := resolveFieldInstance(f, m.src, v, m.ctx) 85 | 86 | if !isTargetField(m.src, fi, m.ctx) { 87 | continue 88 | } 89 | 90 | if f.cnst != "" || f.is(fieldHidden) { 91 | continue 92 | } 93 | 94 | if !f.is(fieldExtend) { 95 | if m.indent != "" { 96 | writeIndent(m.buf, m.indent, depth+1) 97 | } else if m.buf.Len() != 0 && m.buf.Bytes()[m.buf.Len()-1] != '{' { 98 | m.buf.WriteString(" ") 99 | } 100 | m.buf.WriteString(f.name) 101 | m.buf.WriteString("=") 102 | } 103 | 104 | str, ok := fi.cfo.StringifyField(f.name, m.indent, depth+1, m.ctx) 105 | if ok { 106 | m.buf.WriteString(str) 107 | if !f.is(fieldExtend) && m.indent != "" { 108 | m.buf.WriteString("\n") 109 | } 110 | continue 111 | } 112 | 113 | if f.name == "Version" { 114 | m.buf.WriteString(strconv.Itoa(int(m.src.GetVersion()))) 115 | } else if f.name == "Flags" { 116 | fmt.Fprintf(m.buf, "0x%06x", m.src.GetFlags()) 117 | } else { 118 | err := m.stringify(v.FieldByName(f.name), fi, depth) 119 | if err != nil { 120 | return err 121 | } 122 | } 123 | 124 | if !f.is(fieldExtend) && m.indent != "" { 125 | m.buf.WriteString("\n") 126 | } 127 | } 128 | 129 | if !extended { 130 | if m.indent != "" { 131 | writeIndent(m.buf, m.indent, depth) 132 | } 133 | m.buf.WriteString("}") 134 | } 135 | 136 | return nil 137 | } 138 | 139 | func (m *stringifier) stringifyArray(v reflect.Value, fi *fieldInstance, depth int) error { 140 | begin, sep, end := "[", ", ", "]" 141 | if fi.is(fieldString) || fi.is(fieldISO639_2) { 142 | begin, sep, end = "\"", "", "\"" 143 | } else if fi.is(fieldUUID) { 144 | begin, sep, end = "", "", "" 145 | } 146 | 147 | m.buf.WriteString(begin) 148 | 149 | m2 := *m 150 | if fi.is(fieldString) { 151 | m2.buf = bytes.NewBuffer(nil) 152 | } 153 | size := v.Type().Size() 154 | for i := 0; i < int(size)/int(v.Type().Elem().Size()); i++ { 155 | if i != 0 { 156 | m2.buf.WriteString(sep) 157 | } 158 | 159 | if err := m2.stringify(v.Index(i), fi, depth+1); err != nil { 160 | return err 161 | } 162 | 163 | if fi.is(fieldUUID) && (i == 3 || i == 5 || i == 7 || i == 9) { 164 | m.buf.WriteString("-") 165 | } 166 | } 167 | if fi.is(fieldString) { 168 | m.buf.WriteString(util.EscapeUnprintables(m2.buf.String())) 169 | } 170 | 171 | m.buf.WriteString(end) 172 | 173 | return nil 174 | } 175 | 176 | func (m *stringifier) stringifySlice(v reflect.Value, fi *fieldInstance, depth int) error { 177 | begin, sep, end := "[", ", ", "]" 178 | if fi.is(fieldString) || fi.is(fieldISO639_2) { 179 | begin, sep, end = "\"", "", "\"" 180 | } 181 | 182 | m.buf.WriteString(begin) 183 | 184 | m2 := *m 185 | if fi.is(fieldString) { 186 | m2.buf = bytes.NewBuffer(nil) 187 | } 188 | for i := 0; i < v.Len(); i++ { 189 | if fi.length != LengthUnlimited && uint(i) >= fi.length { 190 | break 191 | } 192 | 193 | if i != 0 { 194 | m2.buf.WriteString(sep) 195 | } 196 | 197 | if err := m2.stringify(v.Index(i), fi, depth+1); err != nil { 198 | return err 199 | } 200 | } 201 | if fi.is(fieldString) { 202 | m.buf.WriteString(util.EscapeUnprintables(m2.buf.String())) 203 | } 204 | 205 | m.buf.WriteString(end) 206 | 207 | return nil 208 | } 209 | 210 | func (m *stringifier) stringifyInt(v reflect.Value, fi *fieldInstance, depth int) error { 211 | if fi.is(fieldHex) { 212 | val := v.Int() 213 | if val >= 0 { 214 | m.buf.WriteString("0x") 215 | m.buf.WriteString(strconv.FormatInt(val, 16)) 216 | } else { 217 | m.buf.WriteString("-0x") 218 | m.buf.WriteString(strconv.FormatInt(-val, 16)) 219 | } 220 | } else { 221 | m.buf.WriteString(strconv.FormatInt(v.Int(), 10)) 222 | } 223 | return nil 224 | } 225 | 226 | func (m *stringifier) stringifyUint(v reflect.Value, fi *fieldInstance, depth int) error { 227 | if fi.is(fieldISO639_2) { 228 | m.buf.WriteString(string([]byte{byte(v.Uint() + 0x60)})) 229 | } else if fi.is(fieldUUID) { 230 | fmt.Fprintf(m.buf, "%02x", v.Uint()) 231 | } else if fi.is(fieldString) { 232 | m.buf.WriteString(string([]byte{byte(v.Uint())})) 233 | } else if fi.is(fieldHex) || (!fi.is(fieldDec) && v.Type().Kind() == reflect.Uint8) || v.Type().Kind() == reflect.Uintptr { 234 | m.buf.WriteString("0x") 235 | m.buf.WriteString(strconv.FormatUint(v.Uint(), 16)) 236 | } else { 237 | m.buf.WriteString(strconv.FormatUint(v.Uint(), 10)) 238 | } 239 | 240 | return nil 241 | } 242 | 243 | func (m *stringifier) stringifyBool(v reflect.Value, depth int) error { 244 | m.buf.WriteString(strconv.FormatBool(v.Bool())) 245 | 246 | return nil 247 | } 248 | 249 | func (m *stringifier) stringifyString(v reflect.Value, depth int) error { 250 | m.buf.WriteString("\"") 251 | m.buf.WriteString(util.EscapeUnprintables(v.String())) 252 | m.buf.WriteString("\"") 253 | 254 | return nil 255 | } 256 | 257 | func writeIndent(w io.Writer, indent string, depth int) { 258 | for i := 0; i < depth; i++ { 259 | io.WriteString(w, indent) 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /extract_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestExtractBoxWithPayload(t *testing.T) { 12 | testCases := []struct { 13 | name string 14 | path BoxPath 15 | want []*BoxInfoWithPayload 16 | hasError bool 17 | }{ 18 | { 19 | name: "empty box path", 20 | path: BoxPath{}, 21 | hasError: true, 22 | }, 23 | { 24 | name: "invalid box path", 25 | path: BoxPath{BoxTypeUdta()}, 26 | want: []*BoxInfoWithPayload{}, 27 | }, 28 | { 29 | name: "top level", 30 | path: BoxPath{BoxTypeMoov()}, 31 | want: []*BoxInfoWithPayload{ 32 | { 33 | Info: BoxInfo{Offset: 6442, Size: 1836, HeaderSize: 8, Type: BoxTypeMoov()}, 34 | Payload: &Moov{}, 35 | }, 36 | }, 37 | }, 38 | { 39 | name: "multi hit", 40 | path: BoxPath{BoxTypeMoov(), BoxTypeTrak(), BoxTypeMdia(), BoxTypeHdlr()}, 41 | want: []*BoxInfoWithPayload{ 42 | { 43 | Info: BoxInfo{Offset: 6734, Size: 44, HeaderSize: 8, Type: BoxTypeHdlr()}, 44 | Payload: &Hdlr{ 45 | HandlerType: [4]byte{'v', 'i', 'd', 'e'}, 46 | Name: "VideoHandle", 47 | }, 48 | }, 49 | { 50 | Info: BoxInfo{Offset: 7477, Size: 44, HeaderSize: 8, Type: BoxTypeHdlr()}, 51 | Payload: &Hdlr{ 52 | HandlerType: [4]byte{'s', 'o', 'u', 'n'}, 53 | Name: "SoundHandle", 54 | }, 55 | }, 56 | }, 57 | }, 58 | { 59 | name: "multi hit", 60 | path: BoxPath{BoxTypeMoov(), BoxTypeTrak(), BoxTypeMdia(), BoxTypeAny()}, 61 | want: []*BoxInfoWithPayload{ 62 | { 63 | Info: BoxInfo{Offset: 6702, Size: 32, HeaderSize: 8, Type: BoxTypeMdhd()}, 64 | Payload: &Mdhd{ 65 | Timescale: 10240, 66 | DurationV0: 10240, 67 | Language: [3]byte{'e' - 0x60, 'n' - 0x60, 'g' - 0x60}, 68 | }, 69 | }, 70 | { 71 | Info: BoxInfo{Offset: 6734, Size: 44, HeaderSize: 8, Type: BoxTypeHdlr()}, 72 | Payload: &Hdlr{ 73 | HandlerType: [4]byte{'v', 'i', 'd', 'e'}, 74 | Name: "VideoHandle", 75 | }, 76 | }, 77 | { 78 | Info: BoxInfo{Offset: 6778, Size: 523, HeaderSize: 8, Type: BoxTypeMinf()}, 79 | Payload: &Minf{}, 80 | }, 81 | { 82 | Info: BoxInfo{Offset: 7445, Size: 32, HeaderSize: 8, Type: BoxTypeMdhd()}, 83 | Payload: &Mdhd{ 84 | Timescale: 44100, 85 | DurationV0: 45124, 86 | Language: [3]byte{'e' - 0x60, 'n' - 0x60, 'g' - 0x60}, 87 | }, 88 | }, 89 | { 90 | Info: BoxInfo{Offset: 7477, Size: 44, HeaderSize: 8, Type: BoxTypeHdlr()}, 91 | Payload: &Hdlr{ 92 | HandlerType: [4]byte{'s', 'o', 'u', 'n'}, 93 | Name: "SoundHandle", 94 | }, 95 | }, 96 | { 97 | Info: BoxInfo{Offset: 7521, Size: 624, HeaderSize: 8, Type: BoxTypeMinf()}, 98 | Payload: &Minf{}, 99 | }, 100 | }, 101 | }, 102 | } 103 | 104 | for _, c := range testCases { 105 | t.Run(c.name, func(t *testing.T) { 106 | f, err := os.Open("./testdata/sample.mp4") 107 | require.NoError(t, err) 108 | defer f.Close() 109 | 110 | bs, err := ExtractBoxWithPayload(f, nil, c.path) 111 | if c.hasError { 112 | require.Error(t, err) 113 | return 114 | } 115 | require.NoError(t, err) 116 | assert.Equal(t, c.want, bs) 117 | }) 118 | } 119 | } 120 | 121 | func TestExtractBox(t *testing.T) { 122 | testCases := []struct { 123 | name string 124 | path BoxPath 125 | want []*BoxInfo 126 | hasError bool 127 | }{ 128 | { 129 | name: "empty box path", 130 | path: BoxPath{}, 131 | hasError: true, 132 | }, 133 | { 134 | name: "invalid box path", 135 | path: BoxPath{BoxTypeUdta()}, 136 | want: []*BoxInfo{}, 137 | }, 138 | { 139 | name: "top level", 140 | path: BoxPath{BoxTypeMoov()}, 141 | want: []*BoxInfo{ 142 | {Offset: 6442, Size: 1836, HeaderSize: 8, Type: BoxTypeMoov()}, 143 | }, 144 | }, 145 | { 146 | name: "multi hit", 147 | path: BoxPath{BoxTypeMoov(), BoxTypeTrak(), BoxTypeTkhd()}, 148 | want: []*BoxInfo{ 149 | {Offset: 6566, Size: 92, HeaderSize: 8, Type: BoxTypeTkhd()}, 150 | {Offset: 7309, Size: 92, HeaderSize: 8, Type: BoxTypeTkhd()}, 151 | }, 152 | }, 153 | { 154 | name: "any type", 155 | path: BoxPath{BoxTypeMoov(), BoxTypeTrak(), BoxTypeAny()}, 156 | want: []*BoxInfo{ 157 | {Offset: 6566, Size: 92, HeaderSize: 8, Type: BoxTypeTkhd()}, 158 | {Offset: 6658, Size: 36, HeaderSize: 8, Type: BoxTypeEdts()}, 159 | {Offset: 6694, Size: 607, HeaderSize: 8, Type: BoxTypeMdia()}, 160 | {Offset: 7309, Size: 92, HeaderSize: 8, Type: BoxTypeTkhd()}, 161 | {Offset: 7401, Size: 36, HeaderSize: 8, Type: BoxTypeEdts()}, 162 | {Offset: 7437, Size: 708, HeaderSize: 8, Type: BoxTypeMdia()}, 163 | }, 164 | }, 165 | } 166 | 167 | for _, c := range testCases { 168 | t.Run(c.name, func(t *testing.T) { 169 | f, err := os.Open("./testdata/sample.mp4") 170 | require.NoError(t, err) 171 | defer f.Close() 172 | 173 | boxes, err := ExtractBox(f, nil, c.path) 174 | if c.hasError { 175 | require.Error(t, err) 176 | return 177 | } 178 | require.NoError(t, err) 179 | assert.Equal(t, c.want, boxes) 180 | }) 181 | } 182 | } 183 | 184 | func TestExtractBoxes(t *testing.T) { 185 | testCases := []struct { 186 | name string 187 | paths []BoxPath 188 | want []*BoxInfo 189 | hasError bool 190 | }{ 191 | { 192 | name: "empty path list", 193 | paths: []BoxPath{}, 194 | }, 195 | { 196 | name: "contains empty path", 197 | paths: []BoxPath{ 198 | {BoxTypeMoov()}, 199 | {}, 200 | }, 201 | hasError: true, 202 | }, 203 | { 204 | name: "single path", 205 | paths: []BoxPath{ 206 | {BoxTypeMoov(), BoxTypeUdta()}, 207 | }, 208 | want: []*BoxInfo{ 209 | {Offset: 8145, Size: 133, HeaderSize: 8, Type: BoxTypeUdta()}, 210 | }, 211 | }, 212 | { 213 | name: "multi path", 214 | paths: []BoxPath{ 215 | {BoxTypeMoov()}, 216 | {BoxTypeMoov(), BoxTypeUdta()}, 217 | }, 218 | want: []*BoxInfo{ 219 | {Offset: 6442, Size: 1836, HeaderSize: 8, Type: BoxTypeMoov()}, 220 | {Offset: 8145, Size: 133, HeaderSize: 8, Type: BoxTypeUdta()}, 221 | }, 222 | }, 223 | { 224 | name: "multi hit", 225 | paths: []BoxPath{ 226 | {BoxTypeMoov(), BoxTypeTrak(), BoxTypeTkhd()}, 227 | }, 228 | want: []*BoxInfo{ 229 | {Offset: 6566, Size: 92, HeaderSize: 8, Type: BoxTypeTkhd()}, 230 | {Offset: 7309, Size: 92, HeaderSize: 8, Type: BoxTypeTkhd()}, 231 | }, 232 | }, 233 | } 234 | 235 | for _, c := range testCases { 236 | t.Run(c.name, func(t *testing.T) { 237 | f, err := os.Open("./testdata/sample.mp4") 238 | require.NoError(t, err) 239 | defer f.Close() 240 | 241 | boxes, err := ExtractBoxes(f, nil, c.paths) 242 | if c.hasError { 243 | require.Error(t, err) 244 | return 245 | } 246 | 247 | require.NoError(t, err) 248 | assert.Equal(t, c.want, boxes) 249 | }) 250 | } 251 | } 252 | 253 | func TestExtractDescendantBox(t *testing.T) { 254 | f, err := os.Open("./testdata/sample.mp4") 255 | require.NoError(t, err) 256 | defer f.Close() 257 | 258 | boxes, err := ExtractBox(f, nil, BoxPath{BoxTypeMoov()}) 259 | require.NoError(t, err) 260 | require.Equal(t, 1, len(boxes)) 261 | 262 | descs, err := ExtractBox(f, boxes[0], BoxPath{BoxTypeTrak(), BoxTypeMdia()}) 263 | require.NoError(t, err) 264 | require.Equal(t, 2, len(descs)) 265 | } 266 | -------------------------------------------------------------------------------- /field.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | "reflect" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | type ( 14 | stringType uint8 15 | fieldFlag uint16 16 | ) 17 | 18 | const ( 19 | stringType_C stringType = iota 20 | stringType_C_P 21 | 22 | fieldString fieldFlag = 1 << iota // 0 23 | fieldExtend // 1 24 | fieldDec // 2 25 | fieldHex // 3 26 | fieldISO639_2 // 4 27 | fieldUUID // 5 28 | fieldHidden // 6 29 | fieldOptDynamic // 7 30 | fieldVarint // 8 31 | fieldSizeDynamic // 9 32 | fieldLengthDynamic // 10 33 | fieldBoxString // 11 - non-null-terminated string (14496-30) 34 | ) 35 | 36 | type field struct { 37 | children []*field 38 | name string 39 | cnst string 40 | order int 41 | optFlag uint32 42 | nOptFlag uint32 43 | size uint 44 | length uint 45 | flags fieldFlag 46 | strType stringType 47 | version uint8 48 | nVersion uint8 49 | } 50 | 51 | func (f *field) set(flag fieldFlag) { 52 | f.flags |= flag 53 | } 54 | 55 | func (f *field) is(flag fieldFlag) bool { 56 | return f.flags&flag != 0 57 | } 58 | 59 | func buildFields(box IImmutableBox) []*field { 60 | t := reflect.TypeOf(box).Elem() 61 | return buildFieldsStruct(t) 62 | } 63 | 64 | func buildFieldsStruct(t reflect.Type) []*field { 65 | fs := make([]*field, 0, 8) 66 | for i := 0; i < t.NumField(); i++ { 67 | ft := t.Field(i).Type 68 | tag, ok := t.Field(i).Tag.Lookup("mp4") 69 | if !ok { 70 | continue 71 | } 72 | f := buildField(t.Field(i).Name, tag) 73 | if f.is(fieldBoxString) && i != t.NumField()-1 { 74 | fmt.Fprint(os.Stderr, "go-mp4: boxstring must be the last field!!\n") 75 | } 76 | f.children = buildFieldsAny(ft) 77 | fs = append(fs, f) 78 | } 79 | sort.SliceStable(fs, func(i, j int) bool { 80 | return fs[i].order < fs[j].order 81 | }) 82 | return fs 83 | } 84 | 85 | func buildFieldsAny(t reflect.Type) []*field { 86 | switch t.Kind() { 87 | case reflect.Struct: 88 | return buildFieldsStruct(t) 89 | case reflect.Ptr, reflect.Array, reflect.Slice: 90 | return buildFieldsAny(t.Elem()) 91 | default: 92 | return nil 93 | } 94 | } 95 | 96 | func buildField(fieldName string, tag string) *field { 97 | f := &field{ 98 | name: fieldName, 99 | } 100 | tagMap := parseFieldTag(tag) 101 | for key, val := range tagMap { 102 | if val != "" { 103 | continue 104 | } 105 | if order, err := strconv.Atoi(key); err == nil { 106 | f.order = order 107 | break 108 | } 109 | } 110 | 111 | if val, contained := tagMap["string"]; contained { 112 | f.set(fieldString) 113 | if val == "c_p" { 114 | f.strType = stringType_C_P 115 | fmt.Fprint(os.Stderr, "go-mp4: string=c_p tag is deprecated!! See https://github.com/abema/go-mp4/issues/76\n") 116 | } 117 | } 118 | 119 | if _, contained := tagMap["boxstring"]; contained { 120 | f.set(fieldBoxString) 121 | } 122 | 123 | if _, contained := tagMap["varint"]; contained { 124 | f.set(fieldVarint) 125 | } 126 | 127 | if val, contained := tagMap["opt"]; contained { 128 | if val == "dynamic" { 129 | f.set(fieldOptDynamic) 130 | } else { 131 | base := 10 132 | if strings.HasPrefix(val, "0x") { 133 | val = val[2:] 134 | base = 16 135 | } 136 | opt, err := strconv.ParseUint(val, base, 32) 137 | if err != nil { 138 | panic(err) 139 | } 140 | f.optFlag = uint32(opt) 141 | } 142 | } 143 | 144 | if val, contained := tagMap["nopt"]; contained { 145 | base := 10 146 | if strings.HasPrefix(val, "0x") { 147 | val = val[2:] 148 | base = 16 149 | } 150 | nopt, err := strconv.ParseUint(val, base, 32) 151 | if err != nil { 152 | panic(err) 153 | } 154 | f.nOptFlag = uint32(nopt) 155 | } 156 | 157 | if _, contained := tagMap["extend"]; contained { 158 | f.set(fieldExtend) 159 | } 160 | 161 | if _, contained := tagMap["dec"]; contained { 162 | f.set(fieldDec) 163 | } 164 | 165 | if _, contained := tagMap["hex"]; contained { 166 | f.set(fieldHex) 167 | } 168 | 169 | if _, contained := tagMap["iso639-2"]; contained { 170 | f.set(fieldISO639_2) 171 | } 172 | 173 | if _, contained := tagMap["uuid"]; contained { 174 | f.set(fieldUUID) 175 | } 176 | 177 | if _, contained := tagMap["hidden"]; contained { 178 | f.set(fieldHidden) 179 | } 180 | 181 | if val, contained := tagMap["const"]; contained { 182 | f.cnst = val 183 | } 184 | 185 | f.version = anyVersion 186 | if val, contained := tagMap["ver"]; contained { 187 | ver, err := strconv.Atoi(val) 188 | if err != nil { 189 | panic(err) 190 | } 191 | if ver > math.MaxUint8 { 192 | panic("ver-tag must be <=255") 193 | } 194 | f.version = uint8(ver) 195 | } 196 | 197 | f.nVersion = anyVersion 198 | if val, contained := tagMap["nver"]; contained { 199 | ver, err := strconv.Atoi(val) 200 | if err != nil { 201 | panic(err) 202 | } 203 | if ver > math.MaxUint8 { 204 | panic("nver-tag must be <=255") 205 | } 206 | f.nVersion = uint8(ver) 207 | } 208 | 209 | if val, contained := tagMap["size"]; contained { 210 | if val == "dynamic" { 211 | f.set(fieldSizeDynamic) 212 | } else { 213 | size, err := strconv.ParseUint(val, 10, 32) 214 | if err != nil { 215 | panic(err) 216 | } 217 | f.size = uint(size) 218 | } 219 | } 220 | 221 | f.length = LengthUnlimited 222 | if val, contained := tagMap["len"]; contained { 223 | if val == "dynamic" { 224 | f.set(fieldLengthDynamic) 225 | } else { 226 | l, err := strconv.ParseUint(val, 10, 32) 227 | if err != nil { 228 | panic(err) 229 | } 230 | f.length = uint(l) 231 | } 232 | } 233 | 234 | return f 235 | } 236 | 237 | func parseFieldTag(str string) map[string]string { 238 | tag := make(map[string]string, 8) 239 | 240 | list := strings.Split(str, ",") 241 | for _, e := range list { 242 | kv := strings.SplitN(e, "=", 2) 243 | if len(kv) == 2 { 244 | tag[strings.Trim(kv[0], " ")] = strings.Trim(kv[1], " ") 245 | } else { 246 | tag[strings.Trim(kv[0], " ")] = "" 247 | } 248 | } 249 | 250 | return tag 251 | } 252 | 253 | type fieldInstance struct { 254 | field 255 | cfo ICustomFieldObject 256 | } 257 | 258 | func resolveFieldInstance(f *field, box IImmutableBox, parent reflect.Value, ctx Context) *fieldInstance { 259 | fi := fieldInstance{ 260 | field: *f, 261 | } 262 | 263 | cfo, ok := parent.Addr().Interface().(ICustomFieldObject) 264 | if ok { 265 | fi.cfo = cfo 266 | } else { 267 | fi.cfo = box 268 | } 269 | 270 | if fi.is(fieldSizeDynamic) { 271 | fi.size = fi.cfo.GetFieldSize(f.name, ctx) 272 | } 273 | 274 | if fi.is(fieldLengthDynamic) { 275 | fi.length = fi.cfo.GetFieldLength(f.name, ctx) 276 | } 277 | 278 | return &fi 279 | } 280 | 281 | func isTargetField(box IImmutableBox, fi *fieldInstance, ctx Context) bool { 282 | if box.GetVersion() != anyVersion { 283 | if fi.version != anyVersion && box.GetVersion() != fi.version { 284 | return false 285 | } 286 | 287 | if fi.nVersion != anyVersion && box.GetVersion() == fi.nVersion { 288 | return false 289 | } 290 | } 291 | 292 | if fi.optFlag != 0 && box.GetFlags()&fi.optFlag == 0 { 293 | return false 294 | } 295 | 296 | if fi.nOptFlag != 0 && box.GetFlags()&fi.nOptFlag != 0 { 297 | return false 298 | } 299 | 300 | if fi.is(fieldOptDynamic) && !fi.cfo.IsOptFieldEnabled(fi.name, ctx) { 301 | return false 302 | } 303 | 304 | return true 305 | } 306 | -------------------------------------------------------------------------------- /box_types_metadata.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/abema/go-mp4/internal/util" 7 | ) 8 | 9 | /*************************** ilst ****************************/ 10 | 11 | func BoxTypeIlst() BoxType { return StrToBoxType("ilst") } 12 | func BoxTypeData() BoxType { return StrToBoxType("data") } 13 | 14 | var ilstMetaBoxTypes = []BoxType{ 15 | StrToBoxType("----"), 16 | StrToBoxType("aART"), 17 | StrToBoxType("akID"), 18 | StrToBoxType("apID"), 19 | StrToBoxType("atID"), 20 | StrToBoxType("cmID"), 21 | StrToBoxType("cnID"), 22 | StrToBoxType("covr"), 23 | StrToBoxType("cpil"), 24 | StrToBoxType("cprt"), 25 | StrToBoxType("desc"), 26 | StrToBoxType("disk"), 27 | StrToBoxType("egid"), 28 | StrToBoxType("geID"), 29 | StrToBoxType("gnre"), 30 | StrToBoxType("pcst"), 31 | StrToBoxType("pgap"), 32 | StrToBoxType("plID"), 33 | StrToBoxType("purd"), 34 | StrToBoxType("purl"), 35 | StrToBoxType("rtng"), 36 | StrToBoxType("sfID"), 37 | StrToBoxType("soaa"), 38 | StrToBoxType("soal"), 39 | StrToBoxType("soar"), 40 | StrToBoxType("soco"), 41 | StrToBoxType("sonm"), 42 | StrToBoxType("sosn"), 43 | StrToBoxType("stik"), 44 | StrToBoxType("tmpo"), 45 | StrToBoxType("trkn"), 46 | StrToBoxType("tven"), 47 | StrToBoxType("tves"), 48 | StrToBoxType("tvnn"), 49 | StrToBoxType("tvsh"), 50 | StrToBoxType("tvsn"), 51 | {0xA9, 'A', 'R', 'T'}, 52 | {0xA9, 'a', 'l', 'b'}, 53 | {0xA9, 'c', 'm', 't'}, 54 | {0xA9, 'c', 'o', 'm'}, 55 | {0xA9, 'd', 'a', 'y'}, 56 | {0xA9, 'g', 'e', 'n'}, 57 | {0xA9, 'g', 'r', 'p'}, 58 | {0xA9, 'n', 'a', 'm'}, 59 | {0xA9, 't', 'o', 'o'}, 60 | {0xA9, 'w', 'r', 't'}, 61 | } 62 | 63 | func IsIlstMetaBoxType(boxType BoxType) bool { 64 | for _, bt := range ilstMetaBoxTypes { 65 | if boxType == bt { 66 | return true 67 | } 68 | } 69 | return false 70 | } 71 | 72 | func init() { 73 | AddBoxDef(&Ilst{}) 74 | AddBoxDefEx(&Data{}, isUnderIlstMeta) 75 | for _, bt := range ilstMetaBoxTypes { 76 | AddAnyTypeBoxDefEx(&IlstMetaContainer{}, bt, isIlstMetaContainer) 77 | } 78 | AddAnyTypeBoxDefEx(&StringData{}, StrToBoxType("mean"), isUnderIlstFreeFormat) 79 | AddAnyTypeBoxDefEx(&StringData{}, StrToBoxType("name"), isUnderIlstFreeFormat) 80 | } 81 | 82 | type Ilst struct { 83 | Box 84 | } 85 | 86 | // GetType returns the BoxType 87 | func (*Ilst) GetType() BoxType { 88 | return BoxTypeIlst() 89 | } 90 | 91 | type IlstMetaContainer struct { 92 | AnyTypeBox 93 | } 94 | 95 | func isIlstMetaContainer(ctx Context) bool { 96 | return ctx.UnderIlst && !ctx.UnderIlstMeta 97 | } 98 | 99 | const ( 100 | DataTypeBinary = 0 101 | DataTypeStringUTF8 = 1 102 | DataTypeStringUTF16 = 2 103 | DataTypeStringMac = 3 104 | DataTypeStringJPEG = 14 105 | DataTypeSignedIntBigEndian = 21 106 | DataTypeFloat32BigEndian = 22 107 | DataTypeFloat64BigEndian = 23 108 | ) 109 | 110 | // Data is a Value BoxType 111 | // https://developer.apple.com/documentation/quicktime-file-format/value_atom 112 | type Data struct { 113 | Box 114 | DataType uint32 `mp4:"0,size=32"` 115 | DataLang uint32 `mp4:"1,size=32"` 116 | Data []byte `mp4:"2,size=8"` 117 | } 118 | 119 | // GetType returns the BoxType 120 | func (*Data) GetType() BoxType { 121 | return BoxTypeData() 122 | } 123 | 124 | func isUnderIlstMeta(ctx Context) bool { 125 | return ctx.UnderIlstMeta 126 | } 127 | 128 | // StringifyField returns field value as string 129 | func (data *Data) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) { 130 | switch name { 131 | case "DataType": 132 | switch data.DataType { 133 | case DataTypeBinary: 134 | return "BINARY", true 135 | case DataTypeStringUTF8: 136 | return "UTF8", true 137 | case DataTypeStringUTF16: 138 | return "UTF16", true 139 | case DataTypeStringMac: 140 | return "MAC_STR", true 141 | case DataTypeStringJPEG: 142 | return "JPEG", true 143 | case DataTypeSignedIntBigEndian: 144 | return "INT", true 145 | case DataTypeFloat32BigEndian: 146 | return "FLOAT32", true 147 | case DataTypeFloat64BigEndian: 148 | return "FLOAT64", true 149 | } 150 | case "Data": 151 | switch data.DataType { 152 | case DataTypeStringUTF8: 153 | return fmt.Sprintf("\"%s\"", util.EscapeUnprintables(string(data.Data))), true 154 | } 155 | } 156 | return "", false 157 | } 158 | 159 | type StringData struct { 160 | AnyTypeBox 161 | Data []byte `mp4:"0,size=8"` 162 | } 163 | 164 | // StringifyField returns field value as string 165 | func (sd *StringData) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) { 166 | if name == "Data" { 167 | return fmt.Sprintf("\"%s\"", util.EscapeUnprintables(string(sd.Data))), true 168 | } 169 | return "", false 170 | } 171 | 172 | /*************************** numbered items ****************************/ 173 | 174 | // Item is a numbered item under an item list atom 175 | // https://developer.apple.com/documentation/quicktime-file-format/metadata_item_list_atom/item_list 176 | type Item struct { 177 | AnyTypeBox 178 | Version uint8 `mp4:"0,size=8"` 179 | Flags [3]byte `mp4:"1,size=8"` 180 | ItemName []byte `mp4:"2,size=8,len=4"` 181 | Data Data `mp4:"3"` 182 | } 183 | 184 | // StringifyField returns field value as string 185 | func (i *Item) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) { 186 | switch name { 187 | case "ItemName": 188 | return fmt.Sprintf("\"%s\"", util.EscapeUnprintables(string(i.ItemName))), true 189 | } 190 | return "", false 191 | } 192 | 193 | func isUnderIlstFreeFormat(ctx Context) bool { 194 | return ctx.UnderIlstFreeMeta 195 | } 196 | 197 | func BoxTypeKeys() BoxType { return StrToBoxType("keys") } 198 | 199 | func init() { 200 | AddBoxDef(&Keys{}) 201 | } 202 | 203 | /*************************** keys ****************************/ 204 | 205 | // Keys is the Keys BoxType 206 | // https://developer.apple.com/documentation/quicktime-file-format/metadata_item_keys_atom 207 | type Keys struct { 208 | FullBox `mp4:"0,extend"` 209 | EntryCount int32 `mp4:"1,size=32"` 210 | Entries []Key `mp4:"2,len=dynamic"` 211 | } 212 | 213 | // GetType implements the IBox interface and returns the BoxType 214 | func (*Keys) GetType() BoxType { 215 | return BoxTypeKeys() 216 | } 217 | 218 | // GetFieldLength implements the ICustomFieldObject interface and returns the length of dynamic fields 219 | func (k *Keys) GetFieldLength(name string, ctx Context) uint { 220 | switch name { 221 | case "Entries": 222 | return uint(k.EntryCount) 223 | } 224 | panic(fmt.Errorf("invalid name of dynamic-length field: boxType=keys fieldName=%s", name)) 225 | } 226 | 227 | /*************************** key ****************************/ 228 | 229 | // Key is a key value field in the Keys BoxType 230 | // https://developer.apple.com/documentation/quicktime-file-format/metadata_item_keys_atom/key_value_key_size-8 231 | type Key struct { 232 | BaseCustomFieldObject 233 | KeySize int32 `mp4:"0,size=32"` 234 | KeyNamespace []byte `mp4:"1,size=8,len=4"` 235 | KeyValue []byte `mp4:"2,size=8,len=dynamic"` 236 | } 237 | 238 | // GetFieldLength implements the ICustomFieldObject interface and returns the length of dynamic fields 239 | func (k *Key) GetFieldLength(name string, ctx Context) uint { 240 | switch name { 241 | case "KeyValue": 242 | // sizeOf(KeySize)+sizeOf(KeyNamespace) = 8 bytes 243 | return uint(k.KeySize) - 8 244 | } 245 | panic(fmt.Errorf("invalid name of dynamic-length field: boxType=key fieldName=%s", name)) 246 | } 247 | 248 | // StringifyField returns field value as string 249 | func (k *Key) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) { 250 | switch name { 251 | case "KeyNamespace": 252 | return fmt.Sprintf("\"%s\"", util.EscapeUnprintables(string(k.KeyNamespace))), true 253 | case "KeyValue": 254 | return fmt.Sprintf("\"%s\"", util.EscapeUnprintables(string(k.KeyValue))), true 255 | } 256 | return "", false 257 | } 258 | -------------------------------------------------------------------------------- /box_types_iso23001_7_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBoxTypesISO23001_7(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | src IImmutableBox 16 | dst IBox 17 | bin []byte 18 | str string 19 | ctx Context 20 | }{ 21 | { 22 | name: "pssh: version 0: no KIDs", 23 | src: &Pssh{ 24 | FullBox: FullBox{ 25 | Version: 0, 26 | Flags: [3]byte{0x00, 0x00, 0x00}, 27 | }, 28 | SystemID: [16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, 29 | DataSize: 5, 30 | Data: []byte{0x21, 0x22, 0x23, 0x24, 0x25}, 31 | }, 32 | dst: &Pssh{}, 33 | bin: []byte{ 34 | 0, // version 35 | 0x00, 0x00, 0x00, // flags 36 | // system ID 37 | 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 38 | 0x00, 0x00, 0x00, 0x05, // data size 39 | 0x21, 0x22, 0x23, 0x24, 0x25, // data 40 | }, 41 | str: `Version=0 Flags=0x000000 ` + 42 | `SystemID=01020304-0506-0708-090a-0b0c0d0e0f10 ` + 43 | `DataSize=5 ` + 44 | `Data=[0x21, 0x22, 0x23, 0x24, 0x25]`, 45 | }, 46 | { 47 | name: "pssh: version 1: with KIDs", 48 | src: &Pssh{ 49 | FullBox: FullBox{ 50 | Version: 1, 51 | Flags: [3]byte{0x00, 0x00, 0x00}, 52 | }, 53 | SystemID: [16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, 54 | KIDCount: 2, 55 | KIDs: []PsshKID{ 56 | {KID: [16]byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x10}}, 57 | {KID: [16]byte{0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x20}}, 58 | }, 59 | DataSize: 5, 60 | Data: []byte{0x21, 0x22, 0x23, 0x24, 0x25}, 61 | }, 62 | dst: &Pssh{}, 63 | bin: []byte{ 64 | 1, // version 65 | 0x00, 0x00, 0x00, // flags 66 | // system ID 67 | 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 68 | 0x00, 0x00, 0x00, 0x02, // KID count 69 | // KIDs 70 | 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x10, 71 | 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x20, 72 | 0x00, 0x00, 0x00, 0x05, // data size 73 | 0x21, 0x22, 0x23, 0x24, 0x25, // data 74 | }, 75 | str: `Version=1 Flags=0x000000 ` + 76 | `SystemID=01020304-0506-0708-090a-0b0c0d0e0f10 ` + 77 | `KIDCount=2 ` + 78 | `KIDs=[11121314-1516-1718-191a-1b1c1d1e1f10, 21222324-2526-2728-292a-2b2c2d2e2f20] ` + 79 | `DataSize=5 ` + 80 | `Data=[0x21, 0x22, 0x23, 0x24, 0x25]`, 81 | }, 82 | { 83 | name: "tenc: DefaultIsProtected=1 DefaultPerSampleIVSize=0", 84 | src: &Tenc{ 85 | FullBox: FullBox{ 86 | Version: 1, 87 | Flags: [3]byte{0x00, 0x00, 0x00}, 88 | }, 89 | Reserved: 0x00, 90 | DefaultCryptByteBlock: 0x0a, 91 | DefaultSkipByteBlock: 0x0b, 92 | DefaultIsProtected: 1, 93 | DefaultPerSampleIVSize: 0, 94 | DefaultKID: [16]byte{ 95 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 96 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 97 | }, 98 | DefaultConstantIVSize: 4, 99 | DefaultConstantIV: []byte{0x01, 0x23, 0x45, 0x67}, 100 | }, 101 | dst: &Tenc{}, 102 | bin: []byte{ 103 | 1, // version 104 | 0x00, 0x00, 0x00, // flags 105 | 0x00, // reserved 106 | 0xab, // default crypt byte block / default skip byte block 107 | 0x01, 0x00, // default is protected / default per sample iv size 108 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, // default kid 109 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 110 | 0x04, // default constant iv size 111 | 0x01, 0x23, 0x45, 0x67, // default constant iv 112 | }, 113 | str: `Version=1 Flags=0x000000 ` + 114 | `Reserved=0 ` + 115 | `DefaultCryptByteBlock=10 ` + 116 | `DefaultSkipByteBlock=11 ` + 117 | `DefaultIsProtected=1 ` + 118 | `DefaultPerSampleIVSize=0 ` + 119 | `DefaultKID=01234567-89ab-cdef-0123-456789abcdef ` + 120 | `DefaultConstantIVSize=4 ` + 121 | `DefaultConstantIV=[0x1, 0x23, 0x45, 0x67]`, 122 | }, 123 | { 124 | name: "tenc: DefaultIsProtected=0 DefaultPerSampleIVSize=0", 125 | src: &Tenc{ 126 | FullBox: FullBox{ 127 | Version: 1, 128 | Flags: [3]byte{0x00, 0x00, 0x00}, 129 | }, 130 | Reserved: 0x00, 131 | DefaultCryptByteBlock: 0x0a, 132 | DefaultSkipByteBlock: 0x0b, 133 | DefaultIsProtected: 0, 134 | DefaultPerSampleIVSize: 0, 135 | DefaultKID: [16]byte{ 136 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 137 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 138 | }, 139 | }, 140 | dst: &Tenc{}, 141 | bin: []byte{ 142 | 1, // version 143 | 0x00, 0x00, 0x00, // flags 144 | 0x00, // reserved 145 | 0xab, // default crypt byte block / default skip byte block 146 | 0x00, 0x00, // default is protected / default per sample iv size 147 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, // default kid 148 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 149 | }, 150 | str: `Version=1 Flags=0x000000 ` + 151 | `Reserved=0 ` + 152 | `DefaultCryptByteBlock=10 ` + 153 | `DefaultSkipByteBlock=11 ` + 154 | `DefaultIsProtected=0 ` + 155 | `DefaultPerSampleIVSize=0 ` + 156 | `DefaultKID=01234567-89ab-cdef-0123-456789abcdef`, 157 | }, 158 | { 159 | name: "tenc: DefaultIsProtected=1 DefaultPerSampleIVSize=1", 160 | src: &Tenc{ 161 | FullBox: FullBox{ 162 | Version: 1, 163 | Flags: [3]byte{0x00, 0x00, 0x00}, 164 | }, 165 | Reserved: 0x00, 166 | DefaultCryptByteBlock: 0x0a, 167 | DefaultSkipByteBlock: 0x0b, 168 | DefaultIsProtected: 1, 169 | DefaultPerSampleIVSize: 1, 170 | DefaultKID: [16]byte{ 171 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 172 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 173 | }, 174 | }, 175 | dst: &Tenc{}, 176 | bin: []byte{ 177 | 1, // version 178 | 0x00, 0x00, 0x00, // flags 179 | 0x00, // reserved 180 | 0xab, // default crypt byte block / default skip byte block 181 | 0x01, 0x01, // default is protected / default per sample iv size 182 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, // default kid 183 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 184 | }, 185 | str: `Version=1 Flags=0x000000 ` + 186 | `Reserved=0 ` + 187 | `DefaultCryptByteBlock=10 ` + 188 | `DefaultSkipByteBlock=11 ` + 189 | `DefaultIsProtected=1 ` + 190 | `DefaultPerSampleIVSize=1 ` + 191 | `DefaultKID=01234567-89ab-cdef-0123-456789abcdef`, 192 | }, 193 | } 194 | for _, tc := range testCases { 195 | t.Run(tc.name, func(t *testing.T) { 196 | // Marshal 197 | buf := bytes.NewBuffer(nil) 198 | n, err := Marshal(buf, tc.src, tc.ctx) 199 | require.NoError(t, err) 200 | assert.Equal(t, uint64(len(tc.bin)), n) 201 | assert.Equal(t, tc.bin, buf.Bytes()) 202 | 203 | // Unmarshal 204 | r := bytes.NewReader(tc.bin) 205 | n, err = Unmarshal(r, uint64(len(tc.bin)), tc.dst, tc.ctx) 206 | require.NoError(t, err) 207 | assert.Equal(t, uint64(buf.Len()), n) 208 | assert.Equal(t, tc.src, tc.dst) 209 | s, err := r.Seek(0, io.SeekCurrent) 210 | require.NoError(t, err) 211 | assert.Equal(t, int64(buf.Len()), s) 212 | 213 | // UnmarshalAny 214 | dst, n, err := UnmarshalAny(bytes.NewReader(tc.bin), tc.src.GetType(), uint64(len(tc.bin)), tc.ctx) 215 | require.NoError(t, err) 216 | assert.Equal(t, uint64(buf.Len()), n) 217 | assert.Equal(t, tc.src, dst) 218 | s, err = r.Seek(0, io.SeekCurrent) 219 | require.NoError(t, err) 220 | assert.Equal(t, int64(buf.Len()), s) 221 | 222 | // Stringify 223 | str, err := Stringify(tc.src, tc.ctx) 224 | require.NoError(t, err) 225 | assert.Equal(t, tc.str, str) 226 | }) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /box_types_metadata_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestBoxTypesMetadata(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | src IImmutableBox 16 | dst IBox 17 | bin []byte 18 | str string 19 | ctx Context 20 | }{ 21 | { 22 | name: "ilst", 23 | src: &Ilst{}, 24 | dst: &Ilst{}, 25 | bin: nil, 26 | str: ``, 27 | }, 28 | { 29 | name: "ilst meta container", 30 | src: &IlstMetaContainer{ 31 | AnyTypeBox: AnyTypeBox{Type: StrToBoxType("----")}, 32 | }, 33 | dst: &IlstMetaContainer{ 34 | AnyTypeBox: AnyTypeBox{Type: StrToBoxType("----")}, 35 | }, 36 | bin: nil, 37 | str: ``, 38 | ctx: Context{UnderIlst: true}, 39 | }, 40 | { 41 | name: "ilst data (binary)", 42 | src: &Data{DataType: 0, DataLang: 0x12345678, Data: []byte("foo")}, 43 | dst: &Data{}, 44 | bin: []byte{ 45 | 0x00, 0x00, 0x00, 0x00, // data type 46 | 0x12, 0x34, 0x56, 0x78, // data lang 47 | 0x66, 0x6f, 0x6f, // data 48 | }, 49 | str: `DataType=BINARY DataLang=305419896 Data=[0x66, 0x6f, 0x6f]`, 50 | ctx: Context{UnderIlstMeta: true}, 51 | }, 52 | { 53 | name: "ilst data (utf8)", 54 | src: &Data{DataType: 1, DataLang: 0x12345678, Data: []byte("foo")}, 55 | dst: &Data{}, 56 | bin: []byte{ 57 | 0x00, 0x00, 0x00, 0x01, // data type 58 | 0x12, 0x34, 0x56, 0x78, // data lang 59 | 0x66, 0x6f, 0x6f, // data 60 | }, 61 | str: `DataType=UTF8 DataLang=305419896 Data="foo"`, 62 | ctx: Context{UnderIlstMeta: true}, 63 | }, 64 | { 65 | name: "ilst data (utf8 escaped)", 66 | src: &Data{DataType: 1, DataLang: 0x12345678, Data: []byte{0x00, 'f', 'o', 'o'}}, 67 | dst: &Data{}, 68 | bin: []byte{ 69 | 0x00, 0x00, 0x00, 0x01, // data type 70 | 0x12, 0x34, 0x56, 0x78, // data lang 71 | 0x00, 0x66, 0x6f, 0x6f, // data 72 | }, 73 | str: `DataType=UTF8 DataLang=305419896 Data=".foo"`, 74 | ctx: Context{UnderIlstMeta: true}, 75 | }, 76 | { 77 | name: "ilst data (utf16)", 78 | src: &Data{DataType: 2, DataLang: 0x12345678, Data: []byte("foo")}, 79 | dst: &Data{}, 80 | bin: []byte{ 81 | 0x00, 0x00, 0x00, 0x02, // data type 82 | 0x12, 0x34, 0x56, 0x78, // data lang 83 | 0x66, 0x6f, 0x6f, // data 84 | }, 85 | str: `DataType=UTF16 DataLang=305419896 Data=[0x66, 0x6f, 0x6f]`, 86 | ctx: Context{UnderIlstMeta: true}, 87 | }, 88 | { 89 | name: "ilst data (mac string)", 90 | src: &Data{DataType: 3, DataLang: 0x12345678, Data: []byte("foo")}, 91 | dst: &Data{}, 92 | bin: []byte{ 93 | 0x00, 0x00, 0x00, 0x03, // data type 94 | 0x12, 0x34, 0x56, 0x78, // data lang 95 | 0x66, 0x6f, 0x6f, // data 96 | }, 97 | str: `DataType=MAC_STR DataLang=305419896 Data=[0x66, 0x6f, 0x6f]`, 98 | ctx: Context{UnderIlstMeta: true}, 99 | }, 100 | { 101 | name: "ilst data (jpsg)", 102 | src: &Data{DataType: 14, DataLang: 0x12345678, Data: []byte("foo")}, 103 | dst: &Data{}, 104 | bin: []byte{ 105 | 0x00, 0x00, 0x00, 0x0e, // data type 106 | 0x12, 0x34, 0x56, 0x78, // data lang 107 | 0x66, 0x6f, 0x6f, // data 108 | }, 109 | str: `DataType=JPEG DataLang=305419896 Data=[0x66, 0x6f, 0x6f]`, 110 | ctx: Context{UnderIlstMeta: true}, 111 | }, 112 | { 113 | name: "ilst data (int)", 114 | src: &Data{DataType: 21, DataLang: 0x12345678, Data: []byte("foo")}, 115 | dst: &Data{}, 116 | bin: []byte{ 117 | 0x00, 0x00, 0x00, 0x15, // data type 118 | 0x12, 0x34, 0x56, 0x78, // data lang 119 | 0x66, 0x6f, 0x6f, // data 120 | }, 121 | str: `DataType=INT DataLang=305419896 Data=[0x66, 0x6f, 0x6f]`, 122 | ctx: Context{UnderIlstMeta: true}, 123 | }, 124 | { 125 | name: "ilst data (float32)", 126 | src: &Data{DataType: 22, DataLang: 0x12345678, Data: []byte("foo")}, 127 | dst: &Data{}, 128 | bin: []byte{ 129 | 0x00, 0x00, 0x00, 0x16, // data type 130 | 0x12, 0x34, 0x56, 0x78, // data lang 131 | 0x66, 0x6f, 0x6f, // data 132 | }, 133 | str: `DataType=FLOAT32 DataLang=305419896 Data=[0x66, 0x6f, 0x6f]`, 134 | ctx: Context{UnderIlstMeta: true}, 135 | }, 136 | { 137 | name: "ilst data (float64)", 138 | src: &Data{DataType: 23, DataLang: 0x12345678, Data: []byte("foo")}, 139 | dst: &Data{}, 140 | bin: []byte{ 141 | 0x00, 0x00, 0x00, 0x17, // data type 142 | 0x12, 0x34, 0x56, 0x78, // data lang 143 | 0x66, 0x6f, 0x6f, // data 144 | }, 145 | str: `DataType=FLOAT64 DataLang=305419896 Data=[0x66, 0x6f, 0x6f]`, 146 | ctx: Context{UnderIlstMeta: true}, 147 | }, 148 | { 149 | name: "ilst data (string)", 150 | src: &StringData{ 151 | AnyTypeBox: AnyTypeBox{Type: StrToBoxType("mean")}, 152 | Data: []byte{0x00, 'f', 'o', 'o'}, 153 | }, 154 | dst: &StringData{ 155 | AnyTypeBox: AnyTypeBox{Type: StrToBoxType("mean")}, 156 | }, 157 | bin: []byte{ 158 | 0x00, 0x66, 0x6f, 0x6f, // data 159 | }, 160 | str: `Data=".foo"`, 161 | ctx: Context{UnderIlstFreeMeta: true}, 162 | }, 163 | { 164 | name: "ilst numbered item", 165 | src: &Item{ 166 | AnyTypeBox: AnyTypeBox{Type: Uint32ToBoxType(1)}, 167 | Version: 0, 168 | Flags: [3]byte{'0'}, 169 | ItemName: []byte("data"), 170 | Data: Data{DataType: 0, DataLang: 0x12345678, Data: []byte("foo")}}, 171 | dst: &Item{ 172 | AnyTypeBox: AnyTypeBox{Type: Uint32ToBoxType(1)}, 173 | }, 174 | bin: []byte{ 175 | 0x00, // Version 176 | 0x30, 0x00, 0x0, // Flags 177 | 0x64, 0x61, 0x74, 0x61, // Item Name 178 | 0x0, 0x0, 0x0, 0x0, // data type 179 | 0x12, 0x34, 0x56, 0x78, // data lang 180 | 0x66, 0x6f, 0x6f, // data 181 | }, 182 | str: `Version=0 Flags=0x000000 ItemName="data" Data={DataType=BINARY DataLang=305419896 Data=[0x66, 0x6f, 0x6f]}`, 183 | ctx: Context{UnderIlst: true, QuickTimeKeysMetaEntryCount: 1}, 184 | }, 185 | { 186 | name: "keys", 187 | src: &Keys{ 188 | EntryCount: 2, 189 | Entries: []Key{ 190 | { 191 | KeySize: 27, 192 | KeyNamespace: []byte("mdta"), 193 | KeyValue: []byte("com.android.version"), 194 | }, 195 | { 196 | KeySize: 25, 197 | KeyNamespace: []byte("mdta"), 198 | KeyValue: []byte("com.android.model"), 199 | }, 200 | }, 201 | }, 202 | dst: &Keys{}, 203 | bin: []byte{ 204 | 0x0, // Version 205 | 0x0, 0x0, 0x0, // Flags 206 | 0x0, 0x0, 0x0, 0x2, // entry count 207 | 0x0, 0x0, 0x0, 0x1b, // entry 1 keysize 208 | 0x6d, 0x64, 0x74, 0x61, // entry 1 key namespace 209 | 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, // entry 1 key value 210 | 0x0, 0x0, 0x0, 0x19, // entry 2 keysize 211 | 0x6d, 0x64, 0x74, 0x61, // entry 2 key namespace 212 | 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, // entry 2 key value 213 | }, 214 | str: `Version=0 Flags=0x000000 EntryCount=2 Entries=[{KeySize=27 KeyNamespace="mdta" KeyValue="com.android.version"}, {KeySize=25 KeyNamespace="mdta" KeyValue="com.android.model"}]`, 215 | ctx: Context{}, 216 | }, 217 | } 218 | for _, tc := range testCases { 219 | t.Run(tc.name, func(t *testing.T) { 220 | // Marshal 221 | buf := bytes.NewBuffer(nil) 222 | n, err := Marshal(buf, tc.src, tc.ctx) 223 | require.NoError(t, err) 224 | assert.Equal(t, uint64(len(tc.bin)), n) 225 | assert.Equal(t, tc.bin, buf.Bytes()) 226 | 227 | // Unmarshal 228 | r := bytes.NewReader(tc.bin) 229 | n, err = Unmarshal(r, uint64(len(tc.bin)), tc.dst, tc.ctx) 230 | require.NoError(t, err) 231 | assert.Equal(t, uint64(buf.Len()), n) 232 | assert.Equal(t, tc.src, tc.dst) 233 | s, err := r.Seek(0, io.SeekCurrent) 234 | require.NoError(t, err) 235 | assert.Equal(t, int64(buf.Len()), s) 236 | 237 | // UnmarshalAny 238 | dst, n, err := UnmarshalAny(bytes.NewReader(tc.bin), tc.src.GetType(), uint64(len(tc.bin)), tc.ctx) 239 | require.NoError(t, err) 240 | assert.Equal(t, uint64(buf.Len()), n) 241 | assert.Equal(t, tc.src, dst) 242 | s, err = r.Seek(0, io.SeekCurrent) 243 | require.NoError(t, err) 244 | assert.Equal(t, int64(buf.Len()), s) 245 | 246 | // Stringify 247 | str, err := Stringify(tc.src, tc.ctx) 248 | require.NoError(t, err) 249 | assert.Equal(t, tc.str, str) 250 | }) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /marshaller_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/abema/go-mp4/internal/bitio" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestMarshal(t *testing.T) { 13 | type inner struct { 14 | Array [4]byte `mp4:"0,size=8,string"` 15 | } 16 | 17 | type testBox struct { 18 | mockBox 19 | FullBox `mp4:"0,extend"` 20 | 21 | // integer 22 | Int32 int32 `mp4:"1,size=32"` 23 | Uint32 uint32 `mp4:"2,size=32"` 24 | Int64 int64 `mp4:"3,size=64"` 25 | Uint64 uint64 `mp4:"4,size=64"` 26 | 27 | // left-justified 28 | Int32l int32 `mp4:"5,size=29"` 29 | Padding0 uint8 `mp4:"6,size=3,const=0"` 30 | Uint32l uint32 `mp4:"7,size=29"` 31 | Padding1 uint8 `mp4:"8,size=3,const=0"` 32 | Int64l int64 `mp4:"9,size=59"` 33 | Padding2 uint8 `mp4:"10,size=5,const=0"` 34 | Uint64l uint64 `mp4:"11,size=59"` 35 | Padding3 uint8 `mp4:"12,size=5,const=0"` 36 | 37 | // right-justified 38 | Padding4 uint8 `mp4:"13,size=3,const=0"` 39 | Int32r int32 `mp4:"14,size=29"` 40 | Padding5 uint8 `mp4:"15,size=3,const=0"` 41 | Uint32r uint32 `mp4:"16,size=29"` 42 | Padding6 uint8 `mp4:"17,size=5,const=0"` 43 | Int64r int64 `mp4:"18,size=59"` 44 | Padding7 uint8 `mp4:"19,size=5,const=0"` 45 | Uint64r uint64 `mp4:"20,size=59"` 46 | 47 | // varint 48 | Varint uint16 `mp4:"21,varint"` 49 | 50 | // string, slice, pointer, array 51 | String string `mp4:"22,string"` 52 | StringCP string `mp4:"23,string=c_p"` 53 | Bytes []byte `mp4:"24,size=8,len=5"` 54 | Uints []uint `mp4:"25,size=16,len=dynamic"` 55 | Ptr *inner `mp4:"26,extend"` 56 | 57 | // bool 58 | Bool bool `mp4:"27,size=1"` 59 | Padding8 uint8 `mp4:"28,size=7,const=0"` 60 | 61 | // dynamic-size 62 | DynUint uint `mp4:"29,size=dynamic"` 63 | 64 | // optional 65 | OptUint1 uint `mp4:"30,size=8,opt=0x0100"` // enabled 66 | OptUint2 uint `mp4:"31,size=8,opt=0x0200"` // disabled 67 | OptUint3 uint `mp4:"32,size=8,nopt=0x0400"` // disabled 68 | OptUint4 uint `mp4:"33,size=8,nopt=0x0800"` // enabled 69 | 70 | // not sorted 71 | NotSorted35 uint8 `mp4:"35,size=8,dec"` 72 | NotSorted36 uint8 `mp4:"36,size=8,dec"` 73 | NotSorted34 uint8 `mp4:"34,size=8,dec"` 74 | } 75 | 76 | boxType := StrToBoxType("test") 77 | mb := mockBox{ 78 | Type: boxType, 79 | DynSizeMap: map[string]uint{ 80 | "DynUint": 24, 81 | }, 82 | DynLenMap: map[string]uint{ 83 | "Uints": 5, 84 | }, 85 | } 86 | AddBoxDef(&testBox{mockBox: mb}, 0) 87 | 88 | src := testBox{ 89 | mockBox: mb, 90 | 91 | FullBox: FullBox{ 92 | Version: 0, 93 | Flags: [3]byte{0x00, 0x05, 0x00}, 94 | }, 95 | 96 | Int32: -0x1234567, 97 | Uint32: 0x1234567, 98 | Int64: -0x123456789abcdef, 99 | Uint64: 0x123456789abcdef, 100 | 101 | Int32l: -0x123456, 102 | Uint32l: 0x123456, 103 | Int64l: -0x123456789abcd, 104 | Uint64l: 0x123456789abcd, 105 | 106 | Int32r: -0x123456, 107 | Uint32r: 0x123456, 108 | Int64r: -0x123456789abcd, 109 | Uint64r: 0x123456789abcd, 110 | 111 | // raw : 0x1234=0001,0010,0011,0100b 112 | // varint: 0xa434=1010,0100,0011,0100b 113 | Varint: 0x1234, 114 | 115 | String: "abema.tv", 116 | StringCP: "CyberAgent, Inc.", 117 | Bytes: []byte("abema"), 118 | Uints: []uint{0x01, 0x02, 0x03, 0x04, 0x05}, 119 | Ptr: &inner{ 120 | Array: [4]byte{'h', 'o', 'g', 'e'}, 121 | }, 122 | 123 | Bool: true, 124 | 125 | DynUint: 0x123456, 126 | 127 | OptUint1: 0x11, 128 | OptUint4: 0x44, 129 | 130 | NotSorted35: 35, 131 | NotSorted36: 36, 132 | NotSorted34: 34, 133 | } 134 | 135 | bin := []byte{ 136 | 0, // version 137 | 0x00, 0x05, 0x00, // flags 138 | 0xfe, 0xdc, 0xba, 0x99, // int32 139 | 0x01, 0x23, 0x45, 0x67, // uint32 140 | 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x11, // int64 141 | 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, // uint64 142 | 0xff, 0x6e, 0x5d, 0x50, // int32l & padding(3bits) 143 | 0x00, 0x91, 0xa2, 0xb0, // uint32l & padding(3bits) 144 | 0xff, 0xdb, 0x97, 0x53, 0x0e, 0xca, 0x86, 0x60, // int64l & padding(3bits) 145 | 0x00, 0x24, 0x68, 0xAC, 0xF1, 0x35, 0x79, 0xA0, // uint64l & padding(3bits) 146 | 0x1f, 0xed, 0xcb, 0xaa, // padding(5bits) & int32r 147 | 0x00, 0x12, 0x34, 0x56, // padding(5bits) & uint32r 148 | 0x07, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x33, // padding(5bits) & int64r 149 | 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, // padding(5bits) & uint64r 150 | 0x80, 0x80, 0xa4, 0x34, // varint 151 | 'a', 'b', 'e', 'm', 'a', '.', 't', 'v', 0, // string 152 | 'C', 'y', 'b', 'e', 'r', 'A', 'g', 'e', 'n', 't', ',', ' ', 'I', 'n', 'c', '.', 0, // string 153 | 'a', 'b', 'e', 'm', 'a', // bytes 154 | 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, // uints 155 | 'h', 'o', 'g', 'e', // inner.array 156 | 0x80, // bool & padding 157 | 0x12, 0x34, 0x56, // dynUint 158 | 0x11, // optUint1 159 | 0x44, // optUint4 160 | 34, 35, 36, // not sorted 161 | } 162 | 163 | // marshal 164 | buf := &bytes.Buffer{} 165 | n, err := Marshal(buf, &src, Context{}) 166 | require.NoError(t, err) 167 | assert.Equal(t, uint64(len(bin)), n) 168 | assert.Equal(t, bin, buf.Bytes()) 169 | 170 | // unmarshal 171 | dst := testBox{mockBox: mb} 172 | n, err = Unmarshal(bytes.NewReader(bin), uint64(len(bin)+8), &dst, Context{}) 173 | assert.NoError(t, err) 174 | assert.Equal(t, uint64(len(bin)), n) 175 | assert.Equal(t, src, dst) 176 | } 177 | 178 | func TestUnsupportedBoxVersionErr(t *testing.T) { 179 | type testBox struct { 180 | mockBox 181 | FullBox `mp4:"0,extend"` 182 | } 183 | 184 | boxType := StrToBoxType("test") 185 | mb := mockBox{ 186 | Type: boxType, 187 | } 188 | AddBoxDef(&testBox{mockBox: mb}, 0, 1, 2) 189 | 190 | for _, e := range []struct { 191 | version byte 192 | enabled bool 193 | }{ 194 | {version: 0, enabled: true}, 195 | {version: 1, enabled: true}, 196 | {version: 2, enabled: true}, 197 | {version: 3, enabled: false}, 198 | {version: 4, enabled: false}, 199 | } { 200 | expected := testBox{ 201 | mockBox: mb, 202 | FullBox: FullBox{ 203 | Version: e.version, 204 | Flags: [3]byte{0x00, 0x00, 0x00}, 205 | }, 206 | } 207 | 208 | bin := []byte{ 209 | e.version, // version 210 | 0x00, 0x00, 0x00, // flags 211 | } 212 | 213 | dst := testBox{mockBox: mb} 214 | n, err := Unmarshal(bytes.NewReader(bin), uint64(len(bin)+8), &dst, Context{}) 215 | 216 | if e.enabled { 217 | assert.NoError(t, err, "version=%d", e.version) 218 | assert.Equal(t, uint64(len(bin)), n, "version=%d", e.version) 219 | assert.Equal(t, expected, dst, "version=%d", e.version) 220 | } else { 221 | assert.Error(t, err, "version=%d", e.version) 222 | } 223 | } 224 | } 225 | 226 | func TestReadPString(t *testing.T) { 227 | type testBox struct { 228 | mockBox 229 | Box 230 | String string `mp4:"1,string"` 231 | StringCP string `mp4:"2,string=c_p"` 232 | Uint32 uint32 `mp4:"3,size=32"` 233 | } 234 | 235 | testCases := []struct { 236 | name string 237 | src []byte 238 | isPString bool 239 | wants string 240 | }{ 241 | { 242 | name: "c style string", 243 | src: []byte{0x05, 'a', 'b', 'e', 'm', 'a'}, 244 | isPString: true, 245 | wants: "abema", 246 | }, { 247 | name: "pascal style string", 248 | src: []byte{'a', 'b', 'e', 'm', 'a', 0x00}, 249 | isPString: true, 250 | wants: "abema", 251 | }, { 252 | name: "pascal style string isPString=true", 253 | src: []byte{0x0a, 'a', 'b', 'e', 'm', 'a', '1', '2', '3', '4', '5'}, 254 | isPString: true, 255 | wants: "abema12345", 256 | }, { 257 | name: "pascal style string isPString=false", 258 | src: []byte{0x0a, 'a', 'b', 'e', 'm', 'a', '1', '2', '3', '4', '5', 0x00}, 259 | isPString: false, 260 | wants: "\nabema12345", 261 | }, 262 | } 263 | for _, tc := range testCases { 264 | t.Run(tc.name, func(t *testing.T) { 265 | boxType := StrToBoxType("test") 266 | mb := mockBox{ 267 | Type: boxType, 268 | IsPStringMap: map[string]bool{"StringCP": tc.isPString}, 269 | } 270 | AddBoxDef(&testBox{mockBox: mb}, 0) 271 | src := append(append([]byte{ 272 | 'h', 'e', 'l', 'l', 'o', 0x00, 273 | }, tc.src...), 274 | 0x01, 0x23, 0x45, 0x67, 275 | ) 276 | dst := testBox{mockBox: mb} 277 | n, err := Unmarshal(bytes.NewReader(src), uint64(len(src)), &dst, Context{}) 278 | require.NoError(t, err) 279 | assert.Equal(t, uint64(len(src)), n) 280 | assert.Equal(t, "hello", dst.String) 281 | assert.Equal(t, tc.wants, dst.StringCP) 282 | assert.Equal(t, uint32(0x01234567), dst.Uint32) 283 | }) 284 | } 285 | } 286 | 287 | func TestReadVarint(t *testing.T) { 288 | testCases := []struct { 289 | name string 290 | input []byte 291 | err bool 292 | expected uint64 293 | }{ 294 | {name: "0", input: []byte{0x0}, expected: 0}, 295 | {name: "1 byte", input: []byte{0x6c}, expected: 0x6c}, 296 | {name: "2 bytes", input: []byte{0xac, 0x52}, expected: 0x1652}, 297 | {name: "3 bytes", input: []byte{0xac, 0xd2, 0x43}, expected: 0xb2943}, 298 | {name: "overrun", input: []byte{0xac, 0xd2, 0xef}, err: true}, 299 | {name: "1 byte padded", input: []byte{0x80, 0x80, 0x80, 0x6c}, expected: 0x6c}, 300 | {name: "2 byte padded", input: []byte{0x80, 0x80, 0x81, 0x0c}, expected: 0x8c}, 301 | } 302 | for _, tc := range testCases { 303 | t.Run(tc.name, func(t *testing.T) { 304 | u := &unmarshaller{ 305 | reader: bitio.NewReadSeeker(bytes.NewReader(tc.input)), 306 | size: uint64(len(tc.input)), 307 | } 308 | val, err := u.readUvarint() 309 | if tc.err { 310 | require.Error(t, err) 311 | return 312 | } 313 | if tc.err { 314 | assert.Error(t, err) 315 | } 316 | require.NoError(t, err) 317 | assert.Equal(t, tc.expected, val) 318 | }) 319 | } 320 | } 321 | 322 | func TestWriteVarint(t *testing.T) { 323 | testCases := []struct { 324 | name string 325 | input uint64 326 | expected []byte 327 | }{ 328 | {name: "0", input: 0x0, expected: []byte{0x80, 0x80, 0x80, 0x00}}, 329 | {name: "1 byte", input: 0x6c, expected: []byte{0x80, 0x80, 0x80, 0x6c}}, 330 | {name: "1 byte into 2 bytes", input: 0x8c, expected: []byte{0x80, 0x80, 0x81, 0x0c}}, 331 | {name: "2 bytes", input: 0x1652, expected: []byte{0x80, 0x80, 0xac, 0x52}}, 332 | {name: "3 bytes", input: 0xb2943, expected: []byte{0x80, 0xac, 0xd2, 0x43}}, 333 | } 334 | for _, tc := range testCases { 335 | t.Run(tc.name, func(t *testing.T) { 336 | var b bytes.Buffer 337 | m := &marshaller{ 338 | writer: bitio.NewWriter(&b), 339 | } 340 | err := m.writeUvarint(tc.input) 341 | require.NoError(t, err) 342 | assert.Equal(t, tc.expected, b.Bytes()) 343 | }) 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /cmd/mp4tool/internal/divide/divide.go: -------------------------------------------------------------------------------- 1 | package divide 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math" 7 | "os" 8 | "path" 9 | 10 | "github.com/abema/go-mp4" 11 | ) 12 | 13 | const ( 14 | videoDirName = "video" 15 | audioDirName = "audio" 16 | encVideoDirName = "video_enc" 17 | encAudioDirName = "audio_enc" 18 | initMP4FileName = "init.mp4" 19 | playlistFileName = "playlist.m3u8" 20 | ) 21 | 22 | func segmentFileName(i int) string { 23 | return fmt.Sprintf("%d.mp4", i) 24 | } 25 | 26 | func Main(args []string) int { 27 | if len(args) < 2 { 28 | println("USAGE: mp4tool divide INPUT.mp4 OUTPUT_DIR") 29 | return 1 30 | } 31 | 32 | if err := divide(args[0], args[1]); err != nil { 33 | fmt.Println("Error:", err) 34 | return 1 35 | } 36 | return 0 37 | } 38 | 39 | type childInfo map[uint32]uint64 40 | 41 | type segment struct { 42 | duration float64 43 | } 44 | 45 | type trackType int 46 | 47 | const ( 48 | trackVideo trackType = iota 49 | trackAudio 50 | trackEncVideo 51 | trackEncAudio 52 | ) 53 | 54 | type track struct { 55 | id uint32 56 | trackType trackType 57 | timescale uint32 58 | bandwidth uint64 59 | height uint16 60 | width uint16 61 | segments []segment 62 | outputDir string 63 | initFile *os.File 64 | segmentFile *os.File 65 | } 66 | 67 | func divide(inputFilePath, outputDir string) error { 68 | inputFile, err := os.Open(inputFilePath) 69 | if err != nil { 70 | return err 71 | } 72 | defer inputFile.Close() 73 | 74 | // generate track map 75 | tracks := make(map[uint32]*track, 4) 76 | bis, err := mp4.ExtractBox(inputFile, nil, mp4.BoxPath{mp4.BoxTypeMoov(), mp4.BoxTypeTrak()}) 77 | if err != nil { 78 | return err 79 | } 80 | for _, bi := range bis { 81 | t := new(track) 82 | 83 | // get trackID from Tkhd box 84 | bs, err := mp4.ExtractBoxWithPayload(inputFile, bi, mp4.BoxPath{mp4.BoxTypeTkhd()}) 85 | if err != nil { 86 | return err 87 | } else if len(bs) != 1 { 88 | return fmt.Errorf("trak box must have one tkhd box: tkhd=%d", len(bs)) 89 | } 90 | tkhd := bs[0].Payload.(*mp4.Tkhd) 91 | t.id = tkhd.TrackID 92 | 93 | // get timescale from Mdhd box 94 | bs, err = mp4.ExtractBoxWithPayload(inputFile, bi, mp4.BoxPath{mp4.BoxTypeMdia(), mp4.BoxTypeMdhd()}) 95 | if err != nil { 96 | return err 97 | } else if len(bs) != 1 { 98 | return fmt.Errorf("trak box must have one mdhd box: mdhd=%d", len(bs)) 99 | } 100 | mdhd := bs[0].Payload.(*mp4.Mdhd) 101 | t.timescale = mdhd.Timescale 102 | 103 | bs, err = mp4.ExtractBoxWithPayload(inputFile, bi, mp4.BoxPath{mp4.BoxTypeMdia(), mp4.BoxTypeMinf(), mp4.BoxTypeStbl(), mp4.BoxTypeStsd(), mp4.StrToBoxType("avc1")}) 104 | if err != nil { 105 | return err 106 | } 107 | if len(bs) != 0 { 108 | avc1 := bs[0].Payload.(*mp4.VisualSampleEntry) 109 | t.trackType = trackVideo 110 | t.height = avc1.Height 111 | t.width = avc1.Width 112 | t.outputDir = path.Join(outputDir, videoDirName) 113 | tracks[t.id] = t 114 | continue 115 | } 116 | 117 | bis, err = mp4.ExtractBox(inputFile, bi, mp4.BoxPath{mp4.BoxTypeMdia(), mp4.BoxTypeMinf(), mp4.BoxTypeStbl(), mp4.BoxTypeStsd(), mp4.StrToBoxType("mp4a")}) 118 | if err != nil { 119 | return err 120 | } 121 | if len(bis) != 0 { 122 | t.trackType = trackAudio 123 | t.outputDir = path.Join(outputDir, audioDirName) 124 | tracks[t.id] = t 125 | continue 126 | } 127 | 128 | bs, err = mp4.ExtractBoxWithPayload(inputFile, bi, mp4.BoxPath{mp4.BoxTypeMdia(), mp4.BoxTypeMinf(), mp4.BoxTypeStbl(), mp4.BoxTypeStsd(), mp4.StrToBoxType("encv")}) 129 | if err != nil { 130 | return err 131 | } 132 | if len(bs) != 0 { 133 | encv := bs[0].Payload.(*mp4.VisualSampleEntry) 134 | t.trackType = trackEncVideo 135 | t.height = encv.Height 136 | t.width = encv.Width 137 | t.outputDir = path.Join(outputDir, encVideoDirName) 138 | tracks[t.id] = t 139 | continue 140 | } 141 | 142 | bis, err = mp4.ExtractBox(inputFile, bi, mp4.BoxPath{mp4.BoxTypeMdia(), mp4.BoxTypeMinf(), mp4.BoxTypeStbl(), mp4.BoxTypeStsd(), mp4.StrToBoxType("enca")}) 143 | if err != nil { 144 | return err 145 | } 146 | if len(bis) != 0 { 147 | t.trackType = trackEncAudio 148 | t.outputDir = path.Join(outputDir, encAudioDirName) 149 | tracks[t.id] = t 150 | continue 151 | } 152 | 153 | fmt.Printf("WARN: unsupported track type: trackID=%d\n", t.id) 154 | } 155 | 156 | for _, t := range tracks { 157 | os.MkdirAll(t.outputDir, 0777) 158 | 159 | if t.initFile, err = os.Create(path.Join(t.outputDir, initMP4FileName)); err != nil { 160 | return err 161 | } 162 | defer t.initFile.Close() 163 | 164 | if t.segmentFile, err = os.Create(path.Join(t.outputDir, segmentFileName(0))); err != nil { 165 | return err 166 | } 167 | defer func(t *track) { t.segmentFile.Close() }(t) 168 | } 169 | 170 | currTrackID := uint32(math.MaxUint32) 171 | if _, err = mp4.ReadBoxStructure(inputFile, func(h *mp4.ReadHandle) (interface{}, error) { 172 | // initialization segment 173 | if h.BoxInfo.Type == mp4.BoxTypeMoov() || 174 | h.BoxInfo.Type == mp4.BoxTypeFtyp() || 175 | h.BoxInfo.Type == mp4.BoxTypePssh() || 176 | h.BoxInfo.Type == mp4.BoxTypeMvhd() || 177 | h.BoxInfo.Type == mp4.BoxTypeTrak() || 178 | h.BoxInfo.Type == mp4.BoxTypeMvex() || 179 | h.BoxInfo.Type == mp4.BoxTypeUdta() || 180 | h.BoxInfo.Type == mp4.BoxTypeSidx() { 181 | 182 | var writeAll bool 183 | var trackID uint32 184 | if h.BoxInfo.Type == mp4.BoxTypeTrak() { 185 | // get trackID from Tkhd box 186 | bs, err := mp4.ExtractBoxWithPayload(inputFile, &h.BoxInfo, mp4.BoxPath{mp4.BoxTypeTkhd()}) 187 | if err != nil { 188 | return nil, err 189 | } else if len(bs) != 1 { 190 | return nil, fmt.Errorf("trak box must have one tkhd box: tkhd=%d", len(bs)) 191 | } 192 | tkhd := bs[0].Payload.(*mp4.Tkhd) 193 | trackID = tkhd.TrackID 194 | 195 | } else { 196 | writeAll = true 197 | } 198 | 199 | offsetMap := make(map[uint32]int64, len(tracks)) 200 | biMap := make(map[uint32]*mp4.BoxInfo, len(tracks)) 201 | for _, t := range tracks { 202 | if writeAll || t.id == trackID { 203 | if offsetMap[t.id], err = t.initFile.Seek(0, io.SeekEnd); err != nil { 204 | return nil, err 205 | } 206 | if biMap[t.id], err = mp4.WriteBoxInfo(t.initFile, &h.BoxInfo); err != nil { 207 | return nil, err 208 | } 209 | biMap[t.id].Size = biMap[t.id].HeaderSize 210 | } 211 | } 212 | 213 | if h.BoxInfo.Type == mp4.BoxTypeMoov() { 214 | vals, err := h.Expand() 215 | if err != nil { 216 | return nil, err 217 | } 218 | for _, val := range vals { 219 | ci := val.(childInfo) 220 | for _, t := range tracks { 221 | // already writeAll is true in Moov box 222 | biMap[t.id].Size += ci[t.id] 223 | } 224 | } 225 | 226 | } else { 227 | // copy all data of payload 228 | for _, t := range tracks { 229 | if writeAll || t.id == trackID { 230 | n, err := h.ReadData(t.initFile) 231 | if err != nil { 232 | return nil, err 233 | } 234 | biMap[t.id].Size += uint64(n) 235 | } 236 | } 237 | } 238 | 239 | // rewrite headers 240 | for _, t := range tracks { 241 | if writeAll || t.id == trackID { 242 | if _, err = t.initFile.Seek(offsetMap[t.id], io.SeekStart); err != nil { 243 | return nil, err 244 | } 245 | if biMap[t.id], err = mp4.WriteBoxInfo(t.initFile, biMap[t.id]); err != nil { 246 | return nil, err 247 | } 248 | } 249 | } 250 | 251 | ci := make(childInfo, 0) 252 | for id, bi := range biMap { 253 | ci[id] = bi.Size 254 | } 255 | return ci, nil 256 | } 257 | 258 | // media segment 259 | if h.BoxInfo.Type == mp4.BoxTypeMoof() || 260 | h.BoxInfo.Type == mp4.BoxTypeMdat() { 261 | 262 | if h.BoxInfo.Type == mp4.BoxTypeMoof() { 263 | // extract Tfdt-box 264 | bs, err := mp4.ExtractBoxWithPayload(inputFile, &h.BoxInfo, mp4.BoxPath{mp4.BoxTypeTraf(), mp4.BoxTypeTfhd()}) 265 | if err != nil { 266 | return nil, err 267 | } else if len(bs) != 1 { 268 | return nil, fmt.Errorf("trak box must have one tkhd box: tkhd=%d", len(bs)) 269 | } 270 | tfhd := bs[0].Payload.(*mp4.Tfhd) 271 | 272 | currTrackID = tfhd.TrackID 273 | if _, exists := tracks[currTrackID]; !exists { 274 | return nil, nil 275 | } 276 | 277 | var defaultSampleDuration uint32 278 | if tfhd.CheckFlag(0x000008) { 279 | defaultSampleDuration = tfhd.DefaultSampleDuration 280 | } 281 | 282 | // extract Trun-box 283 | bs, err = mp4.ExtractBoxWithPayload(inputFile, &h.BoxInfo, mp4.BoxPath{mp4.BoxTypeTraf(), mp4.BoxTypeTrun()}) 284 | if err != nil { 285 | return nil, err 286 | } 287 | trun := bs[0].Payload.(*mp4.Trun) 288 | 289 | var duration uint32 290 | for i := range trun.Entries { 291 | if trun.CheckFlag(0x000100) { 292 | duration += trun.Entries[i].SampleDuration 293 | } else { 294 | duration += defaultSampleDuration 295 | } 296 | } 297 | 298 | // close last segment file and create next 299 | t := tracks[currTrackID] 300 | t.segmentFile.Close() 301 | if t.segmentFile, err = os.Create(path.Join(t.outputDir, segmentFileName(len(t.segments)))); err != nil { 302 | return nil, err 303 | } 304 | t.segments = append(t.segments, segment{ 305 | duration: float64(duration) / float64(t.timescale), 306 | }) 307 | 308 | } else { // Mdat box 309 | if _, exists := tracks[currTrackID]; !exists { 310 | return nil, nil 311 | } 312 | 313 | t := tracks[currTrackID] 314 | bandwidth := uint64(float64(h.BoxInfo.Size) * 8 / t.segments[len(t.segments)-1].duration) 315 | if bandwidth > t.bandwidth { 316 | t.bandwidth = bandwidth 317 | } 318 | } 319 | 320 | t := tracks[currTrackID] 321 | if _, err := mp4.WriteBoxInfo(t.segmentFile, &h.BoxInfo); err != nil { 322 | return nil, err 323 | } 324 | if _, err := h.ReadData(t.segmentFile); err != nil { 325 | return nil, err 326 | } 327 | 328 | return nil, nil 329 | } 330 | 331 | // skip 332 | if h.BoxInfo.Type == mp4.BoxTypeMfra() { 333 | return nil, nil 334 | } 335 | 336 | return nil, fmt.Errorf("unexpected box type: %s", h.BoxInfo.Type.String()) 337 | }); err != nil { 338 | return err 339 | } 340 | 341 | trackTypeMap := make(map[trackType]*track, len(tracks)) 342 | for _, t := range tracks { 343 | trackTypeMap[t.trackType] = t 344 | } 345 | 346 | if err := outputMasterPlaylist(path.Join(outputDir, playlistFileName), trackTypeMap); err != nil { 347 | return err 348 | } 349 | 350 | for _, t := range tracks { 351 | if err := outputMediaPlaylist(path.Join(t.outputDir, playlistFileName), t.segments); err != nil { 352 | return err 353 | } 354 | } 355 | 356 | return nil 357 | } 358 | 359 | func outputMasterPlaylist(filePath string, trackTypeMap map[trackType]*track) error { 360 | file, err := os.Create(filePath) 361 | if err != nil { 362 | return err 363 | } 364 | defer file.Close() 365 | 366 | var adir string 367 | var vdir string 368 | var vt *track 369 | 370 | if _, exists := trackTypeMap[trackAudio]; exists { 371 | adir = audioDirName 372 | } else if _, exists := trackTypeMap[trackEncAudio]; exists { 373 | adir = encAudioDirName 374 | } 375 | 376 | if t, exists := trackTypeMap[trackVideo]; exists { 377 | vdir = videoDirName 378 | vt = t 379 | } else if t, exists := trackTypeMap[trackEncVideo]; exists { 380 | vdir = encVideoDirName 381 | vt = t 382 | } 383 | 384 | file.WriteString("#EXTM3U\n") 385 | if adir != "" { 386 | file.WriteString("#EXT-X-MEDIA:TYPE=AUDIO,URI=\"" + adir + "/" + playlistFileName + "\",GROUP-ID=\"audio\",NAME=\"audio\",AUTOSELECT=YES,CHANNELS=\"2\"\n") 387 | } 388 | if vdir != "" { 389 | _, err = fmt.Fprintf(file, "#EXT-X-STREAM-INF:BANDWIDTH=%d,CODECS=\"avc1.64001f,mp4a.40.2\",RESOLUTION=%dx%d", // FIXME: hard coding 390 | vt.bandwidth, vt.width, vt.height) 391 | if err != nil { 392 | return err 393 | } 394 | if adir != "" { 395 | file.WriteString(",AUDIO=\"audio\"") 396 | } 397 | file.WriteString("\n") 398 | file.WriteString(vdir + "/" + playlistFileName + "\n") 399 | } 400 | return nil 401 | } 402 | 403 | func outputMediaPlaylist(filePath string, segments []segment) error { 404 | file, err := os.Create(filePath) 405 | if err != nil { 406 | return err 407 | } 408 | defer file.Close() 409 | 410 | var maxDur float64 411 | for i := range segments { 412 | if segments[i].duration > maxDur { 413 | maxDur = segments[i].duration 414 | } 415 | } 416 | 417 | file.WriteString("#EXTM3U\n") 418 | file.WriteString("#EXT-X-VERSION:7\n") 419 | if _, err := fmt.Fprintf(file, "#EXT-X-TARGETDURATION:%d\n", int(math.Ceil(maxDur))); err != nil { 420 | return err 421 | } 422 | file.WriteString("#EXT-X-PLAYLIST-TYPE:VOD\n") 423 | file.WriteString("#EXT-X-MAP:URI=\"" + initMP4FileName + "\"\n") 424 | for i := range segments { 425 | if _, err := fmt.Fprintf(file, "#EXTINF:%f,\n", segments[i].duration); err != nil { 426 | return err 427 | } 428 | if _, err := fmt.Fprintf(file, "%s\n", segmentFileName(i)); err != nil { 429 | return err 430 | } 431 | } 432 | file.WriteString("#EXT-X-ENDLIST\n") 433 | return nil 434 | } 435 | -------------------------------------------------------------------------------- /field_test.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestBuildField(t *testing.T) { 12 | box := &struct { 13 | mockBox 14 | FullBox `mp4:"0,extend"` 15 | Int32 int32 `mp4:"1,size=32"` 16 | Int17 int32 `mp4:"2,size=17"` 17 | Uint15 uint16 `mp4:"3,size=15"` 18 | Const byte `mp4:"4,size=8,const=0"` 19 | String []byte `mp4:"5,size=8,string"` 20 | PString []byte `mp4:"6,size=8,string=c_p"` 21 | Dec byte `mp4:"7,size=8,dec"` 22 | Hex byte `mp4:"8,size=8,hex"` 23 | ISO639_2 []byte `mp4:"9,size=8,iso639-2"` 24 | UUID [16]byte `mp4:"10,size=8,uuid"` 25 | Hidden byte `mp4:"11,size=8,hidden"` 26 | Opt byte `mp4:"12,size=8,opt=0x000010"` 27 | NOpt byte `mp4:"13,size=8,nopt=0x000010"` 28 | DynOpt byte `mp4:"14,size=8,opt=dynamic"` 29 | Varint uint64 `mp4:"15,varint"` 30 | DynSize uint64 `mp4:"16,size=dynamic"` 31 | FixedLen []byte `mp4:"17,size=8,len=5"` 32 | DynLen []byte `mp4:"18,size=8,len=dynamic"` 33 | Ver byte `mp4:"19,size=8,ver=1"` 34 | NVer byte `mp4:"20,size=8,nver=1"` 35 | NotSorted22 byte `mp4:"22,size=8"` 36 | NotSorted23 byte `mp4:"23,size=8"` 37 | NotSorted21 byte `mp4:"21,size=8"` 38 | }{} 39 | 40 | fs := buildFields(box) 41 | require.Len(t, fs, 24) 42 | assert.Equal(t, &field{ 43 | name: "FullBox", 44 | order: 0, 45 | flags: fieldExtend, 46 | version: anyVersion, 47 | nVersion: anyVersion, 48 | length: LengthUnlimited, 49 | children: []*field{ 50 | { 51 | name: "Version", 52 | order: 0, 53 | version: anyVersion, 54 | nVersion: anyVersion, 55 | size: 8, 56 | length: LengthUnlimited, 57 | }, { 58 | name: "Flags", 59 | order: 1, 60 | version: anyVersion, 61 | nVersion: anyVersion, 62 | size: 8, 63 | length: LengthUnlimited, 64 | }, 65 | }, 66 | }, fs[0]) 67 | assert.Equal(t, &field{ 68 | name: "Int32", 69 | order: 1, 70 | version: anyVersion, 71 | nVersion: anyVersion, 72 | size: 32, 73 | length: LengthUnlimited, 74 | }, fs[1]) 75 | assert.Equal(t, &field{ 76 | name: "Int17", 77 | order: 2, 78 | version: anyVersion, 79 | nVersion: anyVersion, 80 | size: 17, 81 | length: LengthUnlimited, 82 | }, fs[2]) 83 | assert.Equal(t, &field{ 84 | name: "Uint15", 85 | order: 3, 86 | version: anyVersion, 87 | nVersion: anyVersion, 88 | size: 15, 89 | length: LengthUnlimited, 90 | }, fs[3]) 91 | assert.Equal(t, &field{ 92 | name: "Const", 93 | order: 4, 94 | version: anyVersion, 95 | nVersion: anyVersion, 96 | size: 8, 97 | length: LengthUnlimited, 98 | cnst: "0", 99 | }, fs[4]) 100 | assert.Equal(t, &field{ 101 | name: "String", 102 | order: 5, 103 | version: anyVersion, 104 | nVersion: anyVersion, 105 | size: 8, 106 | length: LengthUnlimited, 107 | flags: fieldString, 108 | strType: stringType_C, 109 | }, fs[5]) 110 | assert.Equal(t, &field{ 111 | name: "PString", 112 | order: 6, 113 | version: anyVersion, 114 | nVersion: anyVersion, 115 | size: 8, 116 | length: LengthUnlimited, 117 | flags: fieldString, 118 | strType: stringType_C_P, 119 | }, fs[6]) 120 | assert.Equal(t, &field{ 121 | name: "Dec", 122 | order: 7, 123 | version: anyVersion, 124 | nVersion: anyVersion, 125 | size: 8, 126 | length: LengthUnlimited, 127 | flags: fieldDec, 128 | }, fs[7]) 129 | assert.Equal(t, &field{ 130 | name: "Hex", 131 | order: 8, 132 | version: anyVersion, 133 | nVersion: anyVersion, 134 | size: 8, 135 | length: LengthUnlimited, 136 | flags: fieldHex, 137 | }, fs[8]) 138 | assert.Equal(t, &field{ 139 | name: "ISO639_2", 140 | order: 9, 141 | version: anyVersion, 142 | nVersion: anyVersion, 143 | size: 8, 144 | length: LengthUnlimited, 145 | flags: fieldISO639_2, 146 | }, fs[9]) 147 | assert.Equal(t, &field{ 148 | name: "UUID", 149 | order: 10, 150 | version: anyVersion, 151 | nVersion: anyVersion, 152 | size: 8, 153 | length: LengthUnlimited, 154 | flags: fieldUUID, 155 | }, fs[10]) 156 | assert.Equal(t, &field{ 157 | name: "Hidden", 158 | order: 11, 159 | version: anyVersion, 160 | nVersion: anyVersion, 161 | size: 8, 162 | length: LengthUnlimited, 163 | flags: fieldHidden, 164 | }, fs[11]) 165 | assert.Equal(t, &field{ 166 | name: "Opt", 167 | order: 12, 168 | version: anyVersion, 169 | nVersion: anyVersion, 170 | size: 8, 171 | length: LengthUnlimited, 172 | optFlag: 0x000010, 173 | }, fs[12]) 174 | assert.Equal(t, &field{ 175 | name: "NOpt", 176 | order: 13, 177 | version: anyVersion, 178 | nVersion: anyVersion, 179 | size: 8, 180 | length: LengthUnlimited, 181 | nOptFlag: 0x000010, 182 | }, fs[13]) 183 | assert.Equal(t, &field{ 184 | name: "DynOpt", 185 | order: 14, 186 | version: anyVersion, 187 | nVersion: anyVersion, 188 | size: 8, 189 | length: LengthUnlimited, 190 | flags: fieldOptDynamic, 191 | }, fs[14]) 192 | assert.Equal(t, &field{ 193 | name: "Varint", 194 | order: 15, 195 | version: anyVersion, 196 | nVersion: anyVersion, 197 | length: LengthUnlimited, 198 | flags: fieldVarint, 199 | }, fs[15]) 200 | assert.Equal(t, &field{ 201 | name: "DynSize", 202 | order: 16, 203 | version: anyVersion, 204 | nVersion: anyVersion, 205 | length: LengthUnlimited, 206 | flags: fieldSizeDynamic, 207 | }, fs[16]) 208 | assert.Equal(t, &field{ 209 | name: "FixedLen", 210 | order: 17, 211 | version: anyVersion, 212 | nVersion: anyVersion, 213 | size: 8, 214 | length: 5, 215 | }, fs[17]) 216 | assert.Equal(t, &field{ 217 | name: "DynLen", 218 | order: 18, 219 | version: anyVersion, 220 | nVersion: anyVersion, 221 | size: 8, 222 | length: LengthUnlimited, 223 | flags: fieldLengthDynamic, 224 | }, fs[18]) 225 | assert.Equal(t, &field{ 226 | name: "Ver", 227 | order: 19, 228 | version: 1, 229 | nVersion: anyVersion, 230 | size: 8, 231 | length: LengthUnlimited, 232 | }, fs[19]) 233 | assert.Equal(t, &field{ 234 | name: "NVer", 235 | order: 20, 236 | version: anyVersion, 237 | nVersion: 1, 238 | size: 8, 239 | length: LengthUnlimited, 240 | }, fs[20]) 241 | assert.Equal(t, &field{ 242 | name: "NotSorted21", 243 | order: 21, 244 | version: anyVersion, 245 | nVersion: anyVersion, 246 | size: 8, 247 | length: LengthUnlimited, 248 | }, fs[21]) 249 | assert.Equal(t, &field{ 250 | name: "NotSorted22", 251 | order: 22, 252 | version: anyVersion, 253 | nVersion: anyVersion, 254 | size: 8, 255 | length: LengthUnlimited, 256 | }, fs[22]) 257 | assert.Equal(t, &field{ 258 | name: "NotSorted23", 259 | order: 23, 260 | version: anyVersion, 261 | nVersion: anyVersion, 262 | size: 8, 263 | length: LengthUnlimited, 264 | }, fs[23]) 265 | } 266 | 267 | func TestResolveFieldInstance(t *testing.T) { 268 | fixedSize := uint(8) 269 | fixedLen := uint(1) 270 | dynSize1 := uint(16) 271 | dynLen1 := uint(2) 272 | dynSize2 := uint(32) 273 | dynLen2 := uint(4) 274 | cfo1 := struct { 275 | mockBox 276 | Box 277 | }{ 278 | mockBox: mockBox{ 279 | DynSizeMap: map[string]uint{"TestField": dynSize1}, 280 | DynLenMap: map[string]uint{"TestField": dynLen1}, 281 | }, 282 | } 283 | cfo2 := struct { 284 | mockBox 285 | Box 286 | }{ 287 | mockBox: mockBox{ 288 | DynSizeMap: map[string]uint{"TestField": dynSize2}, 289 | DynLenMap: map[string]uint{"TestField": dynLen2}, 290 | }, 291 | } 292 | nonCFO := struct{}{} 293 | 294 | testCases := []struct { 295 | name string 296 | f *field 297 | box IImmutableBox 298 | parent interface{} 299 | wantSize uint 300 | wantLen uint 301 | wantCFO ICustomFieldObject 302 | }{ 303 | { 304 | name: "dynamic size with non CustomFieldObject", 305 | f: &field{ 306 | name: "TestField", 307 | flags: fieldSizeDynamic, 308 | length: fixedLen, 309 | }, 310 | box: &cfo1, 311 | parent: &nonCFO, 312 | wantSize: dynSize1, 313 | wantLen: fixedLen, 314 | wantCFO: &cfo1, 315 | }, 316 | { 317 | name: "dynamic size with CustomFieldObject", 318 | f: &field{ 319 | name: "TestField", 320 | flags: fieldSizeDynamic, 321 | length: fixedLen, 322 | }, 323 | box: &cfo1, 324 | parent: &cfo2, 325 | wantSize: dynSize2, 326 | wantLen: fixedLen, 327 | wantCFO: &cfo2, 328 | }, 329 | { 330 | name: "dynamic length with non CustomFieldObject", 331 | f: &field{ 332 | name: "TestField", 333 | flags: fieldLengthDynamic, 334 | size: fixedSize, 335 | }, 336 | box: &cfo1, 337 | parent: &nonCFO, 338 | wantSize: fixedSize, 339 | wantLen: dynLen1, 340 | wantCFO: &cfo1, 341 | }, 342 | { 343 | name: "dynamic length with CustomFieldObject", 344 | f: &field{ 345 | name: "TestField", 346 | flags: fieldLengthDynamic, 347 | size: fixedSize, 348 | }, 349 | box: &cfo1, 350 | parent: &cfo2, 351 | wantSize: fixedSize, 352 | wantLen: dynLen2, 353 | wantCFO: &cfo2, 354 | }, 355 | } 356 | 357 | for _, tc := range testCases { 358 | t.Run(tc.name, func(t *testing.T) { 359 | fi := resolveFieldInstance(tc.f, tc.box, reflect.ValueOf(tc.parent).Elem(), Context{}) 360 | assert.Equal(t, tc.wantSize, fi.size) 361 | assert.Equal(t, tc.wantLen, fi.length) 362 | assert.Same(t, tc.wantCFO, fi.cfo) 363 | }) 364 | } 365 | } 366 | 367 | func TestIsTargetField(t *testing.T) { 368 | box := &struct { 369 | AnyTypeBox 370 | FullBox 371 | }{ 372 | FullBox: FullBox{ 373 | Version: 1, 374 | Flags: [3]byte{0x00, 0x00, 0x06}, 375 | }, 376 | } 377 | 378 | cfo := struct { 379 | mockBox 380 | Box 381 | }{ 382 | mockBox: mockBox{ 383 | DynOptMap: map[string]bool{ 384 | "DynEnabledField": true, 385 | "DynDisabledField": false, 386 | }, 387 | }, 388 | } 389 | 390 | testCases := []struct { 391 | name string 392 | fi *fieldInstance 393 | wants bool 394 | }{ 395 | { 396 | name: "normal", 397 | fi: &fieldInstance{ 398 | field: field{ 399 | name: "TestField", 400 | version: anyVersion, 401 | nVersion: anyVersion, 402 | }, 403 | cfo: &cfo, 404 | }, 405 | wants: true, 406 | }, 407 | { 408 | name: "ver=0", 409 | fi: &fieldInstance{ 410 | field: field{ 411 | name: "TestField", 412 | version: 0, 413 | nVersion: anyVersion, 414 | }, 415 | cfo: &cfo, 416 | }, 417 | wants: false, 418 | }, 419 | { 420 | name: "ver=1", 421 | fi: &fieldInstance{ 422 | field: field{ 423 | name: "TestField", 424 | version: 1, 425 | nVersion: anyVersion, 426 | }, 427 | cfo: &cfo, 428 | }, 429 | wants: true, 430 | }, 431 | { 432 | name: "nver=0", 433 | fi: &fieldInstance{ 434 | field: field{ 435 | name: "TestField", 436 | version: anyVersion, 437 | nVersion: 0, 438 | }, 439 | cfo: &cfo, 440 | }, 441 | wants: true, 442 | }, 443 | { 444 | name: "nver=1", 445 | fi: &fieldInstance{ 446 | field: field{ 447 | name: "TestField", 448 | version: anyVersion, 449 | nVersion: 1, 450 | }, 451 | cfo: &cfo, 452 | }, 453 | wants: false, 454 | }, 455 | { 456 | name: "opt=0x000001", 457 | fi: &fieldInstance{ 458 | field: field{ 459 | name: "TestField", 460 | version: anyVersion, 461 | nVersion: anyVersion, 462 | optFlag: 0x000001, 463 | }, 464 | cfo: &cfo, 465 | }, 466 | wants: false, 467 | }, 468 | { 469 | name: "opt=0x000002", 470 | fi: &fieldInstance{ 471 | field: field{ 472 | name: "TestField", 473 | version: anyVersion, 474 | nVersion: anyVersion, 475 | optFlag: 0x000002, 476 | }, 477 | cfo: &cfo, 478 | }, 479 | wants: true, 480 | }, 481 | { 482 | name: "opt=0x000004", 483 | fi: &fieldInstance{ 484 | field: field{ 485 | name: "TestField", 486 | version: anyVersion, 487 | nVersion: anyVersion, 488 | optFlag: 0x000004, 489 | }, 490 | cfo: &cfo, 491 | }, 492 | wants: true, 493 | }, 494 | { 495 | name: "opt=0x000008", 496 | fi: &fieldInstance{ 497 | field: field{ 498 | name: "TestField", 499 | version: anyVersion, 500 | nVersion: anyVersion, 501 | optFlag: 0x000008, 502 | }, 503 | cfo: &cfo, 504 | }, 505 | wants: false, 506 | }, 507 | { 508 | name: "nopt=0x000001", 509 | fi: &fieldInstance{ 510 | field: field{ 511 | name: "TestField", 512 | version: anyVersion, 513 | nVersion: anyVersion, 514 | nOptFlag: 0x000001, 515 | }, 516 | cfo: &cfo, 517 | }, 518 | wants: true, 519 | }, 520 | { 521 | name: "nopt=0x000002", 522 | fi: &fieldInstance{ 523 | field: field{ 524 | name: "TestField", 525 | version: anyVersion, 526 | nVersion: anyVersion, 527 | nOptFlag: 0x000002, 528 | }, 529 | cfo: &cfo, 530 | }, 531 | wants: false, 532 | }, 533 | { 534 | name: "opt=dynamic enabled", 535 | fi: &fieldInstance{ 536 | field: field{ 537 | name: "DynEnabledField", 538 | version: anyVersion, 539 | nVersion: anyVersion, 540 | flags: fieldOptDynamic, 541 | }, 542 | cfo: &cfo, 543 | }, 544 | wants: true, 545 | }, 546 | { 547 | name: "opt=dynamic disabled", 548 | fi: &fieldInstance{ 549 | field: field{ 550 | name: "DynDisabledField", 551 | version: anyVersion, 552 | nVersion: anyVersion, 553 | flags: fieldOptDynamic, 554 | }, 555 | cfo: &cfo, 556 | }, 557 | wants: false, 558 | }, 559 | } 560 | 561 | for _, tc := range testCases { 562 | t.Run(tc.name, func(t *testing.T) { 563 | assert.Equal(t, tc.wants, isTargetField(box, tc.fi, Context{})) 564 | }) 565 | } 566 | } 567 | --------------------------------------------------------------------------------