├── LICENSE ├── README.md ├── box.go ├── cli └── mp4tool.go ├── ctts.go ├── dinf.go ├── doc.go ├── dref.go ├── edts.go ├── elst.go ├── filter ├── clip.go ├── filter.go └── noop.go ├── free.go ├── ftyp.go ├── hdlr.go ├── iods.go ├── mdat.go ├── mdhd.go ├── mdia.go ├── meta.go ├── minf.go ├── moov.go ├── mp4.go ├── mvhd.go ├── smhd.go ├── stbl.go ├── stco.go ├── stsc.go ├── stsd.go ├── stss.go ├── stsz.go ├── stts.go ├── tkhd.go ├── trak.go ├── udta.go ├── vmhd.go └── wercker.yml /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jean-François Bustarret 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mp4 2 | 3 | 4 | [![wercker status](https://app.wercker.com/status/880648789317885e754d7054fa026b56/s/master "wercker status")](https://app.wercker.com/project/bykey/880648789317885e754d7054fa026b56) 5 | 6 | A encoder/decoder class, io.Reader and io.Writer compatible, usable for HTTP pseudo streaming 7 | 8 | For the complete MP4 specifications, see http://standards.iso.org/ittf/PubliclyAvailableStandards/c061988_ISO_IEC_14496-12_2012.zip and http://standards.iso.org/ittf/PubliclyAvailableStandards/c061989_ISO_IEC_15444-12_2012.zip 9 | 10 | ## Doc 11 | 12 | See http://godoc.org/github.com/jfbus/mp4 and http://godoc.org/github.com/jfbus/mp4/filter 13 | 14 | ## Warning 15 | 16 | Some boxes can have multiple formats (ctts, elst, tkhd, ...). Only the version 0 of those boxes is currently decoded (see https://github.com/jfbus/mp4/issues/7). 17 | Version 1 will be supported, and this will break a few things (e.g. some uint32 attributes will switch to uint64). 18 | 19 | ## CLI 20 | 21 | A CLI can be found in cli/mp4tool.go 22 | 23 | It can : 24 | 25 | * Display info about a media 26 | ``` 27 | mp4tool info file.mp4 28 | ``` 29 | * Copy a video (decode it and reencode it to another file, useful for debugging) 30 | ``` 31 | mp4tool copy in.mp4 out.mp4 32 | ``` 33 | * Generate a clip 34 | ``` 35 | mp4tool clip --start 10 --duration 30 in.mp4 out.mp4 36 | ``` 37 | 38 | (if you really want to generate a clip, you should use ffmpeg, you will ge better results) 39 | 40 | ## LICENSE 41 | 42 | See LICENSE -------------------------------------------------------------------------------- /box.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | ) 10 | 11 | const ( 12 | BoxHeaderSize = 8 13 | ) 14 | 15 | var ( 16 | ErrUnknownBoxType = errors.New("unknown box type") 17 | ErrTruncatedHeader = errors.New("truncated header") 18 | ErrBadFormat = errors.New("bad format") 19 | ) 20 | 21 | var decoders map[string]BoxDecoder 22 | 23 | func init() { 24 | decoders = map[string]BoxDecoder{ 25 | "ftyp": DecodeFtyp, 26 | "moov": DecodeMoov, 27 | "mvhd": DecodeMvhd, 28 | "iods": DecodeIods, 29 | "trak": DecodeTrak, 30 | "udta": DecodeUdta, 31 | "tkhd": DecodeTkhd, 32 | "edts": DecodeEdts, 33 | "elst": DecodeElst, 34 | "mdia": DecodeMdia, 35 | "minf": DecodeMinf, 36 | "mdhd": DecodeMdhd, 37 | "hdlr": DecodeHdlr, 38 | "vmhd": DecodeVmhd, 39 | "smhd": DecodeSmhd, 40 | "dinf": DecodeDinf, 41 | "dref": DecodeDref, 42 | "stbl": DecodeStbl, 43 | "stco": DecodeStco, 44 | "stsc": DecodeStsc, 45 | "stsz": DecodeStsz, 46 | "ctts": DecodeCtts, 47 | "stsd": DecodeStsd, 48 | "stts": DecodeStts, 49 | "stss": DecodeStss, 50 | "meta": DecodeMeta, 51 | "mdat": DecodeMdat, 52 | "free": DecodeFree, 53 | } 54 | } 55 | 56 | // The header of a box 57 | type BoxHeader struct { 58 | Type string 59 | Size uint32 60 | } 61 | 62 | // DecodeHeader decodes a box header (size + box type) 63 | func DecodeHeader(r io.Reader) (BoxHeader, error) { 64 | buf := make([]byte, BoxHeaderSize) 65 | n, err := r.Read(buf) 66 | if err != nil { 67 | return BoxHeader{}, err 68 | } 69 | if n != BoxHeaderSize { 70 | return BoxHeader{}, ErrTruncatedHeader 71 | } 72 | return BoxHeader{string(buf[4:8]), binary.BigEndian.Uint32(buf[0:4])}, nil 73 | } 74 | 75 | // EncodeHeader encodes a box header to a writer 76 | func EncodeHeader(b Box, w io.Writer) error { 77 | buf := make([]byte, BoxHeaderSize) 78 | binary.BigEndian.PutUint32(buf, uint32(b.Size())) 79 | strtobuf(buf[4:], b.Type(), 4) 80 | _, err := w.Write(buf) 81 | return err 82 | } 83 | 84 | // A box 85 | type Box interface { 86 | Type() string 87 | Size() int 88 | Encode(w io.Writer) error 89 | } 90 | 91 | type BoxDecoder func(r io.Reader) (Box, error) 92 | 93 | // DecodeBox decodes a box 94 | func DecodeBox(h BoxHeader, r io.Reader) (Box, error) { 95 | d := decoders[h.Type] 96 | if d == nil { 97 | log.Printf("Error while decoding %s : unknown box type", h.Type) 98 | return nil, ErrUnknownBoxType 99 | } 100 | b, err := d(io.LimitReader(r, int64(h.Size-BoxHeaderSize))) 101 | if err != nil { 102 | log.Printf("Error while decoding %s : %s", h.Type, err) 103 | return nil, err 104 | } 105 | return b, nil 106 | } 107 | 108 | // DecodeContainer decodes a container box 109 | func DecodeContainer(r io.Reader) ([]Box, error) { 110 | l := []Box{} 111 | for { 112 | h, err := DecodeHeader(r) 113 | if err == io.EOF { 114 | return l, nil 115 | } 116 | if err != nil { 117 | return l, err 118 | } 119 | b, err := DecodeBox(h, r) 120 | if err != nil { 121 | return l, err 122 | } 123 | l = append(l, b) 124 | } 125 | } 126 | 127 | // An 8.8 fixed point number 128 | type Fixed16 uint16 129 | 130 | func (f Fixed16) String() string { 131 | return fmt.Sprintf("%d.%d", uint16(f)>>8, uint16(f)&7) 132 | } 133 | 134 | func fixed16(bytes []byte) Fixed16 { 135 | return Fixed16(binary.BigEndian.Uint16(bytes)) 136 | } 137 | 138 | func putFixed16(bytes []byte, i Fixed16) { 139 | binary.BigEndian.PutUint16(bytes, uint16(i)) 140 | } 141 | 142 | // A 16.16 fixed point number 143 | type Fixed32 uint32 144 | 145 | func (f Fixed32) String() string { 146 | return fmt.Sprintf("%d.%d", uint32(f)>>16, uint32(f)&15) 147 | } 148 | 149 | func fixed32(bytes []byte) Fixed32 { 150 | return Fixed32(binary.BigEndian.Uint32(bytes)) 151 | } 152 | 153 | func putFixed32(bytes []byte, i Fixed32) { 154 | binary.BigEndian.PutUint32(bytes, uint32(i)) 155 | } 156 | 157 | func strtobuf(out []byte, str string, l int) { 158 | in := []byte(str) 159 | if l < len(in) { 160 | copy(out, in) 161 | } else { 162 | copy(out, in[0:l]) 163 | } 164 | } 165 | 166 | func makebuf(b Box) []byte { 167 | return make([]byte, b.Size()-BoxHeaderSize) 168 | } 169 | -------------------------------------------------------------------------------- /cli/mp4tool.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | cli "github.com/jawher/mow.cli" 9 | "github.com/jfbus/mp4" 10 | "github.com/jfbus/mp4/filter" 11 | ) 12 | 13 | func main() { 14 | cmd := cli.App("mp4tool", "MP4 command line tool") 15 | 16 | cmd.Command("info", "Displays information about a media", func(cmd *cli.Cmd) { 17 | file := cmd.StringArg("FILE", "", "the file to display") 18 | cmd.Action = func() { 19 | fd, err := os.Open(*file) 20 | defer fd.Close() 21 | v, err := mp4.Decode(fd) 22 | if err != nil { 23 | fmt.Println(err) 24 | } 25 | v.Dump() 26 | } 27 | }) 28 | 29 | cmd.Command("clip", "Generates a clip", func(cmd *cli.Cmd) { 30 | start := cmd.IntOpt("s start", 0, "start time (sec)") 31 | duration := cmd.IntOpt("d duration", 10, "duration (sec)") 32 | src := cmd.StringArg("SRC", "", "the source file name") 33 | dst := cmd.StringArg("DST", "", "the destination file name") 34 | cmd.Action = func() { 35 | in, err := os.Open(*src) 36 | if err != nil { 37 | fmt.Println(err) 38 | } 39 | defer in.Close() 40 | v, err := mp4.Decode(in) 41 | if err != nil { 42 | fmt.Println(err) 43 | } 44 | out, err := os.Create(*dst) 45 | if err != nil { 46 | fmt.Println(err) 47 | } 48 | defer out.Close() 49 | filter.EncodeFiltered(out, v, filter.Clip(time.Duration(*start)*time.Second, time.Duration(*duration)*time.Second)) 50 | } 51 | }) 52 | 53 | cmd.Command("copy", "Decodes a media and reencodes it to another file", func(cmd *cli.Cmd) { 54 | src := cmd.StringArg("SRC", "", "the source file name") 55 | dst := cmd.StringArg("DST", "", "the destination file name") 56 | cmd.Action = func() { 57 | in, err := os.Open(*src) 58 | if err != nil { 59 | fmt.Println(err) 60 | } 61 | defer in.Close() 62 | v, err := mp4.Decode(in) 63 | if err != nil { 64 | fmt.Println(err) 65 | } 66 | out, err := os.Create(*dst) 67 | if err != nil { 68 | fmt.Println(err) 69 | } 70 | defer out.Close() 71 | v.Encode(out) 72 | } 73 | }) 74 | cmd.Run(os.Args) 75 | } 76 | -------------------------------------------------------------------------------- /ctts.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "io/ioutil" 7 | ) 8 | 9 | // Composition Time to Sample Box (ctts - optional) 10 | // 11 | // Contained in: Sample Table Box (stbl) 12 | // 13 | // Status: version 0 decoded. version 1 uses int32 for offsets 14 | type CttsBox struct { 15 | Version byte 16 | Flags [3]byte 17 | SampleCount []uint32 18 | SampleOffset []uint32 // int32 for version 1 19 | } 20 | 21 | func DecodeCtts(r io.Reader) (Box, error) { 22 | data, err := ioutil.ReadAll(r) 23 | if err != nil { 24 | return nil, err 25 | } 26 | b := &CttsBox{ 27 | Version: data[0], 28 | Flags: [3]byte{data[1], data[2], data[3]}, 29 | SampleCount: []uint32{}, 30 | SampleOffset: []uint32{}, 31 | } 32 | ec := binary.BigEndian.Uint32(data[4:8]) 33 | for i := 0; i < int(ec); i++ { 34 | s_count := binary.BigEndian.Uint32(data[(8 + 8*i):(12 + 8*i)]) 35 | s_offset := binary.BigEndian.Uint32(data[(12 + 8*i):(16 + 8*i)]) 36 | b.SampleCount = append(b.SampleCount, s_count) 37 | b.SampleOffset = append(b.SampleOffset, s_offset) 38 | } 39 | return b, nil 40 | } 41 | 42 | func (b *CttsBox) Type() string { 43 | return "ctts" 44 | } 45 | 46 | func (b *CttsBox) Size() int { 47 | return BoxHeaderSize + 8 + len(b.SampleCount)*8 48 | } 49 | 50 | func (b *CttsBox) Encode(w io.Writer) error { 51 | err := EncodeHeader(b, w) 52 | if err != nil { 53 | return err 54 | } 55 | buf := makebuf(b) 56 | buf[0] = b.Version 57 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 58 | binary.BigEndian.PutUint32(buf[4:], uint32(len(b.SampleCount))) 59 | for i := range b.SampleCount { 60 | binary.BigEndian.PutUint32(buf[8+8*i:], b.SampleCount[i]) 61 | binary.BigEndian.PutUint32(buf[12+8*i:], b.SampleOffset[i]) 62 | } 63 | _, err = w.Write(buf) 64 | return err 65 | } 66 | -------------------------------------------------------------------------------- /dinf.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import "io" 4 | 5 | // Data Information Box (dinf - mandatory) 6 | // 7 | // Contained in : Media Information Box (minf) or Meta Box (meta) 8 | // 9 | // Status : decoded 10 | type DinfBox struct { 11 | Dref *DrefBox 12 | } 13 | 14 | func DecodeDinf(r io.Reader) (Box, error) { 15 | l, err := DecodeContainer(r) 16 | if err != nil { 17 | return nil, err 18 | } 19 | d := &DinfBox{} 20 | for _, b := range l { 21 | switch b.Type() { 22 | case "dref": 23 | d.Dref = b.(*DrefBox) 24 | default: 25 | return nil, ErrBadFormat 26 | } 27 | } 28 | return d, nil 29 | } 30 | 31 | func (b *DinfBox) Type() string { 32 | return "dinf" 33 | } 34 | 35 | func (b *DinfBox) Size() int { 36 | return BoxHeaderSize + b.Dref.Size() 37 | } 38 | 39 | func (b *DinfBox) Encode(w io.Writer) error { 40 | err := EncodeHeader(b, w) 41 | if err != nil { 42 | return err 43 | } 44 | return b.Dref.Encode(w) 45 | } 46 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package mp4 implements encoding/decoding of MP4 media. 3 | */ 4 | package mp4 5 | -------------------------------------------------------------------------------- /dref.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | ) 7 | 8 | // Data Reference Box (dref - mandatory) 9 | // 10 | // Contained id: Data Information Box (dinf) 11 | // 12 | // Status: not decoded 13 | // 14 | // Defines the location of the media data. If the data for the track is located in the same file 15 | // it contains nothing useful. 16 | type DrefBox struct { 17 | Version byte 18 | Flags [3]byte 19 | notDecoded []byte 20 | } 21 | 22 | func DecodeDref(r io.Reader) (Box, error) { 23 | data, err := ioutil.ReadAll(r) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return &DrefBox{ 28 | Version: data[0], 29 | Flags: [3]byte{data[1], data[2], data[3]}, 30 | notDecoded: data[4:], 31 | }, nil 32 | } 33 | 34 | func (b *DrefBox) Type() string { 35 | return "dref" 36 | } 37 | 38 | func (b *DrefBox) Size() int { 39 | return BoxHeaderSize + 4 + len(b.notDecoded) 40 | } 41 | 42 | func (b *DrefBox) Encode(w io.Writer) error { 43 | err := EncodeHeader(b, w) 44 | if err != nil { 45 | return err 46 | } 47 | buf := makebuf(b) 48 | buf[0] = b.Version 49 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 50 | copy(buf[4:], b.notDecoded) 51 | _, err = w.Write(buf) 52 | return err 53 | } 54 | -------------------------------------------------------------------------------- /edts.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import "io" 4 | 5 | // Edit Box (edts - optional) 6 | // 7 | // Contained in: Track Box ("trak") 8 | // 9 | // Status: decoded 10 | // 11 | // The edit box maps the presentation timeline to the media-time line 12 | type EdtsBox struct { 13 | Elst *ElstBox 14 | } 15 | 16 | func DecodeEdts(r io.Reader) (Box, error) { 17 | l, err := DecodeContainer(r) 18 | if err != nil { 19 | return nil, err 20 | } 21 | e := &EdtsBox{} 22 | for _, b := range l { 23 | switch b.Type() { 24 | case "elst": 25 | e.Elst = b.(*ElstBox) 26 | default: 27 | return nil, ErrBadFormat 28 | } 29 | } 30 | return e, nil 31 | } 32 | 33 | func (b *EdtsBox) Type() string { 34 | return "edts" 35 | } 36 | 37 | func (b *EdtsBox) Size() int { 38 | return BoxHeaderSize + b.Elst.Size() 39 | } 40 | 41 | func (b *EdtsBox) Dump() { 42 | b.Elst.Dump() 43 | } 44 | 45 | func (b *EdtsBox) Encode(w io.Writer) error { 46 | err := EncodeHeader(b, w) 47 | if err != nil { 48 | return err 49 | } 50 | return b.Elst.Encode(w) 51 | } 52 | -------------------------------------------------------------------------------- /elst.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | ) 9 | 10 | // Edit List Box (elst - optional) 11 | // 12 | // Contained in : Edit Box (edts) 13 | // 14 | // Status: version 0 decoded. version 1 not supported 15 | type ElstBox struct { 16 | Version byte 17 | Flags [3]byte 18 | SegmentDuration, MediaTime []uint32 // should be uint32/int32 for version 0 and uint64/int32 for version 1 19 | MediaRateInteger, MediaRateFraction []uint16 // should be int16 20 | } 21 | 22 | func DecodeElst(r io.Reader) (Box, error) { 23 | data, err := ioutil.ReadAll(r) 24 | if err != nil { 25 | return nil, err 26 | } 27 | b := &ElstBox{ 28 | Version: data[0], 29 | Flags: [3]byte{data[1], data[2], data[3]}, 30 | SegmentDuration: []uint32{}, 31 | MediaTime: []uint32{}, 32 | MediaRateInteger: []uint16{}, 33 | MediaRateFraction: []uint16{}, 34 | } 35 | ec := binary.BigEndian.Uint32(data[4:8]) 36 | for i := 0; i < int(ec); i++ { 37 | sd := binary.BigEndian.Uint32(data[(8 + 12*i):(12 + 12*i)]) 38 | mt := binary.BigEndian.Uint32(data[(12 + 12*i):(16 + 12*i)]) 39 | mri := binary.BigEndian.Uint16(data[(16 + 12*i):(18 + 12*i)]) 40 | mrf := binary.BigEndian.Uint16(data[(18 + 12*i):(20 + 12*i)]) 41 | b.SegmentDuration = append(b.SegmentDuration, sd) 42 | b.MediaTime = append(b.MediaTime, mt) 43 | b.MediaRateInteger = append(b.MediaRateInteger, mri) 44 | b.MediaRateFraction = append(b.MediaRateFraction, mrf) 45 | } 46 | return b, nil 47 | } 48 | 49 | func (b *ElstBox) Type() string { 50 | return "elst" 51 | } 52 | 53 | func (b *ElstBox) Size() int { 54 | return BoxHeaderSize + 8 + len(b.SegmentDuration)*12 55 | } 56 | 57 | func (b *ElstBox) Dump() { 58 | fmt.Println("Segment Duration:") 59 | for i, d := range b.SegmentDuration { 60 | fmt.Printf(" #%d: %d units\n", i, d) 61 | } 62 | } 63 | 64 | func (b *ElstBox) Encode(w io.Writer) error { 65 | err := EncodeHeader(b, w) 66 | if err != nil { 67 | return err 68 | } 69 | buf := make([]byte, b.Size()-BoxHeaderSize) 70 | buf[0] = b.Version 71 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 72 | binary.BigEndian.PutUint32(buf[4:], uint32(len(b.SegmentDuration))) 73 | for i := range b.SegmentDuration { 74 | binary.BigEndian.PutUint32(buf[8+12*i:], b.SegmentDuration[i]) 75 | binary.BigEndian.PutUint32(buf[12+12*i:], b.MediaTime[i]) 76 | binary.BigEndian.PutUint16(buf[16+12*i:], b.MediaRateInteger[i]) 77 | binary.BigEndian.PutUint16(buf[18+12*i:], b.MediaRateFraction[i]) 78 | } 79 | _, err = w.Write(buf) 80 | return err 81 | } 82 | -------------------------------------------------------------------------------- /filter/clip.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "log" 7 | "sort" 8 | "time" 9 | 10 | "github.com/jfbus/mp4" 11 | ) 12 | 13 | var ( 14 | ErrInvalidDuration = errors.New("invalid duration") 15 | ErrClipOutside = errors.New("clip zone is outside video") 16 | ErrTruncatedChunk = errors.New("chunk was truncated") 17 | ) 18 | 19 | type chunk struct { 20 | track int 21 | index int 22 | firstTC, lastTC time.Duration 23 | descriptionID uint32 24 | oldOffset uint32 25 | samples []uint32 26 | firstSample, lastSample uint32 27 | keyFrame bool 28 | skip bool 29 | } 30 | 31 | func (c *chunk) size() uint32 { 32 | var sz uint32 33 | for _, ssz := range c.samples { 34 | sz += ssz 35 | } 36 | return sz 37 | } 38 | 39 | type mdat []*chunk 40 | 41 | func (m mdat) Len() int { 42 | return len(m) 43 | } 44 | 45 | func (m mdat) Less(i, j int) bool { 46 | return m[i].oldOffset < m[j].oldOffset 47 | } 48 | 49 | func (m mdat) Swap(i, j int) { 50 | m[i], m[j] = m[j], m[i] 51 | } 52 | 53 | func (m mdat) firstSample(tnum int, timecode time.Duration) uint32 { 54 | for _, c := range m { 55 | if c.track != tnum { 56 | continue 57 | } 58 | if timecode >= c.firstTC && timecode <= c.lastTC { 59 | return c.firstSample 60 | } 61 | } 62 | return 0 63 | } 64 | 65 | func (m mdat) lastSample(tnum int, timecode time.Duration) uint32 { 66 | for _, c := range m { 67 | if c.track != tnum { 68 | continue 69 | } 70 | if timecode >= c.firstTC && timecode < c.lastTC { 71 | return c.lastSample 72 | } 73 | } 74 | return 0 75 | } 76 | 77 | type clipFilter struct { 78 | err error 79 | begin, end time.Duration 80 | mdatSize uint32 81 | chunks mdat 82 | } 83 | 84 | // Clip returns a filter that extracts a clip between begin and begin + duration (in seconds, starting at 0) 85 | // Il will try to include a key frame at the beginning, and keeps the same chunks as the origin media 86 | func Clip(begin, duration time.Duration) Filter { 87 | f := &clipFilter{begin: begin, end: begin + duration} 88 | if begin < 0 { 89 | f.err = ErrClipOutside 90 | } 91 | return f 92 | } 93 | 94 | func (f *clipFilter) FilterMoov(m *mp4.MoovBox) error { 95 | if f.err != nil { 96 | return f.err 97 | } 98 | if f.begin > time.Second*time.Duration(m.Mvhd.Duration)/time.Duration(m.Mvhd.Timescale) { 99 | return ErrClipOutside 100 | } 101 | if f.end > time.Second*time.Duration(m.Mvhd.Duration)/time.Duration(m.Mvhd.Timescale) || f.end == f.begin { 102 | f.end = time.Second * time.Duration(m.Mvhd.Duration) / time.Duration(m.Mvhd.Timescale) 103 | } 104 | oldSize := m.Size() 105 | f.chunks = []*chunk{} 106 | for tnum, t := range m.Trak { 107 | f.buildChunkList(tnum, t) 108 | } 109 | f.syncToKF() 110 | for tnum, t := range m.Trak { 111 | // update stts, find first/last sample 112 | f.updateSamples(tnum, t) 113 | f.updateChunks(tnum, t) 114 | // co64 ? 115 | } 116 | f.updateDurations(m) 117 | sort.Sort(f.chunks) 118 | for _, c := range f.chunks { 119 | sz := 0 120 | for _, ssz := range c.samples { 121 | sz += int(ssz) 122 | } 123 | } 124 | deltaOffset := m.Size() - oldSize 125 | f.mdatSize = f.updateChunkOffsets(m, deltaOffset) 126 | return nil 127 | } 128 | 129 | func (f *clipFilter) syncToKF() { 130 | var tc time.Duration 131 | for _, c := range f.chunks { 132 | if c.keyFrame && c.firstTC <= f.begin { 133 | tc = c.firstTC 134 | } 135 | } 136 | f.end += f.begin - tc 137 | f.begin = tc 138 | } 139 | 140 | func (f *clipFilter) buildChunkList(tnum int, t *mp4.TrakBox) { 141 | stsz := t.Mdia.Minf.Stbl.Stsz 142 | stsc := t.Mdia.Minf.Stbl.Stsc 143 | stco := t.Mdia.Minf.Stbl.Stco 144 | stts := t.Mdia.Minf.Stbl.Stts 145 | stss := t.Mdia.Minf.Stbl.Stss 146 | timescale := t.Mdia.Mdhd.Timescale 147 | sci, ssi, ski := 0, 0, 0 148 | for i, off := range stco.ChunkOffset { 149 | c := &chunk{ 150 | track: tnum, 151 | index: i + 1, 152 | oldOffset: uint32(off), 153 | samples: []uint32{}, 154 | firstSample: uint32(ssi + 1), 155 | firstTC: stts.GetTimeCode(uint32(ssi+1), timescale), 156 | } 157 | if sci < len(stsc.FirstChunk)-1 && c.index >= int(stsc.FirstChunk[sci+1]) { 158 | sci++ 159 | } 160 | c.descriptionID = stsc.SampleDescriptionID[sci] 161 | samples := stsc.SamplesPerChunk[sci] 162 | for samples > 0 { 163 | c.samples = append(c.samples, stsz.GetSampleSize(ssi+1)) 164 | ssi++ 165 | samples-- 166 | } 167 | c.lastSample = uint32(ssi) 168 | c.lastTC = stts.GetTimeCode(c.lastSample+1, timescale) 169 | if stss != nil { 170 | for ski < len(stss.SampleNumber) && stss.SampleNumber[ski] < c.lastSample { 171 | c.keyFrame = true 172 | ski++ 173 | } 174 | } 175 | f.chunks = append(f.chunks, c) 176 | } 177 | } 178 | 179 | func (f *clipFilter) updateSamples(tnum int, t *mp4.TrakBox) { 180 | // stts - sample duration 181 | stts := t.Mdia.Minf.Stbl.Stts 182 | oldCount, oldDelta := stts.SampleCount, stts.SampleTimeDelta 183 | stts.SampleCount, stts.SampleTimeDelta = []uint32{}, []uint32{} 184 | 185 | firstSample := f.chunks.firstSample(tnum, f.begin) 186 | lastSample := f.chunks.lastSample(tnum, f.end) 187 | 188 | log.Printf("first sample %d, last %d", firstSample, lastSample) 189 | sample := uint32(1) 190 | for i := 0; i < len(oldCount) && sample < lastSample; i++ { 191 | if sample+oldCount[i] >= firstSample { 192 | var current uint32 193 | switch { 194 | case sample < firstSample && sample+oldCount[i] > lastSample: 195 | current = lastSample - firstSample + 1 196 | case sample < firstSample: 197 | current = oldCount[i] + sample - firstSample 198 | case sample+oldCount[i] > lastSample: 199 | current = oldCount[i] + sample - lastSample 200 | default: 201 | current = oldCount[i] 202 | } 203 | stts.SampleCount = append(stts.SampleCount, current) 204 | stts.SampleTimeDelta = append(stts.SampleTimeDelta, oldDelta[i]) 205 | } 206 | sample += oldCount[i] 207 | } 208 | 209 | // stss (key frames) 210 | stss := t.Mdia.Minf.Stbl.Stss 211 | if stss != nil { 212 | oldNumber := stss.SampleNumber 213 | stss.SampleNumber = []uint32{} 214 | for _, n := range oldNumber { 215 | if n >= firstSample && n <= lastSample { 216 | stss.SampleNumber = append(stss.SampleNumber, n-uint32(firstSample)+1) 217 | } 218 | } 219 | } 220 | 221 | // stsz (sample sizes) 222 | stsz := t.Mdia.Minf.Stbl.Stsz 223 | oldSize := stsz.SampleSize 224 | stsz.SampleSize = []uint32{} 225 | for n, sz := range oldSize { 226 | if uint32(n) >= firstSample-1 && uint32(n) <= lastSample-1 { 227 | stsz.SampleSize = append(stsz.SampleSize, sz) 228 | } 229 | } 230 | log.Printf("stsz => %d", len(stsz.SampleSize)) 231 | 232 | // ctts - time offsets 233 | ctts := t.Mdia.Minf.Stbl.Ctts 234 | if ctts != nil { 235 | oldCount, oldOffset := ctts.SampleCount, ctts.SampleOffset 236 | ctts.SampleCount, ctts.SampleOffset = []uint32{}, []uint32{} 237 | sample := uint32(1) 238 | for i := 0; i < len(oldCount) && sample < lastSample; i++ { 239 | if sample+oldCount[i] >= firstSample { 240 | current := oldCount[i] 241 | if sample < firstSample && sample+oldCount[i] > firstSample { 242 | current += sample - firstSample 243 | } 244 | if sample+oldCount[i] > lastSample { 245 | current += lastSample - sample - oldCount[i] 246 | } 247 | 248 | ctts.SampleCount = append(ctts.SampleCount, current) 249 | ctts.SampleOffset = append(ctts.SampleOffset, oldOffset[i]) 250 | } 251 | sample += oldCount[i] 252 | } 253 | } 254 | 255 | } 256 | 257 | func (f *clipFilter) updateChunks(tnum int, t *mp4.TrakBox) { 258 | // stsc (sample to chunk) - full rebuild 259 | stsc := t.Mdia.Minf.Stbl.Stsc 260 | stsc.FirstChunk, stsc.SamplesPerChunk, stsc.SampleDescriptionID = []uint32{}, []uint32{}, []uint32{} 261 | var firstChunk *chunk 262 | var index, firstIndex uint32 263 | firstSample := f.chunks.firstSample(tnum, f.begin) 264 | lastSample := f.chunks.lastSample(tnum, f.end) 265 | for _, c := range f.chunks { 266 | if c.track != tnum { 267 | continue 268 | } 269 | if c.firstSample > lastSample || c.lastSample < firstSample { 270 | c.skip = true 271 | continue 272 | } 273 | index++ 274 | if firstChunk == nil { 275 | firstChunk = c 276 | firstIndex = index 277 | } 278 | if len(c.samples) != len(firstChunk.samples) || c.descriptionID != firstChunk.descriptionID { 279 | stsc.FirstChunk = append(stsc.FirstChunk, firstIndex) 280 | stsc.SamplesPerChunk = append(stsc.SamplesPerChunk, uint32(len(firstChunk.samples))) 281 | stsc.SampleDescriptionID = append(stsc.SampleDescriptionID, firstChunk.descriptionID) 282 | firstChunk = c 283 | firstIndex = index 284 | } 285 | } 286 | if firstChunk != nil { 287 | stsc.FirstChunk = append(stsc.FirstChunk, firstIndex) 288 | stsc.SamplesPerChunk = append(stsc.SamplesPerChunk, uint32(len(firstChunk.samples))) 289 | stsc.SampleDescriptionID = append(stsc.SampleDescriptionID, firstChunk.descriptionID) 290 | } 291 | 292 | // stco (chunk offsets) - build empty table to compute moov box size 293 | stco := t.Mdia.Minf.Stbl.Stco 294 | stco.ChunkOffset = make([]uint32, index) 295 | } 296 | 297 | func (f *clipFilter) updateChunkOffsets(m *mp4.MoovBox, deltaOff int) uint32 { 298 | stco, i := make([]*mp4.StcoBox, len(m.Trak)), make([]int, len(m.Trak)) 299 | for tnum, t := range m.Trak { 300 | stco[tnum] = t.Mdia.Minf.Stbl.Stco 301 | } 302 | var offset, sz uint32 303 | for _, c := range f.chunks { 304 | if offset == 0 { 305 | offset = uint32(int(c.oldOffset) + deltaOff) 306 | } 307 | if !c.skip { 308 | stco[c.track].ChunkOffset[i[c.track]] = offset + sz 309 | i[c.track]++ 310 | sz += c.size() 311 | } 312 | } 313 | return sz 314 | } 315 | 316 | func (f *clipFilter) updateDurations(m *mp4.MoovBox) { 317 | timescale := m.Mvhd.Timescale 318 | m.Mvhd.Duration = 0 319 | for tnum, t := range m.Trak { 320 | var start, end time.Duration 321 | for _, c := range f.chunks { 322 | if c.track != tnum || c.skip { 323 | continue 324 | } 325 | if start == 0 || c.firstTC < start { 326 | start = c.firstTC 327 | } 328 | if end == 0 || c.lastTC > end { 329 | end = c.lastTC 330 | } 331 | } 332 | t.Mdia.Mdhd.Duration = uint32((end - start) * time.Duration(t.Mdia.Mdhd.Timescale) / time.Second) 333 | t.Tkhd.Duration = uint32((end - start) * time.Duration(timescale) / time.Second) 334 | if t.Tkhd.Duration > m.Mvhd.Duration { 335 | m.Mvhd.Duration = t.Tkhd.Duration 336 | } 337 | } 338 | } 339 | 340 | func (f *clipFilter) FilterMdat(w io.Writer, m *mp4.MdatBox) error { 341 | if f.err != nil { 342 | return f.err 343 | } 344 | m.ContentSize = f.mdatSize 345 | err := mp4.EncodeHeader(m, w) 346 | if err != nil { 347 | return err 348 | } 349 | var bufSize uint32 350 | for _, c := range f.chunks { 351 | if c.size() > bufSize { 352 | bufSize = c.size() 353 | } 354 | } 355 | buffer := make([]byte, bufSize) 356 | for _, c := range f.chunks { 357 | s := c.size() 358 | // Seek if the reader supports it 359 | if rs, seekable := m.Reader().(io.Seeker); c.skip && seekable { 360 | _, err := rs.Seek(int64(s), 1) 361 | if err != nil { 362 | return err 363 | } 364 | continue 365 | } 366 | // Read otherwise, and only write if the chunk was not skipped 367 | n, err := io.ReadFull(m.Reader(), buffer[:s]) 368 | if err != nil { 369 | return err 370 | } 371 | if n != int(s) { 372 | return ErrTruncatedChunk 373 | } 374 | if !c.skip { 375 | n, err = w.Write(buffer[:s]) 376 | if err != nil { 377 | return err 378 | } 379 | if n != int(s) { 380 | return ErrTruncatedChunk 381 | } 382 | } 383 | } 384 | return nil 385 | } 386 | -------------------------------------------------------------------------------- /filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/jfbus/mp4" 7 | ) 8 | 9 | type Filter interface { 10 | // Updates the moov box 11 | FilterMoov(m *mp4.MoovBox) error 12 | // Filters the Mdat data and writes it to w 13 | FilterMdat(w io.Writer, m *mp4.MdatBox) error 14 | } 15 | 16 | // EncodeFiltered encodes a media to a writer, filtering the media using the specified filter 17 | func EncodeFiltered(w io.Writer, m *mp4.MP4, f Filter) error { 18 | err := m.Ftyp.Encode(w) 19 | if err != nil { 20 | return err 21 | } 22 | err = f.FilterMoov(m.Moov) 23 | if err != nil { 24 | return err 25 | } 26 | err = m.Moov.Encode(w) 27 | if err != nil { 28 | return err 29 | } 30 | for _, b := range m.Boxes() { 31 | if b.Type() != "ftyp" && b.Type() != "moov" && b.Type() != "mdat" { 32 | err = b.Encode(w) 33 | if err != nil { 34 | return err 35 | } 36 | } 37 | } 38 | return f.FilterMdat(w, m.Mdat) 39 | } 40 | -------------------------------------------------------------------------------- /filter/noop.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/jfbus/mp4" 7 | ) 8 | 9 | type noopFilter struct{} 10 | 11 | // Noop returns a filter that does nothing 12 | func Noop() Filter { 13 | return &noopFilter{} 14 | } 15 | 16 | func (f *noopFilter) FilterMoov(m *mp4.MoovBox) error { 17 | return nil 18 | } 19 | 20 | func (f *noopFilter) FilterMdat(w io.Writer, m *mp4.MdatBox) error { 21 | err := mp4.EncodeHeader(m, w) 22 | if err == nil { 23 | _, err = io.Copy(w, m.Reader()) 24 | } 25 | return err 26 | } 27 | -------------------------------------------------------------------------------- /free.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | ) 7 | 8 | // File Type Box (ftyp - mandatory) 9 | // 10 | // Status: decoded 11 | type FreeBox struct { 12 | notDecoded []byte 13 | } 14 | 15 | func DecodeFree(r io.Reader) (Box, error) { 16 | data, err := ioutil.ReadAll(r) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return &FreeBox{data}, nil 21 | } 22 | 23 | func (b *FreeBox) Type() string { 24 | return "free" 25 | } 26 | 27 | func (b *FreeBox) Size() int { 28 | return BoxHeaderSize + len(b.notDecoded) 29 | } 30 | 31 | func (b *FreeBox) Encode(w io.Writer) error { 32 | err := EncodeHeader(b, w) 33 | if err != nil { 34 | return err 35 | } 36 | _, err = w.Write(b.notDecoded) 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /ftyp.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | ) 8 | 9 | // File Type Box (ftyp - mandatory) 10 | // 11 | // Status: decoded 12 | type FtypBox struct { 13 | MajorBrand string 14 | MinorVersion []byte 15 | CompatibleBrands []string 16 | } 17 | 18 | func DecodeFtyp(r io.Reader) (Box, error) { 19 | data, err := ioutil.ReadAll(r) 20 | if err != nil { 21 | return nil, err 22 | } 23 | b := &FtypBox{ 24 | MajorBrand: string(data[0:4]), 25 | MinorVersion: data[4:8], 26 | CompatibleBrands: []string{}, 27 | } 28 | if len(data) > 8 { 29 | for i := 8; i < len(data); i += 4 { 30 | b.CompatibleBrands = append(b.CompatibleBrands, string(data[i:i+4])) 31 | } 32 | } 33 | return b, nil 34 | } 35 | 36 | func (b *FtypBox) Type() string { 37 | return "ftyp" 38 | } 39 | 40 | func (b *FtypBox) Size() int { 41 | return BoxHeaderSize + 8 + 4*len(b.CompatibleBrands) 42 | } 43 | 44 | func (b *FtypBox) Dump() { 45 | fmt.Printf("File Type: %s\n", b.MajorBrand) 46 | } 47 | 48 | func (b *FtypBox) Encode(w io.Writer) error { 49 | err := EncodeHeader(b, w) 50 | if err != nil { 51 | return err 52 | } 53 | buf := makebuf(b) 54 | strtobuf(buf, b.MajorBrand, 4) 55 | copy(buf[4:], b.MinorVersion) 56 | for i, c := range b.CompatibleBrands { 57 | strtobuf(buf[8+i*4:], c, 4) 58 | } 59 | _, err = w.Write(buf) 60 | return err 61 | } 62 | -------------------------------------------------------------------------------- /hdlr.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "io/ioutil" 7 | ) 8 | 9 | // Handler Reference Box (hdlr - mandatory) 10 | // 11 | // Contained in: Media Box (mdia) or Meta Box (meta) 12 | // 13 | // Status: decoded 14 | // 15 | // This box describes the type of data contained in the trak. 16 | // 17 | // HandlerType can be : "vide" (video track), "soun" (audio track), "hint" (hint track), "meta" (timed Metadata track), "auxv" (auxiliary video track). 18 | type HdlrBox struct { 19 | Version byte 20 | Flags [3]byte 21 | PreDefined uint32 22 | HandlerType string 23 | Name string 24 | } 25 | 26 | func DecodeHdlr(r io.Reader) (Box, error) { 27 | data, err := ioutil.ReadAll(r) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &HdlrBox{ 32 | Version: data[0], 33 | Flags: [3]byte{data[1], data[2], data[3]}, 34 | PreDefined: binary.BigEndian.Uint32(data[4:8]), 35 | HandlerType: string(data[8:12]), 36 | Name: string(data[24:]), 37 | }, nil 38 | } 39 | 40 | func (b *HdlrBox) Type() string { 41 | return "hdlr" 42 | } 43 | 44 | func (b *HdlrBox) Size() int { 45 | return BoxHeaderSize + 24 + len(b.Name) 46 | } 47 | 48 | func (b *HdlrBox) Encode(w io.Writer) error { 49 | err := EncodeHeader(b, w) 50 | if err != nil { 51 | return err 52 | } 53 | buf := makebuf(b) 54 | buf[0] = b.Version 55 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 56 | binary.BigEndian.PutUint32(buf[4:], b.PreDefined) 57 | strtobuf(buf[8:], b.HandlerType, 4) 58 | strtobuf(buf[24:], b.Name, len(b.Name)) 59 | _, err = w.Write(buf) 60 | return err 61 | } 62 | -------------------------------------------------------------------------------- /iods.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | ) 7 | 8 | // Object Descriptor Container Box (iods - optional) 9 | // 10 | // Contained in : Movie Box (‘moov’) 11 | // 12 | // Status: not decoded 13 | type IodsBox struct { 14 | notDecoded []byte 15 | } 16 | 17 | func DecodeIods(r io.Reader) (Box, error) { 18 | data, err := ioutil.ReadAll(r) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return &IodsBox{ 23 | notDecoded: data, 24 | }, nil 25 | } 26 | 27 | func (b *IodsBox) Type() string { 28 | return "iods" 29 | } 30 | 31 | func (b *IodsBox) Size() int { 32 | return BoxHeaderSize + len(b.notDecoded) 33 | } 34 | 35 | func (b *IodsBox) Encode(w io.Writer) error { 36 | err := EncodeHeader(b, w) 37 | if err != nil { 38 | return err 39 | } 40 | _, err = w.Write(b.notDecoded) 41 | return err 42 | } 43 | -------------------------------------------------------------------------------- /mdat.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import "io" 4 | 5 | // Media Data Box (mdat - optional) 6 | // 7 | // Status: not decoded 8 | // 9 | // The mdat box contains media chunks/samples. 10 | // 11 | // It is not read, only the io.Reader is stored, and will be used to Encode (io.Copy) the box to a io.Writer. 12 | type MdatBox struct { 13 | ContentSize uint32 14 | r io.Reader 15 | } 16 | 17 | func DecodeMdat(r io.Reader) (Box, error) { 18 | // r is a LimitedReader 19 | if lr, limited := r.(*io.LimitedReader); limited { 20 | r = lr.R 21 | } 22 | return &MdatBox{r: r}, nil 23 | } 24 | 25 | func (b *MdatBox) Type() string { 26 | return "mdat" 27 | } 28 | 29 | func (b *MdatBox) Size() int { 30 | return BoxHeaderSize + int(b.ContentSize) 31 | } 32 | 33 | func (b *MdatBox) Reader() io.Reader { 34 | return b.r 35 | } 36 | 37 | func (b *MdatBox) Encode(w io.Writer) error { 38 | err := EncodeHeader(b, w) 39 | if err != nil { 40 | return err 41 | } 42 | _, err = io.Copy(w, b.r) 43 | return err 44 | } 45 | -------------------------------------------------------------------------------- /mdhd.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "time" 9 | ) 10 | 11 | // Media Header Box (mdhd - mandatory) 12 | // 13 | // Contained in : Media Box (mdia) 14 | // 15 | // Status : only version 0 is decoded. version 1 is not supported 16 | // 17 | // Timescale defines the timescale used for tracks. 18 | // Language is a ISO-639-2/T language code stored as 1bit padding + [3]int5 19 | type MdhdBox struct { 20 | Version byte 21 | Flags [3]byte 22 | CreationTime uint32 23 | ModificationTime uint32 24 | Timescale uint32 25 | Duration uint32 26 | Language uint16 27 | } 28 | 29 | func DecodeMdhd(r io.Reader) (Box, error) { 30 | data, err := ioutil.ReadAll(r) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return &MdhdBox{ 35 | Version: data[0], 36 | Flags: [3]byte{data[1], data[2], data[3]}, 37 | CreationTime: binary.BigEndian.Uint32(data[4:8]), 38 | ModificationTime: binary.BigEndian.Uint32(data[8:12]), 39 | Timescale: binary.BigEndian.Uint32(data[12:16]), 40 | Duration: binary.BigEndian.Uint32(data[16:20]), 41 | Language: binary.BigEndian.Uint16(data[20:22]), 42 | }, nil 43 | } 44 | 45 | func (b *MdhdBox) Type() string { 46 | return "mdhd" 47 | } 48 | 49 | func (b *MdhdBox) Size() int { 50 | return BoxHeaderSize + 24 51 | } 52 | 53 | func (b *MdhdBox) Dump() { 54 | fmt.Printf("Media Header:\n Timescale: %d units/sec\n Duration: %d units (%s)\n", b.Timescale, b.Duration, time.Duration(b.Duration/b.Timescale)*time.Second) 55 | 56 | } 57 | 58 | func (b *MdhdBox) Encode(w io.Writer) error { 59 | err := EncodeHeader(b, w) 60 | if err != nil { 61 | return err 62 | } 63 | buf := makebuf(b) 64 | buf[0] = b.Version 65 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 66 | binary.BigEndian.PutUint32(buf[4:], b.CreationTime) 67 | binary.BigEndian.PutUint32(buf[8:], b.ModificationTime) 68 | binary.BigEndian.PutUint32(buf[12:], b.Timescale) 69 | binary.BigEndian.PutUint32(buf[16:], b.Duration) 70 | binary.BigEndian.PutUint16(buf[20:], b.Language) 71 | _, err = w.Write(buf) 72 | return err 73 | } 74 | -------------------------------------------------------------------------------- /mdia.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import "io" 4 | 5 | // Media Box (mdia - mandatory) 6 | // 7 | // Contained in : Track Box (trak) 8 | // 9 | // Status: decoded 10 | // 11 | // Contains all information about the media data. 12 | type MdiaBox struct { 13 | Mdhd *MdhdBox 14 | Hdlr *HdlrBox 15 | Minf *MinfBox 16 | } 17 | 18 | func DecodeMdia(r io.Reader) (Box, error) { 19 | l, err := DecodeContainer(r) 20 | if err != nil { 21 | return nil, err 22 | } 23 | m := &MdiaBox{} 24 | for _, b := range l { 25 | switch b.Type() { 26 | case "mdhd": 27 | m.Mdhd = b.(*MdhdBox) 28 | case "hdlr": 29 | m.Hdlr = b.(*HdlrBox) 30 | case "minf": 31 | m.Minf = b.(*MinfBox) 32 | default: 33 | return nil, ErrBadFormat 34 | } 35 | } 36 | return m, nil 37 | } 38 | 39 | func (b *MdiaBox) Type() string { 40 | return "mdia" 41 | } 42 | 43 | func (b *MdiaBox) Size() int { 44 | sz := b.Mdhd.Size() 45 | if b.Hdlr != nil { 46 | sz += b.Hdlr.Size() 47 | } 48 | if b.Minf != nil { 49 | sz += b.Minf.Size() 50 | } 51 | return sz + BoxHeaderSize 52 | } 53 | 54 | func (b *MdiaBox) Dump() { 55 | b.Mdhd.Dump() 56 | if b.Minf != nil { 57 | b.Minf.Dump() 58 | } 59 | } 60 | 61 | func (b *MdiaBox) Encode(w io.Writer) error { 62 | err := EncodeHeader(b, w) 63 | if err != nil { 64 | return err 65 | } 66 | err = b.Mdhd.Encode(w) 67 | if err != nil { 68 | return err 69 | } 70 | if b.Hdlr != nil { 71 | err = b.Hdlr.Encode(w) 72 | if err != nil { 73 | return err 74 | } 75 | } 76 | return b.Minf.Encode(w) 77 | } 78 | -------------------------------------------------------------------------------- /meta.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | ) 7 | 8 | // Meta Box (meta - optional) 9 | // 10 | // Status: not decoded 11 | type MetaBox struct { 12 | Version byte 13 | Flags [3]byte 14 | notDecoded []byte 15 | } 16 | 17 | func DecodeMeta(r io.Reader) (Box, error) { 18 | data, err := ioutil.ReadAll(r) 19 | if err != nil { 20 | return nil, err 21 | } 22 | return &MetaBox{ 23 | Version: data[0], 24 | Flags: [3]byte{data[1], data[2], data[3]}, 25 | notDecoded: data[4:], 26 | }, nil 27 | } 28 | 29 | func (b *MetaBox) Type() string { 30 | return "meta" 31 | } 32 | 33 | func (b *MetaBox) Size() int { 34 | return BoxHeaderSize + 4 + len(b.notDecoded) 35 | } 36 | 37 | func (b *MetaBox) Encode(w io.Writer) error { 38 | err := EncodeHeader(b, w) 39 | if err != nil { 40 | return err 41 | } 42 | buf := makebuf(b) 43 | buf[0] = b.Version 44 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 45 | copy(buf[4:], b.notDecoded) 46 | _, err = w.Write(buf) 47 | return err 48 | } 49 | -------------------------------------------------------------------------------- /minf.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import "io" 4 | 5 | // Media Information Box (minf - mandatory) 6 | // 7 | // Contained in : Media Box (mdia) 8 | // 9 | // Status: partially decoded (hmhd - hint tracks - and nmhd - null media - are ignored) 10 | type MinfBox struct { 11 | Vmhd *VmhdBox 12 | Smhd *SmhdBox 13 | Stbl *StblBox 14 | Dinf *DinfBox 15 | Hdlr *HdlrBox 16 | } 17 | 18 | func DecodeMinf(r io.Reader) (Box, error) { 19 | l, err := DecodeContainer(r) 20 | if err != nil { 21 | return nil, err 22 | } 23 | m := &MinfBox{} 24 | for _, b := range l { 25 | switch b.Type() { 26 | case "vmhd": 27 | m.Vmhd = b.(*VmhdBox) 28 | case "smhd": 29 | m.Smhd = b.(*SmhdBox) 30 | case "stbl": 31 | m.Stbl = b.(*StblBox) 32 | case "dinf": 33 | m.Dinf = b.(*DinfBox) 34 | case "hdlr": 35 | m.Hdlr = b.(*HdlrBox) 36 | } 37 | } 38 | return m, nil 39 | } 40 | 41 | func (b *MinfBox) Type() string { 42 | return "minf" 43 | } 44 | 45 | func (b *MinfBox) Size() int { 46 | sz := 0 47 | if b.Vmhd != nil { 48 | sz += b.Vmhd.Size() 49 | } 50 | if b.Smhd != nil { 51 | sz += b.Smhd.Size() 52 | } 53 | sz += b.Stbl.Size() 54 | if b.Dinf != nil { 55 | sz += b.Dinf.Size() 56 | } 57 | if b.Hdlr != nil { 58 | sz += b.Hdlr.Size() 59 | } 60 | return sz + BoxHeaderSize 61 | } 62 | 63 | func (b *MinfBox) Dump() { 64 | b.Stbl.Dump() 65 | } 66 | 67 | func (b *MinfBox) Encode(w io.Writer) error { 68 | err := EncodeHeader(b, w) 69 | if err != nil { 70 | return err 71 | } 72 | if b.Vmhd != nil { 73 | err = b.Vmhd.Encode(w) 74 | if err != nil { 75 | return err 76 | } 77 | } 78 | if b.Smhd != nil { 79 | err = b.Smhd.Encode(w) 80 | if err != nil { 81 | return err 82 | } 83 | } 84 | err = b.Dinf.Encode(w) 85 | if err != nil { 86 | return err 87 | } 88 | err = b.Stbl.Encode(w) 89 | if err != nil { 90 | return err 91 | } 92 | if b.Hdlr != nil { 93 | return b.Hdlr.Encode(w) 94 | } 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /moov.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Movie Box (moov - mandatory) 9 | // 10 | // Status: partially decoded (anything other than mvhd, iods, trak or udta is ignored) 11 | // 12 | // Contains all meta-data. To be able to stream a file, the moov box should be placed before the mdat box. 13 | type MoovBox struct { 14 | Mvhd *MvhdBox 15 | Iods *IodsBox 16 | Trak []*TrakBox 17 | Udta *UdtaBox 18 | } 19 | 20 | func DecodeMoov(r io.Reader) (Box, error) { 21 | l, err := DecodeContainer(r) 22 | if err != nil { 23 | return nil, err 24 | } 25 | m := &MoovBox{} 26 | for _, b := range l { 27 | switch b.Type() { 28 | case "mvhd": 29 | m.Mvhd = b.(*MvhdBox) 30 | case "iods": 31 | m.Iods = b.(*IodsBox) 32 | case "trak": 33 | m.Trak = append(m.Trak, b.(*TrakBox)) 34 | case "udta": 35 | m.Udta = b.(*UdtaBox) 36 | } 37 | } 38 | return m, err 39 | } 40 | 41 | func (b *MoovBox) Type() string { 42 | return "moov" 43 | } 44 | 45 | func (b *MoovBox) Size() int { 46 | sz := b.Mvhd.Size() 47 | if b.Iods != nil { 48 | sz += b.Iods.Size() 49 | } 50 | for _, t := range b.Trak { 51 | sz += t.Size() 52 | } 53 | if b.Udta != nil { 54 | sz += b.Udta.Size() 55 | } 56 | return sz + BoxHeaderSize 57 | } 58 | 59 | func (b *MoovBox) Dump() { 60 | b.Mvhd.Dump() 61 | for i, t := range b.Trak { 62 | fmt.Println("Track", i) 63 | t.Dump() 64 | } 65 | } 66 | 67 | func (b *MoovBox) Encode(w io.Writer) error { 68 | err := EncodeHeader(b, w) 69 | if err != nil { 70 | return err 71 | } 72 | err = b.Mvhd.Encode(w) 73 | if err != nil { 74 | return err 75 | } 76 | if b.Iods != nil { 77 | err = b.Iods.Encode(w) 78 | if err != nil { 79 | return err 80 | } 81 | } 82 | for _, t := range b.Trak { 83 | err = t.Encode(w) 84 | if err != nil { 85 | return err 86 | } 87 | } 88 | if b.Udta != nil { 89 | return b.Udta.Encode(w) 90 | } 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /mp4.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import "io" 4 | 5 | // A MPEG-4 media 6 | // 7 | // A MPEG-4 media contains three main boxes : 8 | // 9 | // ftyp : the file type box 10 | // moov : the movie box (meta-data) 11 | // mdat : the media data (chunks and samples) 12 | // 13 | // Other boxes can also be present (pdin, moof, mfra, free, ...), but are not decoded. 14 | type MP4 struct { 15 | Ftyp *FtypBox 16 | Moov *MoovBox 17 | Mdat *MdatBox 18 | boxes []Box 19 | } 20 | 21 | // Decode decodes a media from a Reader 22 | func Decode(r io.Reader) (*MP4, error) { 23 | v := &MP4{ 24 | boxes: []Box{}, 25 | } 26 | LoopBoxes: 27 | for { 28 | h, err := DecodeHeader(r) 29 | if err != nil { 30 | return nil, err 31 | } 32 | box, err := DecodeBox(h, r) 33 | if err != nil { 34 | return nil, err 35 | } 36 | v.boxes = append(v.boxes, box) 37 | switch h.Type { 38 | case "ftyp": 39 | v.Ftyp = box.(*FtypBox) 40 | case "moov": 41 | v.Moov = box.(*MoovBox) 42 | case "mdat": 43 | v.Mdat = box.(*MdatBox) 44 | v.Mdat.ContentSize = h.Size - BoxHeaderSize 45 | break LoopBoxes 46 | } 47 | } 48 | return v, nil 49 | } 50 | 51 | // Dump displays some information about a media 52 | func (m *MP4) Dump() { 53 | m.Ftyp.Dump() 54 | m.Moov.Dump() 55 | } 56 | 57 | // Boxes lists the top-level boxes from a media 58 | func (m *MP4) Boxes() []Box { 59 | return m.boxes 60 | } 61 | 62 | // Encode encodes a media to a Writer 63 | func (m *MP4) Encode(w io.Writer) error { 64 | err := m.Ftyp.Encode(w) 65 | if err != nil { 66 | return err 67 | } 68 | err = m.Moov.Encode(w) 69 | if err != nil { 70 | return err 71 | } 72 | for _, b := range m.boxes { 73 | if b.Type() != "ftyp" && b.Type() != "moov" { 74 | err = b.Encode(w) 75 | if err != nil { 76 | return err 77 | } 78 | } 79 | } 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /mvhd.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "time" 9 | ) 10 | 11 | // Movie Header Box (mvhd - mandatory) 12 | // 13 | // Contained in : Movie Box (‘moov’) 14 | // 15 | // Status: version 0 is partially decoded. version 1 is not supported 16 | // 17 | // Contains all media information (duration, ...). 18 | // 19 | // Duration is measured in "time units", and timescale defines the number of time units per second. 20 | // 21 | // Only version 0 is decoded. 22 | type MvhdBox struct { 23 | Version byte 24 | Flags [3]byte 25 | CreationTime uint32 26 | ModificationTime uint32 27 | Timescale uint32 28 | Duration uint32 29 | NextTrackId uint32 30 | Rate Fixed32 31 | Volume Fixed16 32 | notDecoded []byte 33 | } 34 | 35 | func DecodeMvhd(r io.Reader) (Box, error) { 36 | data, err := ioutil.ReadAll(r) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return &MvhdBox{ 41 | Version: data[0], 42 | Flags: [3]byte{data[1], data[2], data[3]}, 43 | CreationTime: binary.BigEndian.Uint32(data[4:8]), 44 | ModificationTime: binary.BigEndian.Uint32(data[8:12]), 45 | Timescale: binary.BigEndian.Uint32(data[12:16]), 46 | Duration: binary.BigEndian.Uint32(data[16:20]), 47 | Rate: fixed32(data[20:24]), 48 | Volume: fixed16(data[24:26]), 49 | notDecoded: data[26:], 50 | }, nil 51 | } 52 | 53 | func (b *MvhdBox) Type() string { 54 | return "mvhd" 55 | } 56 | 57 | func (b *MvhdBox) Size() int { 58 | return BoxHeaderSize + 26 + len(b.notDecoded) 59 | } 60 | 61 | func (b *MvhdBox) Dump() { 62 | fmt.Printf("Movie Header:\n Timescale: %d units/sec\n Duration: %d units (%s)\n Rate: %s\n Volume: %s\n", b.Timescale, b.Duration, time.Duration(b.Duration/b.Timescale)*time.Second, b.Rate, b.Volume) 63 | } 64 | 65 | func (b *MvhdBox) Encode(w io.Writer) error { 66 | err := EncodeHeader(b, w) 67 | if err != nil { 68 | return err 69 | } 70 | buf := makebuf(b) 71 | buf[0] = b.Version 72 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 73 | binary.BigEndian.PutUint32(buf[4:], b.CreationTime) 74 | binary.BigEndian.PutUint32(buf[8:], b.ModificationTime) 75 | binary.BigEndian.PutUint32(buf[12:], b.Timescale) 76 | binary.BigEndian.PutUint32(buf[16:], b.Duration) 77 | binary.BigEndian.PutUint32(buf[20:], uint32(b.Rate)) 78 | binary.BigEndian.PutUint16(buf[24:], uint16(b.Volume)) 79 | copy(buf[26:], b.notDecoded) 80 | _, err = w.Write(buf) 81 | return err 82 | } 83 | -------------------------------------------------------------------------------- /smhd.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "io/ioutil" 7 | ) 8 | 9 | // Sound Media Header Box (smhd - mandatory for sound tracks) 10 | // 11 | // Contained in : Media Information Box (minf) 12 | // 13 | // Status: decoded 14 | type SmhdBox struct { 15 | Version byte 16 | Flags [3]byte 17 | Balance uint16 // should be int16 18 | } 19 | 20 | func DecodeSmhd(r io.Reader) (Box, error) { 21 | data, err := ioutil.ReadAll(r) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return &SmhdBox{ 26 | Version: data[0], 27 | Flags: [3]byte{data[1], data[2], data[3]}, 28 | Balance: binary.BigEndian.Uint16(data[4:6]), 29 | }, nil 30 | } 31 | 32 | func (b *SmhdBox) Type() string { 33 | return "smhd" 34 | } 35 | 36 | func (b *SmhdBox) Size() int { 37 | return BoxHeaderSize + 8 38 | } 39 | 40 | func (b *SmhdBox) Encode(w io.Writer) error { 41 | err := EncodeHeader(b, w) 42 | if err != nil { 43 | return err 44 | } 45 | buf := makebuf(b) 46 | buf[0] = b.Version 47 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 48 | binary.BigEndian.PutUint16(buf[4:], b.Balance) 49 | _, err = w.Write(buf) 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /stbl.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import "io" 4 | 5 | // Sample Table Box (stbl - mandatory) 6 | // 7 | // Contained in : Media Information Box (minf) 8 | // 9 | // Status: partially decoded (anything other than stsd, stts, stsc, stss, stsz, stco, ctts is ignored) 10 | // 11 | // The table contains all information relevant to data samples (times, chunks, sizes, ...) 12 | type StblBox struct { 13 | Stsd *StsdBox 14 | Stts *SttsBox 15 | Stss *StssBox 16 | Stsc *StscBox 17 | Stsz *StszBox 18 | Stco *StcoBox 19 | Ctts *CttsBox 20 | } 21 | 22 | func DecodeStbl(r io.Reader) (Box, error) { 23 | l, err := DecodeContainer(r) 24 | if err != nil { 25 | return nil, err 26 | } 27 | s := &StblBox{} 28 | for _, b := range l { 29 | switch b.Type() { 30 | case "stsd": 31 | s.Stsd = b.(*StsdBox) 32 | case "stts": 33 | s.Stts = b.(*SttsBox) 34 | case "stsc": 35 | s.Stsc = b.(*StscBox) 36 | case "stss": 37 | s.Stss = b.(*StssBox) 38 | case "stsz": 39 | s.Stsz = b.(*StszBox) 40 | case "stco": 41 | s.Stco = b.(*StcoBox) 42 | case "ctts": 43 | s.Ctts = b.(*CttsBox) 44 | } 45 | } 46 | return s, nil 47 | } 48 | 49 | func (b *StblBox) Type() string { 50 | return "stbl" 51 | } 52 | 53 | func (b *StblBox) Size() int { 54 | sz := b.Stsd.Size() 55 | if b.Stts != nil { 56 | sz += b.Stts.Size() 57 | } 58 | if b.Stss != nil { 59 | sz += b.Stss.Size() 60 | } 61 | if b.Stsc != nil { 62 | sz += b.Stsc.Size() 63 | } 64 | if b.Stsz != nil { 65 | sz += b.Stsz.Size() 66 | } 67 | if b.Stco != nil { 68 | sz += b.Stco.Size() 69 | } 70 | if b.Ctts != nil { 71 | sz += b.Ctts.Size() 72 | } 73 | return sz + BoxHeaderSize 74 | } 75 | 76 | func (b *StblBox) Dump() { 77 | if b.Stsc != nil { 78 | b.Stsc.Dump() 79 | } 80 | if b.Stts != nil { 81 | b.Stts.Dump() 82 | } 83 | if b.Stsz != nil { 84 | b.Stsz.Dump() 85 | } 86 | if b.Stss != nil { 87 | b.Stss.Dump() 88 | } 89 | if b.Stco != nil { 90 | b.Stco.Dump() 91 | } 92 | } 93 | 94 | func (b *StblBox) Encode(w io.Writer) error { 95 | err := EncodeHeader(b, w) 96 | if err != nil { 97 | return err 98 | } 99 | err = b.Stsd.Encode(w) 100 | if err != nil { 101 | return err 102 | } 103 | err = b.Stts.Encode(w) 104 | if err != nil { 105 | return err 106 | } 107 | if b.Stss != nil { 108 | err = b.Stss.Encode(w) 109 | if err != nil { 110 | return err 111 | } 112 | } 113 | err = b.Stsc.Encode(w) 114 | if err != nil { 115 | return err 116 | } 117 | err = b.Stsz.Encode(w) 118 | if err != nil { 119 | return err 120 | } 121 | err = b.Stco.Encode(w) 122 | if err != nil { 123 | return err 124 | } 125 | if b.Ctts != nil { 126 | return b.Ctts.Encode(w) 127 | } 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /stco.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | ) 9 | 10 | // Chunk Offset Box (stco - mandatory) 11 | // 12 | // Contained in : Sample Table box (stbl) 13 | // 14 | // Status: decoded 15 | // 16 | // This is the 32bits version of the box, the 64bits version (co64) is not decoded. 17 | // 18 | // The table contains the offsets (starting at the beginning of the file) for each chunk of data for the current track. 19 | // A chunk contains samples, the table defining the allocation of samples to each chunk is stsc. 20 | type StcoBox struct { 21 | Version byte 22 | Flags [3]byte 23 | ChunkOffset []uint32 24 | } 25 | 26 | func DecodeStco(r io.Reader) (Box, error) { 27 | data, err := ioutil.ReadAll(r) 28 | if err != nil { 29 | return nil, err 30 | } 31 | b := &StcoBox{ 32 | Version: data[0], 33 | Flags: [3]byte{data[1], data[2], data[3]}, 34 | ChunkOffset: []uint32{}, 35 | } 36 | ec := binary.BigEndian.Uint32(data[4:8]) 37 | for i := 0; i < int(ec); i++ { 38 | chunk := binary.BigEndian.Uint32(data[(8 + 4*i):(12 + 4*i)]) 39 | b.ChunkOffset = append(b.ChunkOffset, chunk) 40 | } 41 | return b, nil 42 | } 43 | 44 | func (b *StcoBox) Type() string { 45 | return "stco" 46 | } 47 | 48 | func (b *StcoBox) Size() int { 49 | return BoxHeaderSize + 8 + len(b.ChunkOffset)*4 50 | } 51 | 52 | func (b *StcoBox) Dump() { 53 | fmt.Println("Chunk byte offsets:") 54 | for i, o := range b.ChunkOffset { 55 | fmt.Printf(" #%d : starts at %d\n", i, o) 56 | } 57 | } 58 | 59 | func (b *StcoBox) Encode(w io.Writer) error { 60 | err := EncodeHeader(b, w) 61 | if err != nil { 62 | return err 63 | } 64 | buf := makebuf(b) 65 | buf[0] = b.Version 66 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 67 | binary.BigEndian.PutUint32(buf[4:], uint32(len(b.ChunkOffset))) 68 | for i := range b.ChunkOffset { 69 | binary.BigEndian.PutUint32(buf[8+4*i:], b.ChunkOffset[i]) 70 | } 71 | _, err = w.Write(buf) 72 | return err 73 | } 74 | -------------------------------------------------------------------------------- /stsc.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | ) 9 | 10 | // Sample To Chunk Box (stsc - mandatory) 11 | // 12 | // Contained in : Sample Table box (stbl) 13 | // 14 | // Status: decoded 15 | // 16 | // A chunk contains samples. This table defines to which chunk a sample is associated. 17 | // Each entry is defined by : 18 | // 19 | // * first chunk : all chunks starting at this index up to the next first chunk have the same sample count/description 20 | // * samples per chunk : number of samples in the chunk 21 | // * description id : description (see the sample description box - stsd) 22 | type StscBox struct { 23 | Version byte 24 | Flags [3]byte 25 | FirstChunk []uint32 26 | SamplesPerChunk []uint32 27 | SampleDescriptionID []uint32 28 | } 29 | 30 | func DecodeStsc(r io.Reader) (Box, error) { 31 | data, err := ioutil.ReadAll(r) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | b := &StscBox{ 37 | Version: data[0], 38 | Flags: [3]byte{data[1], data[2], data[3]}, 39 | FirstChunk: []uint32{}, 40 | SamplesPerChunk: []uint32{}, 41 | SampleDescriptionID: []uint32{}, 42 | } 43 | ec := binary.BigEndian.Uint32(data[4:8]) 44 | for i := 0; i < int(ec); i++ { 45 | fc := binary.BigEndian.Uint32(data[(8 + 12*i):(12 + 12*i)]) 46 | spc := binary.BigEndian.Uint32(data[(12 + 12*i):(16 + 12*i)]) 47 | sdi := binary.BigEndian.Uint32(data[(16 + 12*i):(20 + 12*i)]) 48 | b.FirstChunk = append(b.FirstChunk, fc) 49 | b.SamplesPerChunk = append(b.SamplesPerChunk, spc) 50 | b.SampleDescriptionID = append(b.SampleDescriptionID, sdi) 51 | } 52 | return b, nil 53 | } 54 | 55 | func (b *StscBox) Type() string { 56 | return "stsc" 57 | } 58 | 59 | func (b *StscBox) Size() int { 60 | return BoxHeaderSize + 8 + len(b.FirstChunk)*12 61 | } 62 | 63 | func (b *StscBox) Dump() { 64 | fmt.Println("Sample to Chunk:") 65 | for i := range b.SamplesPerChunk { 66 | fmt.Printf(" #%d : %d samples per chunk starting @chunk #%d \n", i, b.SamplesPerChunk[i], b.FirstChunk[i]) 67 | } 68 | } 69 | 70 | func (b *StscBox) Encode(w io.Writer) error { 71 | err := EncodeHeader(b, w) 72 | if err != nil { 73 | return err 74 | } 75 | buf := makebuf(b) 76 | buf[0] = b.Version 77 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 78 | binary.BigEndian.PutUint32(buf[4:], uint32(len(b.FirstChunk))) 79 | for i := range b.FirstChunk { 80 | binary.BigEndian.PutUint32(buf[8+12*i:], b.FirstChunk[i]) 81 | binary.BigEndian.PutUint32(buf[12+12*i:], b.SamplesPerChunk[i]) 82 | binary.BigEndian.PutUint32(buf[16+12*i:], b.SampleDescriptionID[i]) 83 | } 84 | _, err = w.Write(buf) 85 | return err 86 | } 87 | -------------------------------------------------------------------------------- /stsd.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | ) 7 | 8 | // Sample Description Box (stsd - manatory) 9 | // 10 | // Contained in : Sample Table box (stbl) 11 | // 12 | // Status: not decoded 13 | // 14 | // This box contains information that describes how the data can be decoded. 15 | type StsdBox struct { 16 | Version byte 17 | Flags [3]byte 18 | notDecoded []byte 19 | } 20 | 21 | func DecodeStsd(r io.Reader) (Box, error) { 22 | data, err := ioutil.ReadAll(r) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return &StsdBox{ 27 | Version: data[0], 28 | Flags: [3]byte{data[1], data[2], data[3]}, 29 | notDecoded: data[4:], 30 | }, nil 31 | } 32 | 33 | func (b *StsdBox) Type() string { 34 | return "stsd" 35 | } 36 | 37 | func (b *StsdBox) Size() int { 38 | return BoxHeaderSize + 4 + len(b.notDecoded) 39 | } 40 | 41 | func (b *StsdBox) Encode(w io.Writer) error { 42 | err := EncodeHeader(b, w) 43 | if err != nil { 44 | return err 45 | } 46 | buf := makebuf(b) 47 | buf[0] = b.Version 48 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 49 | copy(buf[4:], b.notDecoded) 50 | _, err = w.Write(buf) 51 | return err 52 | } 53 | -------------------------------------------------------------------------------- /stss.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | ) 9 | 10 | // Sync Sample Box (stss - optional) 11 | // 12 | // Contained in : Sample Table box (stbl) 13 | // 14 | // Status: decoded 15 | // 16 | // This lists all sync samples (key frames for video tracks) in the data. If absent, all samples are sync samples. 17 | type StssBox struct { 18 | Version byte 19 | Flags [3]byte 20 | SampleNumber []uint32 21 | } 22 | 23 | func DecodeStss(r io.Reader) (Box, error) { 24 | data, err := ioutil.ReadAll(r) 25 | if err != nil { 26 | return nil, err 27 | } 28 | b := &StssBox{ 29 | Version: data[0], 30 | Flags: [3]byte{data[1], data[2], data[3]}, 31 | SampleNumber: []uint32{}, 32 | } 33 | ec := binary.BigEndian.Uint32(data[4:8]) 34 | for i := 0; i < int(ec); i++ { 35 | sample := binary.BigEndian.Uint32(data[(8 + 4*i):(12 + 4*i)]) 36 | b.SampleNumber = append(b.SampleNumber, sample) 37 | } 38 | return b, nil 39 | } 40 | 41 | func (b *StssBox) Type() string { 42 | return "stss" 43 | } 44 | 45 | func (b *StssBox) Size() int { 46 | return BoxHeaderSize + 8 + len(b.SampleNumber)*4 47 | } 48 | 49 | func (b *StssBox) Dump() { 50 | fmt.Println("Key frames:") 51 | for i, n := range b.SampleNumber { 52 | fmt.Printf(" #%d : sample #%d\n", i, n) 53 | } 54 | } 55 | 56 | func (b *StssBox) Encode(w io.Writer) error { 57 | err := EncodeHeader(b, w) 58 | if err != nil { 59 | return err 60 | } 61 | buf := makebuf(b) 62 | buf[0] = b.Version 63 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 64 | binary.BigEndian.PutUint32(buf[4:], uint32(len(b.SampleNumber))) 65 | for i := range b.SampleNumber { 66 | binary.BigEndian.PutUint32(buf[8+4*i:], b.SampleNumber[i]) 67 | } 68 | _, err = w.Write(buf) 69 | return err 70 | } 71 | -------------------------------------------------------------------------------- /stsz.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | ) 9 | 10 | // Sample Size Box (stsz - mandatory) 11 | // 12 | // Contained in : Sample Table box (stbl) 13 | // 14 | // Status : decoded 15 | // 16 | // For each track, either stsz of the more compact stz2 must be present. stz2 variant is not supported. 17 | // 18 | // This table lists the size of each sample. If all samples have the same size, it can be defined in the 19 | // SampleUniformSize attribute. 20 | type StszBox struct { 21 | Version byte 22 | Flags [3]byte 23 | SampleUniformSize uint32 24 | SampleNumber uint32 25 | SampleSize []uint32 26 | } 27 | 28 | func DecodeStsz(r io.Reader) (Box, error) { 29 | data, err := ioutil.ReadAll(r) 30 | if err != nil { 31 | return nil, err 32 | } 33 | b := &StszBox{ 34 | Version: data[0], 35 | Flags: [3]byte{data[1], data[2], data[3]}, 36 | SampleUniformSize: binary.BigEndian.Uint32(data[4:8]), 37 | SampleNumber: binary.BigEndian.Uint32(data[8:12]), 38 | SampleSize: []uint32{}, 39 | } 40 | if len(data) > 12 { 41 | for i := 0; i < int(b.SampleNumber); i++ { 42 | sz := binary.BigEndian.Uint32(data[(12 + 4*i):(16 + 4*i)]) 43 | b.SampleSize = append(b.SampleSize, sz) 44 | } 45 | } 46 | return b, nil 47 | } 48 | 49 | func (b *StszBox) Type() string { 50 | return "stsz" 51 | } 52 | 53 | func (b *StszBox) Size() int { 54 | return BoxHeaderSize + 12 + len(b.SampleSize)*4 55 | } 56 | 57 | func (b *StszBox) Dump() { 58 | if len(b.SampleSize) == 0 { 59 | fmt.Printf("Samples : %d total samples\n", b.SampleNumber) 60 | } else { 61 | fmt.Printf("Samples : %d total samples\n", len(b.SampleSize)) 62 | } 63 | } 64 | 65 | // GetSampleSize returns the size (in bytes) of a sample 66 | func (b *StszBox) GetSampleSize(i int) uint32 { 67 | if i > len(b.SampleSize) { 68 | return b.SampleUniformSize 69 | } 70 | return b.SampleSize[i-1] 71 | } 72 | 73 | func (b *StszBox) Encode(w io.Writer) error { 74 | err := EncodeHeader(b, w) 75 | if err != nil { 76 | return err 77 | } 78 | buf := makebuf(b) 79 | buf[0] = b.Version 80 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 81 | binary.BigEndian.PutUint32(buf[4:], b.SampleUniformSize) 82 | if len(b.SampleSize) == 0 { 83 | binary.BigEndian.PutUint32(buf[8:], b.SampleNumber) 84 | } else { 85 | binary.BigEndian.PutUint32(buf[8:], uint32(len(b.SampleSize))) 86 | for i := range b.SampleSize { 87 | binary.BigEndian.PutUint32(buf[12+4*i:], b.SampleSize[i]) 88 | } 89 | } 90 | _, err = w.Write(buf) 91 | return err 92 | } 93 | -------------------------------------------------------------------------------- /stts.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "time" 9 | ) 10 | 11 | // Decoding Time to Sample Box (stts - mandatory) 12 | // 13 | // Contained in : Sample Table box (stbl) 14 | // 15 | // Status: decoded 16 | // 17 | // This table contains the duration in time units for each sample. 18 | // 19 | // * sample count : the number of consecutive samples having the same duration 20 | // * time delta : duration in time units 21 | type SttsBox struct { 22 | Version byte 23 | Flags [3]byte 24 | SampleCount []uint32 25 | SampleTimeDelta []uint32 26 | } 27 | 28 | func DecodeStts(r io.Reader) (Box, error) { 29 | data, err := ioutil.ReadAll(r) 30 | if err != nil { 31 | return nil, err 32 | } 33 | b := &SttsBox{ 34 | Version: data[0], 35 | Flags: [3]byte{data[1], data[2], data[3]}, 36 | SampleCount: []uint32{}, 37 | SampleTimeDelta: []uint32{}, 38 | } 39 | ec := binary.BigEndian.Uint32(data[4:8]) 40 | for i := 0; i < int(ec); i++ { 41 | s_count := binary.BigEndian.Uint32(data[(8 + 8*i):(12 + 8*i)]) 42 | s_delta := binary.BigEndian.Uint32(data[(12 + 8*i):(16 + 8*i)]) 43 | b.SampleCount = append(b.SampleCount, s_count) 44 | b.SampleTimeDelta = append(b.SampleTimeDelta, s_delta) 45 | } 46 | return b, nil 47 | } 48 | 49 | func (b *SttsBox) Type() string { 50 | return "stts" 51 | } 52 | 53 | func (b *SttsBox) Size() int { 54 | return BoxHeaderSize + 8 + len(b.SampleCount)*8 55 | } 56 | 57 | // GetTimeCode returns the timecode (duration since the beginning of the media) 58 | // of the beginning of a sample 59 | func (b *SttsBox) GetTimeCode(sample, timescale uint32) time.Duration { 60 | sample-- 61 | var units uint32 62 | i := 0 63 | for sample > 0 && i < len(b.SampleCount) { 64 | if sample >= b.SampleCount[i] { 65 | units += b.SampleCount[i] * b.SampleTimeDelta[i] 66 | sample -= b.SampleCount[i] 67 | } else { 68 | units += sample * b.SampleTimeDelta[i] 69 | sample = 0 70 | } 71 | i++ 72 | } 73 | return time.Second * time.Duration(units) / time.Duration(timescale) 74 | } 75 | 76 | func (b *SttsBox) Dump() { 77 | fmt.Println("Time to sample:") 78 | for i := range b.SampleCount { 79 | fmt.Printf(" #%d : %d samples with duration %d units\n", i, b.SampleCount[i], b.SampleTimeDelta[i]) 80 | } 81 | } 82 | 83 | func (b *SttsBox) Encode(w io.Writer) error { 84 | err := EncodeHeader(b, w) 85 | if err != nil { 86 | return err 87 | } 88 | buf := makebuf(b) 89 | buf[0] = b.Version 90 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 91 | binary.BigEndian.PutUint32(buf[4:], uint32(len(b.SampleCount))) 92 | for i := range b.SampleCount { 93 | binary.BigEndian.PutUint32(buf[8+8*i:], b.SampleCount[i]) 94 | binary.BigEndian.PutUint32(buf[12+8*i:], b.SampleTimeDelta[i]) 95 | } 96 | _, err = w.Write(buf) 97 | return err 98 | } 99 | -------------------------------------------------------------------------------- /tkhd.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | ) 9 | 10 | // Track Header Box (tkhd - mandatory) 11 | // 12 | // Status : only version 0 is decoded. version 1 is not supported 13 | // 14 | // This box describes the track. Duration is measured in time units (according to the time scale 15 | // defined in the movie header box). 16 | // 17 | // Volume (relevant for audio tracks) is a fixed point number (8 bits + 8 bits). Full volume is 1.0. 18 | // Width and Height (relevant for video tracks) are fixed point numbers (16 bits + 16 bits). 19 | // Video pixels are not necessarily square. 20 | type TkhdBox struct { 21 | Version byte 22 | Flags [3]byte 23 | CreationTime uint32 24 | ModificationTime uint32 25 | TrackId uint32 26 | Duration uint32 27 | Layer uint16 28 | AlternateGroup uint16 // should be int16 29 | Volume Fixed16 30 | Matrix []byte 31 | Width, Height Fixed32 32 | } 33 | 34 | func DecodeTkhd(r io.Reader) (Box, error) { 35 | data, err := ioutil.ReadAll(r) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return &TkhdBox{ 40 | Version: data[0], 41 | Flags: [3]byte{data[1], data[2], data[3]}, 42 | CreationTime: binary.BigEndian.Uint32(data[4:8]), 43 | ModificationTime: binary.BigEndian.Uint32(data[8:12]), 44 | TrackId: binary.BigEndian.Uint32(data[12:16]), 45 | Volume: fixed16(data[36:38]), 46 | Duration: binary.BigEndian.Uint32(data[20:24]), 47 | Layer: binary.BigEndian.Uint16(data[32:34]), 48 | AlternateGroup: binary.BigEndian.Uint16(data[34:36]), 49 | Matrix: data[40:76], 50 | Width: fixed32(data[76:80]), 51 | Height: fixed32(data[80:84]), 52 | }, nil 53 | } 54 | 55 | func (b *TkhdBox) Type() string { 56 | return "tkhd" 57 | } 58 | 59 | func (b *TkhdBox) Size() int { 60 | return BoxHeaderSize + 84 61 | } 62 | 63 | func (b *TkhdBox) Encode(w io.Writer) error { 64 | err := EncodeHeader(b, w) 65 | if err != nil { 66 | return err 67 | } 68 | buf := makebuf(b) 69 | buf[0] = b.Version 70 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 71 | binary.BigEndian.PutUint32(buf[4:], b.CreationTime) 72 | binary.BigEndian.PutUint32(buf[8:], b.ModificationTime) 73 | binary.BigEndian.PutUint32(buf[12:], b.TrackId) 74 | binary.BigEndian.PutUint32(buf[20:], b.Duration) 75 | binary.BigEndian.PutUint16(buf[32:], b.Layer) 76 | binary.BigEndian.PutUint16(buf[34:], b.AlternateGroup) 77 | putFixed16(buf[36:], b.Volume) 78 | copy(buf[40:], b.Matrix) 79 | putFixed32(buf[76:], b.Width) 80 | putFixed32(buf[80:], b.Height) 81 | _, err = w.Write(buf) 82 | return err 83 | } 84 | 85 | func (b *TkhdBox) Dump() { 86 | fmt.Println("Track Header:") 87 | fmt.Printf(" Duration: %d units\n WxH: %sx%s\n", b.Duration, b.Width, b.Height) 88 | } 89 | -------------------------------------------------------------------------------- /trak.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import "io" 4 | 5 | // Track Box (tkhd - mandatory) 6 | // 7 | // Contained in : Movie Box (moov) 8 | // 9 | // A media file can contain one or more tracks. 10 | type TrakBox struct { 11 | Tkhd *TkhdBox 12 | Mdia *MdiaBox 13 | Edts *EdtsBox 14 | } 15 | 16 | func DecodeTrak(r io.Reader) (Box, error) { 17 | l, err := DecodeContainer(r) 18 | if err != nil { 19 | return nil, err 20 | } 21 | t := &TrakBox{} 22 | for _, b := range l { 23 | switch b.Type() { 24 | case "tkhd": 25 | t.Tkhd = b.(*TkhdBox) 26 | case "mdia": 27 | t.Mdia = b.(*MdiaBox) 28 | case "edts": 29 | t.Edts = b.(*EdtsBox) 30 | default: 31 | return nil, ErrBadFormat 32 | } 33 | } 34 | return t, nil 35 | } 36 | 37 | func (b *TrakBox) Type() string { 38 | return "trak" 39 | } 40 | 41 | func (b *TrakBox) Size() int { 42 | sz := b.Tkhd.Size() 43 | sz += b.Mdia.Size() 44 | if b.Edts != nil { 45 | sz += b.Edts.Size() 46 | } 47 | return sz + BoxHeaderSize 48 | } 49 | 50 | func (b *TrakBox) Dump() { 51 | b.Tkhd.Dump() 52 | if b.Edts != nil { 53 | b.Edts.Dump() 54 | } 55 | b.Mdia.Dump() 56 | } 57 | 58 | func (b *TrakBox) Encode(w io.Writer) error { 59 | err := EncodeHeader(b, w) 60 | if err != nil { 61 | return err 62 | } 63 | err = b.Tkhd.Encode(w) 64 | if err != nil { 65 | return err 66 | } 67 | if b.Edts != nil { 68 | err = b.Edts.Encode(w) 69 | if err != nil { 70 | return err 71 | } 72 | } 73 | return b.Mdia.Encode(w) 74 | } 75 | -------------------------------------------------------------------------------- /udta.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import "io" 4 | 5 | // User Data Box (udta - optional) 6 | // 7 | // Contained in: Movie Box (moov) or Track Box (trak) 8 | type UdtaBox struct { 9 | Meta *MetaBox 10 | } 11 | 12 | func DecodeUdta(r io.Reader) (Box, error) { 13 | l, err := DecodeContainer(r) 14 | if err != nil { 15 | return nil, err 16 | } 17 | u := &UdtaBox{} 18 | for _, b := range l { 19 | switch b.Type() { 20 | case "meta": 21 | u.Meta = b.(*MetaBox) 22 | default: 23 | return nil, ErrBadFormat 24 | } 25 | } 26 | return u, nil 27 | } 28 | 29 | func (b *UdtaBox) Type() string { 30 | return "udta" 31 | } 32 | 33 | func (b *UdtaBox) Size() int { 34 | return BoxHeaderSize + b.Meta.Size() 35 | } 36 | 37 | func (b *UdtaBox) Encode(w io.Writer) error { 38 | err := EncodeHeader(b, w) 39 | if err != nil { 40 | return err 41 | } 42 | return b.Meta.Encode(w) 43 | } 44 | -------------------------------------------------------------------------------- /vmhd.go: -------------------------------------------------------------------------------- 1 | package mp4 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "io/ioutil" 7 | ) 8 | 9 | // Video Media Header Box (vhmd - mandatory for video tracks) 10 | // 11 | // Contained in : Media Information Box (minf) 12 | // 13 | // Status: decoded 14 | type VmhdBox struct { 15 | Version byte 16 | Flags [3]byte 17 | GraphicsMode uint16 18 | OpColor [3]uint16 19 | } 20 | 21 | func DecodeVmhd(r io.Reader) (Box, error) { 22 | data, err := ioutil.ReadAll(r) 23 | if err != nil { 24 | return nil, err 25 | } 26 | b := &VmhdBox{ 27 | Version: data[0], 28 | Flags: [3]byte{data[1], data[2], data[3]}, 29 | GraphicsMode: binary.BigEndian.Uint16(data[4:6]), 30 | } 31 | for i := 0; i < 3; i++ { 32 | b.OpColor[i] = binary.BigEndian.Uint16(data[(6 + 2*i):(8 + 2*i)]) 33 | } 34 | return b, nil 35 | } 36 | 37 | func (b *VmhdBox) Type() string { 38 | return "vmhd" 39 | } 40 | 41 | func (b *VmhdBox) Size() int { 42 | return BoxHeaderSize + 12 43 | } 44 | 45 | func (b *VmhdBox) Encode(w io.Writer) error { 46 | err := EncodeHeader(b, w) 47 | if err != nil { 48 | return err 49 | } 50 | buf := makebuf(b) 51 | buf[0] = b.Version 52 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 53 | binary.BigEndian.PutUint16(buf[4:], b.GraphicsMode) 54 | for i := 0; i < 3; i++ { 55 | binary.BigEndian.PutUint16(buf[6+2*i:], b.OpColor[i]) 56 | } 57 | _, err = w.Write(buf) 58 | return err 59 | } 60 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: wercker/golang 2 | 3 | build: 4 | steps: 5 | - setup-go-workspace 6 | - script: 7 | name: Install goconvey 8 | code: go get github.com/smartystreets/goconvey/convey 9 | - script: 10 | name: Go get 11 | code: go get -v ./... 12 | - script: 13 | name: Go build 14 | code: go build ./... 15 | - script: 16 | name: Go test 17 | code: go test -p 1 -v ./... 18 | --------------------------------------------------------------------------------