├── README.md ├── example └── example.go ├── main.go └── stream ├── README.md ├── box.go ├── ctts.go ├── filter └── clip.go ├── mdat.go ├── mdhd.go ├── mdia.go ├── minf.go ├── moov.go ├── mvhd.go ├── stbl.go ├── stco.go ├── stream.go ├── stsc.go ├── stss.go ├── stsz.go ├── stts.go ├── tkhd.go ├── trak.go └── uni.go /README.md: -------------------------------------------------------------------------------- 1 | # A pure golang mp4 library 2 | 3 | Provides mp4 reader/writer and mp4 atom manipulations functions. 4 | 5 | Open a mp4 file and read the first sample: 6 | ```go 7 | file, _ := os.Open("test.mp4") 8 | demuxer := &mp4.Demuxer{R: file} 9 | demuxer.ReadHeader() 10 | pts, dts, isKeyFrame, data, err := demuxer.TrackH264.ReadSample() 11 | ``` 12 | 13 | do some seeking: 14 | 15 | ```go 16 | demuxer.TrackH264.SeekToTime(2.0) 17 | ``` 18 | 19 | demuxer demo code [here](https://github.com/nareix/mp4/blob/master/example/example.go#L11) 20 | 21 | the library also provide atom struct decoding/encoding functions( 22 | learn more about mp4 atoms [here](https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html) 23 | ) 24 | 25 | you can access atom structs via `Demuxer.TrackH264.TrackAtom`. for example: 26 | 27 | ```go 28 | // Get the raw TimeScale field inside `mvhd` atom 29 | fmt.Println(demuxer.TrackH264.TrackAtom.Media.Header.TimeScale) 30 | ``` 31 | 32 | for more see Atom API Docs 33 | 34 | # Documentation 35 | 36 | [API Docs](https://godoc.org/github.com/nareix/mp4) 37 | 38 | [Atom API Docs](https://godoc.org/github.com/nareix/mp4/atom) 39 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | 2 | package main 3 | 4 | import ( 5 | "github.com/nareix/mp4" 6 | "os" 7 | "fmt" 8 | "encoding/hex" 9 | ) 10 | 11 | func DemuxExample() { 12 | file, _ := os.Open("test.mp4") 13 | demuxer := &mp4.Demuxer{R: file} 14 | demuxer.ReadHeader() 15 | 16 | fmt.Println("Total tracks: ", len(demuxer.Tracks)) 17 | fmt.Println("Duration: ", demuxer.TrackH264.Duration()) 18 | 19 | count := demuxer.TrackH264.SampleCount() 20 | fmt.Println("SampleCount: ", count) 21 | 22 | demuxer.TrackH264.SeekToTime(2.3) 23 | 24 | var sample []byte 25 | for i := 0; i < 5; i++ { 26 | pts, dts, isKeyFrame, data, err := demuxer.TrackH264.ReadSample() 27 | fmt.Println("sample #", 28 | i, pts, dts, isKeyFrame, len(data), 29 | demuxer.TrackH264.CurTime(), 30 | err, 31 | ) 32 | if i == 3 { 33 | sample = data 34 | } 35 | } 36 | fmt.Println("Sample H264 frame:") 37 | fmt.Print(hex.Dump(sample)) 38 | 39 | fmt.Println("Duration(AAC): ", demuxer.TrackAAC.Duration()) 40 | fmt.Println("SampleCount(AAC): ", demuxer.TrackAAC.SampleCount()) 41 | demuxer.TrackAAC.SeekToTime(1.3) 42 | 43 | for i := 0; i < 5; i++ { 44 | pts, dts, isKeyFrame, data, err := demuxer.TrackAAC.ReadSample() 45 | fmt.Println("sample(AAC) #", 46 | i, pts, dts, isKeyFrame, len(data), 47 | demuxer.TrackAAC.CurTime(), 48 | err, 49 | ) 50 | if i == 1 { 51 | sample = data 52 | } 53 | } 54 | fmt.Println("Sample AAC frame:") 55 | fmt.Print(hex.Dump(sample)) 56 | } 57 | 58 | func MuxExample() { 59 | infile, _ := os.Open("test.mp4") 60 | outfile, _ := os.Create("test.out.mp4") 61 | 62 | demuxer := &mp4.Demuxer{R: infile} 63 | demuxer.ReadHeader() 64 | 65 | muxer := &mp4.Muxer{W: outfile} 66 | muxer.AddH264Track() 67 | muxer.TrackH264.SetH264PPS(demuxer.TrackH264.GetH264PPS()) 68 | muxer.TrackH264.SetH264SPS(demuxer.TrackH264.GetH264SPS()) 69 | muxer.TrackH264.SetTimeScale(demuxer.TrackH264.TimeScale()) 70 | muxer.WriteHeader() 71 | for { 72 | pts, dts, isKeyFrame, data, err := demuxer.TrackH264.ReadSample() 73 | if err != nil { 74 | break 75 | } 76 | fmt.Println("sample #", dts, pts) 77 | muxer.TrackH264.WriteSample(pts, dts, isKeyFrame, data) 78 | } 79 | 80 | muxer.WriteTrailer() 81 | outfile.Close() 82 | } 83 | 84 | func main() { 85 | //DemuxExample() 86 | MuxExample() 87 | } 88 | 89 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | import ( 3 | "os" 4 | "github.com/nareix/mp4" 5 | "fmt" 6 | "encoding/hex" 7 | ) 8 | 9 | 10 | 11 | func main() { 12 | myDemux() 13 | } 14 | 15 | func myDemux(){ 16 | file, _ := os.Open("videos/tth_dashinit.mp4") 17 | fileInfo, _ := file.Stat() 18 | demuxer := &mp4.Demuxer{R: file} 19 | demuxer.ReadHeader() 20 | 21 | fmt.Println("File Size: ", fileInfo.Size(), " bytes") 22 | fmt.Println("Total tracks: ", len(demuxer.Tracks)) 23 | fmt.Println("Duration: ", demuxer.TrackH264.Duration()) 24 | fmt.Println("Created Time: ", demuxer.MovieAtom.Header.CreateTime) 25 | fmt.Println("Time Scale: ", demuxer.MovieAtom.Header.TimeScale) 26 | fmt.Println("Track 0: ", demuxer.Tracks[0].TrackAtom.Header) 27 | } 28 | 29 | func DemuxExample() { 30 | file, _ := os.Open("videos/tth_dashinit.mp4") 31 | fileInfo, _ := file.Stat() 32 | demuxer := &mp4.Demuxer{R: file} 33 | demuxer.ReadHeader() 34 | 35 | fmt.Println("File Size: ", fileInfo.Size(), " bytes") 36 | fmt.Println("Total tracks: ", len(demuxer.Tracks)) 37 | fmt.Println("Duration: ", demuxer.TrackH264.Duration()) 38 | 39 | count := demuxer.TrackH264.SampleCount() 40 | fmt.Println("SampleCount: ", count) 41 | 42 | demuxer.TrackH264.SeekToTime(2.3) 43 | 44 | var sample []byte 45 | for i := 0; i < 5; i++ { 46 | pts, dts, isKeyFrame, data, err := demuxer.TrackH264.ReadSample() 47 | fmt.Println("sample #", 48 | i, pts, dts, isKeyFrame, len(data), 49 | demuxer.TrackH264.CurTime(), 50 | err, 51 | ) 52 | if i == 3 { 53 | sample = data 54 | } 55 | } 56 | fmt.Println("Sample H264 frame:") 57 | fmt.Print(hex.Dump(sample)) 58 | 59 | fmt.Println("Duration(AAC): ", demuxer.TrackAAC.Duration()) 60 | fmt.Println("SampleCount(AAC): ", demuxer.TrackAAC.SampleCount()) 61 | demuxer.TrackAAC.SeekToTime(1.3) 62 | 63 | for i := 0; i < 5; i++ { 64 | pts, dts, isKeyFrame, data, err := demuxer.TrackAAC.ReadSample() 65 | fmt.Println("sample(AAC) #", 66 | i, pts, dts, isKeyFrame, len(data), 67 | demuxer.TrackAAC.CurTime(), 68 | err, 69 | ) 70 | if i == 1 { 71 | sample = data 72 | } 73 | } 74 | fmt.Println("Sample AAC frame:") 75 | fmt.Print(hex.Dump(sample)) 76 | } 77 | 78 | func MuxExample() { 79 | // infile, _ := os.Open("test.mp4") 80 | // outfile, _ := os.Create("test.out.mp4") 81 | // 82 | // demuxer := &mp4.Demuxer{R: infile} 83 | // demuxer.ReadHeader() 84 | // 85 | // muxer := &mp4.Muxer{W: outfile} 86 | // muxer.AddH264Track() 87 | // muxer.TrackH264.SetH264PPS(demuxer.TrackH264.GetH264PPS()) 88 | // muxer.TrackH264.SetH264SPS(demuxer.TrackH264.GetH264SPS()) 89 | // muxer.TrackH264.SetTimeScale(demuxer.TrackH264.TimeScale()) 90 | // muxer.WriteHeader() 91 | // for { 92 | // pts, dts, isKeyFrame, data, err := demuxer.TrackH264.ReadSample() 93 | // if err != nil { 94 | // break 95 | // } 96 | // fmt.Println("sample #", dts, pts) 97 | // muxer.TrackH264.WriteSample(pts, dts, isKeyFrame, data) 98 | // } 99 | // 100 | // muxer.WriteTrailer() 101 | // outfile.Close() 102 | } -------------------------------------------------------------------------------- /stream/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /stream/box.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | ) 10 | 11 | const ( 12 | BoxHeaderSize = 8 13 | ) 14 | 15 | var ( 16 | ErrTruncatedHeader = errors.New("truncated header") 17 | ) 18 | 19 | var decoders map[string]BoxDecoder 20 | 21 | func init() { 22 | decoders = map[string]BoxDecoder{ 23 | "moov": DecodeMoov, 24 | "mvhd": DecodeMvhd, 25 | "trak": DecodeTrak, 26 | "tkhd": DecodeTkhd, 27 | "mdia": DecodeMdia, 28 | "minf": DecodeMinf, 29 | "mdhd": DecodeMdhd, 30 | "stbl": DecodeStbl, 31 | "stco": DecodeStco, 32 | "stsc": DecodeStsc, 33 | "stsz": DecodeStsz, 34 | "ctts": DecodeCtts, 35 | "stts": DecodeStts, 36 | "stss": DecodeStss, 37 | "mdat": DecodeMdat, 38 | } 39 | } 40 | 41 | // A box 42 | type Box interface { 43 | Size() int 44 | Type() string 45 | Encode(w io.Writer) error 46 | } 47 | 48 | type BoxDecoder func(r io.Reader) (Box, error) 49 | 50 | // DecodeContainer decodes a container box 51 | func DecodeContainer(r io.Reader) (l []Box, err error) { 52 | var b Box 53 | var ht string 54 | var hs uint32 55 | 56 | buf := make([]byte, BoxHeaderSize) 57 | 58 | for { 59 | n, err := r.Read(buf) 60 | 61 | if err != nil { 62 | if err == io.EOF { 63 | return l, nil 64 | } else { 65 | return nil, err 66 | } 67 | } 68 | 69 | if n != BoxHeaderSize { 70 | return nil, ErrTruncatedHeader 71 | } 72 | 73 | ht = string(buf[4:8]) 74 | hs = binary.BigEndian.Uint32(buf[0:4]) 75 | 76 | if d := decoders[ht]; d != nil { 77 | b, err = d(io.LimitReader(r, int64(hs-BoxHeaderSize))) 78 | } else { 79 | b, err = DecodeUni(io.LimitReader(r, int64(hs-BoxHeaderSize)), ht) 80 | } 81 | 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | l = append(l, b) 87 | 88 | if ht == "mdat" { 89 | b.(*MdatBox).ContentSize = hs - BoxHeaderSize 90 | return l, nil 91 | } 92 | } 93 | } 94 | 95 | // An 8.8 fixed point number 96 | type Fixed16 uint16 97 | 98 | func (f Fixed16) String() string { 99 | return fmt.Sprintf("%d.%d", uint16(f)>>8, uint16(f)&7) 100 | } 101 | 102 | func fixed16(bytes []byte) Fixed16 { 103 | return Fixed16(binary.BigEndian.Uint16(bytes)) 104 | } 105 | 106 | func putFixed16(bytes []byte, i Fixed16) { 107 | binary.BigEndian.PutUint16(bytes, uint16(i)) 108 | } 109 | 110 | // A 16.16 fixed point number 111 | type Fixed32 uint32 112 | 113 | func (f Fixed32) String() string { 114 | return fmt.Sprintf("%d.%d", uint32(f)>>16, uint32(f)&15) 115 | } 116 | 117 | func fixed32(bytes []byte) Fixed32 { 118 | return Fixed32(binary.BigEndian.Uint32(bytes)) 119 | } 120 | 121 | func putFixed32(bytes []byte, i Fixed32) { 122 | binary.BigEndian.PutUint32(bytes, uint32(i)) 123 | } 124 | 125 | // Utils 126 | func makebuf(b Box) []byte { 127 | return make([]byte, b.Size()-BoxHeaderSize) 128 | } 129 | 130 | func readAllO(r io.Reader) ([]byte, error) { 131 | if lr, ok := r.(*io.LimitedReader); ok { 132 | buf := make([]byte, lr.N) 133 | _, err := io.ReadFull(lr, buf) 134 | return buf, err 135 | } 136 | return ioutil.ReadAll(r) 137 | } 138 | -------------------------------------------------------------------------------- /stream/ctts.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | // Composition Time to Sample Box (ctts - optional) 9 | // 10 | // Contained in: Sample Table Box (stbl) 11 | // 12 | // Status: version 0 decoded. version 1 uses int32 for offsets 13 | type CttsBox struct { 14 | Version byte 15 | Flags [3]byte 16 | header [8]byte 17 | SampleCount []uint32 18 | SampleOffset []uint32 // int32 for version 1 19 | } 20 | 21 | func DecodeCtts(r io.Reader) (Box, error) { 22 | data, err := readAllO(r) 23 | 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | c := binary.BigEndian.Uint32(data[4:8]) 29 | b := &CttsBox{ 30 | Flags: [3]byte{data[1], data[2], data[3]}, 31 | Version: data[0], 32 | SampleCount: make([]uint32, c), 33 | SampleOffset: make([]uint32, c), 34 | } 35 | 36 | for i := 0; i < int(c); i++ { 37 | b.SampleCount[i] = binary.BigEndian.Uint32(data[(8 + 8*i):(12 + 8*i)]) 38 | b.SampleOffset[i] = binary.BigEndian.Uint32(data[(12 + 8*i):(16 + 8*i)]) 39 | } 40 | 41 | return b, nil 42 | } 43 | 44 | func (b *CttsBox) Type() string { 45 | return "ctts" 46 | } 47 | 48 | func (b *CttsBox) Size() int { 49 | return BoxHeaderSize + 8 + len(b.SampleCount)*8 50 | } 51 | 52 | func (b *CttsBox) Encode(w io.Writer) error { 53 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 54 | copy(b.header[4:], b.Type()) 55 | _, err := w.Write(b.header[:]) 56 | if err != nil { 57 | return err 58 | } 59 | buf := makebuf(b) 60 | buf[0] = b.Version 61 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 62 | binary.BigEndian.PutUint32(buf[4:], uint32(len(b.SampleCount))) 63 | for i := range b.SampleCount { 64 | binary.BigEndian.PutUint32(buf[8+8*i:], b.SampleCount[i]) 65 | binary.BigEndian.PutUint32(buf[12+8*i:], b.SampleOffset[i]) 66 | } 67 | _, err = w.Write(buf) 68 | return err 69 | } 70 | -------------------------------------------------------------------------------- /stream/filter/clip.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "io" 8 | "os" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/seifer/go-mp4/stream" 13 | ) 14 | 15 | var ( 16 | ErrClipOutside = errors.New("clip zone is outside video") 17 | ErrTruncatedChunk = errors.New("chunk was truncated") 18 | ErrInvalidDuration = errors.New("invalid duration") 19 | ) 20 | 21 | type chunk struct { 22 | size int64 23 | oldOffset int64 24 | newOffset int64 25 | } 26 | 27 | type trakInfo struct { 28 | rebuilded bool 29 | 30 | sci int 31 | currentChunk int 32 | 33 | index uint32 34 | currentSample uint32 35 | firstSample uint32 36 | } 37 | 38 | type clipFilter struct { 39 | firstChunk int 40 | bufferLength int 41 | 42 | size int64 43 | offset int64 44 | forskip int64 45 | 46 | buffer []byte 47 | chunks []chunk 48 | 49 | m *stream.MP4 50 | reader io.Reader 51 | 52 | end time.Duration 53 | begin time.Duration 54 | } 55 | 56 | type ClipInterface interface { 57 | io.ReadSeeker 58 | 59 | Filter() error 60 | WriteTo(io.Writer) (n int64, err error) 61 | } 62 | 63 | // Clip returns a filter that extracts a clip between begin and begin + duration (in seconds, starting at 0) 64 | // Il will try to include a key frame at the beginning, and keeps the same chunks as the origin media 65 | func Clip(m *stream.MP4, begin, duration time.Duration) (ClipInterface, error) { 66 | end := begin + duration 67 | 68 | if begin < 0 { 69 | return nil, ErrClipOutside 70 | } 71 | 72 | if begin > m.Duration() { 73 | return nil, ErrClipOutside 74 | } 75 | 76 | if end > m.Duration() { 77 | end = m.Duration() 78 | } 79 | 80 | if end < 0 { 81 | return nil, ErrClipOutside 82 | } 83 | 84 | return &clipFilter{ 85 | m: m, 86 | end: end, 87 | begin: begin, 88 | }, nil 89 | } 90 | 91 | func (f *clipFilter) Seek(offset int64, whence int) (int64, error) { 92 | noffset := f.offset 93 | 94 | if whence == os.SEEK_END { 95 | noffset = f.size + offset 96 | } else if whence == os.SEEK_SET { 97 | noffset = offset 98 | } else if whence == os.SEEK_CUR { 99 | noffset += offset 100 | } else { 101 | return -1, syscall.EINVAL 102 | } 103 | 104 | if noffset < 0 { 105 | return -1, syscall.EINVAL 106 | } 107 | 108 | if noffset > f.size { 109 | return -1, syscall.EINVAL 110 | } 111 | 112 | f.offset = noffset 113 | 114 | return noffset, nil 115 | } 116 | 117 | func (f *clipFilter) Read(buf []byte) (n int, err error) { 118 | var nn int 119 | 120 | if len(buf) == 0 { 121 | return 122 | } 123 | 124 | if int(f.offset) < f.bufferLength && len(buf) > 0 { 125 | nn := copy(buf, f.buffer[f.offset:]) 126 | f.offset += int64(nn) 127 | n += nn 128 | buf = buf[nn:] 129 | 130 | if int(f.offset) >= f.bufferLength { 131 | f.buffer = nil 132 | } 133 | } 134 | 135 | s, seekable := f.reader.(io.ReadSeeker) 136 | 137 | for f.firstChunk < len(f.chunks) && err == nil && len(buf) > 0 { 138 | c := f.chunks[f.firstChunk] 139 | 140 | if f.offset >= c.newOffset+c.size { 141 | f.firstChunk++ 142 | continue 143 | } 144 | 145 | realOffset := c.oldOffset + (f.offset - c.newOffset) 146 | if seekable { 147 | if _, err = s.Seek(realOffset, os.SEEK_SET); err != nil { 148 | return 149 | } 150 | } 151 | 152 | can := int(c.size - (f.offset - c.newOffset)) 153 | 154 | if can > len(buf) { 155 | can = len(buf) 156 | } 157 | 158 | nn, err = io.ReadFull(f.reader, buf[:can]) 159 | f.offset += int64(nn) 160 | n += nn 161 | buf = buf[nn:] 162 | 163 | if nn != can { 164 | if err == nil { 165 | err = ErrTruncatedChunk 166 | } 167 | } 168 | } 169 | 170 | return 171 | } 172 | 173 | func (f *clipFilter) Filter() (err error) { 174 | f.buildChunkList() 175 | 176 | bsz := uint32(stream.BoxHeaderSize) 177 | bsz += uint32(f.m.Moov.Size()) 178 | 179 | for _, b := range f.m.Boxes() { 180 | bsz += uint32(b.Size()) 181 | } 182 | 183 | // Update chunk offset 184 | for _, t := range f.m.Moov.Trak { 185 | for i, _ := range t.Mdia.Minf.Stbl.Stco.ChunkOffset { 186 | t.Mdia.Minf.Stbl.Stco.ChunkOffset[i] += bsz 187 | } 188 | } 189 | 190 | // Prepare blob with moov and other small atoms 191 | buffer := make([]byte, 0) 192 | Buffer := bytes.NewBuffer(buffer) 193 | 194 | if err = f.m.Moov.Encode(Buffer); err != nil { 195 | return 196 | } 197 | 198 | for _, b := range f.m.Boxes() { 199 | if err = b.Encode(Buffer); err != nil { 200 | return 201 | } 202 | } 203 | 204 | buf := make([]byte, stream.BoxHeaderSize) 205 | binary.BigEndian.PutUint32(buf, uint32(f.m.Mdat.Size())) 206 | copy(buf[4:], f.m.Mdat.Type()) 207 | 208 | if _, err = Buffer.Write(buf); err != nil { 209 | return 210 | } 211 | 212 | f.size = int64(f.m.Size()) 213 | f.buffer = Buffer.Bytes() 214 | f.reader = f.m.Mdat.Reader() 215 | f.bufferLength = len(f.buffer) 216 | 217 | if len(f.chunks) > 0 { 218 | f.compactChunks() 219 | } 220 | 221 | f.m = nil 222 | 223 | return 224 | } 225 | 226 | func (f *clipFilter) WriteTo(w io.Writer) (n int64, err error) { 227 | var nn int 228 | var nnn int64 229 | 230 | if nn, err = w.Write(f.buffer); err != nil { 231 | return 232 | } 233 | 234 | n += int64(nn) 235 | s, seekable := f.reader.(io.Seeker) 236 | 237 | for _, c := range f.chunks { 238 | csize := int64(c.size) 239 | 240 | if seekable { 241 | if _, err = s.Seek(int64(c.oldOffset), os.SEEK_SET); err != nil { 242 | return 243 | } 244 | } 245 | 246 | nnn, err = io.CopyN(w, f.reader, csize) 247 | n += nnn 248 | if err != nil { 249 | return 250 | } 251 | 252 | if nnn != csize { 253 | if err == nil { 254 | err = ErrTruncatedChunk 255 | } 256 | } 257 | 258 | if err != nil { 259 | return 260 | } 261 | } 262 | 263 | return 264 | } 265 | 266 | func (f *clipFilter) WriteToN(dst io.Writer, size int64) (n int64, err error) { 267 | var nn int 268 | var nnn int64 269 | 270 | if size == 0 { 271 | return 272 | } 273 | 274 | for int(f.offset) < f.bufferLength && err == nil && n < size { 275 | can := int64(f.bufferLength - int(f.offset)) 276 | 277 | if can > size { 278 | can = size 279 | } 280 | 281 | nn, err = dst.Write(f.buffer[f.offset : f.offset+can]) 282 | f.offset += int64(nn) 283 | n += int64(nn) 284 | 285 | if int(f.offset) == f.bufferLength { 286 | f.buffer = nil 287 | } 288 | } 289 | 290 | s, seekable := f.reader.(io.ReadSeeker) 291 | 292 | for f.firstChunk < len(f.chunks) && err == nil && n < size { 293 | c := f.chunks[f.firstChunk] 294 | 295 | if f.offset >= c.newOffset+c.size { 296 | f.firstChunk++ 297 | continue 298 | } 299 | 300 | realOffset := c.oldOffset + (f.offset - c.newOffset) 301 | 302 | if seekable { 303 | if _, err = s.Seek(realOffset, os.SEEK_SET); err != nil { 304 | return 305 | } 306 | } 307 | 308 | can := c.size - (f.offset - c.newOffset) 309 | 310 | if can > size-n { 311 | can = size - n 312 | } 313 | 314 | nnn, err = io.CopyN(dst, f.reader, can) 315 | f.offset += nnn 316 | n += nnn 317 | 318 | if nnn != can { 319 | if err == nil { 320 | err = ErrTruncatedChunk 321 | } 322 | } 323 | } 324 | 325 | return 326 | } 327 | 328 | func (f *clipFilter) compactChunks() { 329 | newChunks := make([]chunk, 0, 4) 330 | last := f.chunks[0] 331 | last.newOffset = int64(f.bufferLength) 332 | lastBound := last.oldOffset + last.size 333 | for i := 1; i < len(f.chunks); i++ { 334 | ch := f.chunks[i] 335 | if lastBound == ch.oldOffset { 336 | lastBound += ch.size 337 | last.size += ch.size 338 | } else { 339 | newChunks = append(newChunks, last) 340 | ch.newOffset = last.newOffset + last.size 341 | last = ch 342 | lastBound = ch.oldOffset + ch.size 343 | } 344 | } 345 | newChunks = append(newChunks, last) 346 | f.chunks = newChunks 347 | } 348 | 349 | func (f *clipFilter) buildChunkList() { 350 | var sz, mt int 351 | var mv, off, size, sample, current, descriptionID, chunkFirstSample uint32 352 | 353 | for _, t := range f.m.Moov.Trak { 354 | sz += len(t.Mdia.Minf.Stbl.Stco.ChunkOffset) 355 | } 356 | 357 | f.m.Mdat.ContentSize = 0 358 | 359 | f.chunks = make([]chunk, 0, sz) 360 | 361 | cnt := len(f.m.Moov.Trak) 362 | ti := make([]trakInfo, cnt, cnt) 363 | 364 | newFirstChunk := make([][]uint32, cnt, cnt) 365 | newChunkOffset := make([][]uint32, cnt, cnt) 366 | newSamplesPerChunk := make([][]uint32, cnt, cnt) 367 | newSampleDescriptionID := make([][]uint32, cnt, cnt) 368 | 369 | firstChunkSamples := make([]uint32, cnt, cnt) 370 | firstChunkDescriptionID := make([]uint32, cnt, cnt) 371 | 372 | // Correct filters (begin, end) timecode 373 | for tnum, t := range f.m.Moov.Trak { 374 | newFirstChunk[tnum] = make([]uint32, 0, len(t.Mdia.Minf.Stbl.Stsc.FirstChunk)) 375 | newChunkOffset[tnum] = make([]uint32, 0, len(t.Mdia.Minf.Stbl.Stco.ChunkOffset)) 376 | newSamplesPerChunk[tnum] = make([]uint32, 0, len(t.Mdia.Minf.Stbl.Stsc.SamplesPerChunk)) 377 | newSampleDescriptionID[tnum] = make([]uint32, 0, len(t.Mdia.Minf.Stbl.Stsc.SampleDescriptionID)) 378 | 379 | // Find stss. Video trak 380 | if stss := t.Mdia.Minf.Stbl.Stss; stss != nil { 381 | stts := t.Mdia.Minf.Stbl.Stts 382 | 383 | // Find sample number current begin timecode 384 | fs := stts.GetSample(uint32(f.begin.Seconds()) * t.Mdia.Mdhd.Timescale) 385 | 386 | // Find timecode for closest l-frame 387 | tc := stts.GetTimeCode(stss.GetClosestSample(fs)) 388 | 389 | // Rebuild begin timecode 390 | f.begin = time.Second * time.Duration(tc) / time.Duration(t.Mdia.Mdhd.Timescale) 391 | } 392 | } 393 | 394 | f.m.Moov.Mvhd.Duration = uint32((f.end - f.begin).Seconds()) * f.m.Moov.Mvhd.Timescale 395 | 396 | // Skip excess chunks 397 | for tnum, t := range f.m.Moov.Trak { 398 | cti := &ti[tnum] 399 | 400 | stco := t.Mdia.Minf.Stbl.Stco 401 | stsc := t.Mdia.Minf.Stbl.Stsc 402 | 403 | firstSample := t.Mdia.Minf.Stbl.Stts.GetSample(uint32(f.begin.Seconds()) * t.Mdia.Mdhd.Timescale) 404 | lastSample := t.Mdia.Minf.Stbl.Stts.GetSample(uint32(f.end.Seconds()) * t.Mdia.Mdhd.Timescale) 405 | 406 | for i, _ := range stco.ChunkOffset { 407 | if cti.sci < len(stsc.FirstChunk)-1 && i+1 >= int(stsc.FirstChunk[cti.sci+1]) { 408 | cti.sci++ 409 | } 410 | 411 | chunkFirstSample = cti.currentSample 412 | cti.currentSample += stsc.SamplesPerChunk[cti.sci] 413 | 414 | if cti.currentSample-1 < firstSample || chunkFirstSample > lastSample { 415 | continue 416 | } 417 | 418 | cti.currentChunk = i 419 | 420 | cti.firstSample = chunkFirstSample 421 | cti.currentSample = chunkFirstSample 422 | 423 | break 424 | } 425 | 426 | if cti.currentChunk == len(stco.ChunkOffset)-1 { 427 | cnt-- 428 | cti.rebuilded = true 429 | } 430 | } 431 | 432 | for cnt > 0 { 433 | mv = 0 434 | 435 | for tnum, t := range f.m.Moov.Trak { 436 | if ti[tnum].rebuilded { 437 | continue 438 | } 439 | 440 | if mv == 0 || t.Mdia.Minf.Stbl.Stco.ChunkOffset[ti[tnum].currentChunk] < mv { 441 | mt = tnum 442 | mv = t.Mdia.Minf.Stbl.Stco.ChunkOffset[ti[tnum].currentChunk] 443 | } 444 | } 445 | 446 | cti := &ti[mt] 447 | newChunkOffset[mt] = append(newChunkOffset[mt], off) 448 | 449 | stsc := f.m.Moov.Trak[mt].Mdia.Minf.Stbl.Stsc 450 | stsz := f.m.Moov.Trak[mt].Mdia.Minf.Stbl.Stsz 451 | 452 | if cti.sci < len(stsc.FirstChunk)-1 && cti.currentChunk+1 >= int(stsc.FirstChunk[cti.sci+1]) { 453 | cti.sci++ 454 | } 455 | 456 | samples := stsc.SamplesPerChunk[cti.sci] 457 | descriptionID = stsc.SampleDescriptionID[cti.sci] 458 | 459 | size = 0 460 | 461 | for i := 0; i < int(samples); i++ { 462 | size += stsz.GetSampleSize(int(cti.currentSample)) 463 | cti.currentSample++ 464 | } 465 | 466 | off += size 467 | f.m.Mdat.ContentSize += size 468 | 469 | f.chunks = append(f.chunks, chunk{ 470 | size: int64(size), 471 | oldOffset: int64(mv), 472 | }) 473 | 474 | cti.index++ 475 | 476 | if samples != firstChunkSamples[mt] || descriptionID != firstChunkDescriptionID[mt] { 477 | newFirstChunk[mt] = append(newFirstChunk[mt], cti.index) 478 | newSamplesPerChunk[mt] = append(newSamplesPerChunk[mt], samples) 479 | newSampleDescriptionID[mt] = append(newSampleDescriptionID[mt], descriptionID) 480 | firstChunkSamples[mt] = samples 481 | firstChunkDescriptionID[mt] = descriptionID 482 | } 483 | 484 | // Go in next chunk 485 | cti.currentChunk++ 486 | 487 | if cti.currentChunk == len(f.m.Moov.Trak[mt].Mdia.Minf.Stbl.Stco.ChunkOffset) { 488 | cnt-- 489 | cti.rebuilded = true 490 | } 491 | } 492 | 493 | for tnum, t := range f.m.Moov.Trak { 494 | cti := &ti[tnum] 495 | stts := t.Mdia.Minf.Stbl.Stts 496 | start := stts.GetTimeCode(cti.firstSample) 497 | end := stts.GetTimeCode(cti.currentSample) 498 | 499 | t.Tkhd.Duration = ((end - start) / t.Mdia.Mdhd.Timescale) * f.m.Moov.Mvhd.Timescale 500 | t.Mdia.Mdhd.Duration = end - start 501 | 502 | // stts - sample duration 503 | if stts := t.Mdia.Minf.Stbl.Stts; stts != nil { 504 | sample = 0 505 | current = 0 506 | 507 | firstSample := cti.firstSample 508 | currentSample := cti.currentSample 509 | 510 | oldSampleCount := stts.SampleCount 511 | oldSampleTimeDelta := stts.SampleTimeDelta 512 | 513 | newSampleCount := make([]uint32, 0, len(oldSampleCount)) 514 | newSampleTimeDelta := make([]uint32, 0, len(oldSampleTimeDelta)) 515 | 516 | for i := 0; i < len(oldSampleCount) && sample < currentSample; i++ { 517 | if sample+oldSampleCount[i] >= firstSample { 518 | switch { 519 | case sample <= firstSample && sample+oldSampleCount[i] > currentSample: 520 | current = currentSample - firstSample + 1 521 | case sample < firstSample: 522 | current = oldSampleCount[i] + sample - firstSample 523 | case sample+oldSampleCount[i] > currentSample: 524 | current = oldSampleCount[i] + sample - currentSample 525 | default: 526 | current = oldSampleCount[i] 527 | } 528 | 529 | newSampleCount = append(newSampleCount, current) 530 | newSampleTimeDelta = append(newSampleTimeDelta, oldSampleTimeDelta[i]) 531 | } 532 | 533 | sample += oldSampleCount[i] 534 | } 535 | 536 | stts.SampleCount = newSampleCount 537 | stts.SampleTimeDelta = newSampleTimeDelta 538 | } 539 | 540 | // stss (key frames) 541 | if stss := t.Mdia.Minf.Stbl.Stss; stss != nil { 542 | firstSample := cti.firstSample 543 | currentSample := cti.currentSample 544 | 545 | oldSampleNumber := stss.SampleNumber 546 | newSampleNumber := make([]uint32, 0, len(oldSampleNumber)) 547 | 548 | for _, n := range oldSampleNumber { 549 | if n >= firstSample && n <= currentSample { 550 | newSampleNumber = append(newSampleNumber, n-firstSample) 551 | } 552 | } 553 | 554 | stss.SampleNumber = newSampleNumber 555 | } 556 | 557 | // stsz (sample sizes) 558 | if stsz := t.Mdia.Minf.Stbl.Stsz; stsz != nil { 559 | stsz.SampleStart = cti.firstSample 560 | stsz.SampleNumber = cti.currentSample - cti.firstSample 561 | } 562 | 563 | // ctts - time offsets (b-frames) 564 | if ctts := t.Mdia.Minf.Stbl.Ctts; ctts != nil { 565 | sample = 0 566 | 567 | firstSample := cti.firstSample 568 | currentSample := cti.currentSample 569 | 570 | oldSampleCount := ctts.SampleCount 571 | oldSampleOffset := ctts.SampleOffset 572 | 573 | newSampleCount := make([]uint32, 0, len(oldSampleCount)) 574 | newSampleOffset := make([]uint32, 0, len(oldSampleOffset)) 575 | 576 | for i := 0; i < len(oldSampleCount) && sample < currentSample; i++ { 577 | if sample+oldSampleCount[i] >= firstSample { 578 | current := oldSampleCount[i] 579 | 580 | if sample+oldSampleCount[i] >= firstSample && sample < firstSample { 581 | current += sample - firstSample 582 | } 583 | 584 | if sample+oldSampleCount[i] > currentSample { 585 | current += currentSample - sample - oldSampleCount[i] 586 | } 587 | 588 | newSampleCount = append(newSampleCount, current) 589 | newSampleOffset = append(newSampleOffset, oldSampleOffset[i]) 590 | } 591 | 592 | sample += oldSampleCount[i] 593 | } 594 | 595 | ctts.SampleCount = newSampleCount 596 | ctts.SampleOffset = newSampleOffset 597 | } 598 | 599 | // co64 ? 600 | 601 | t.Mdia.Minf.Stbl.Stsc.FirstChunk = newFirstChunk[tnum] 602 | t.Mdia.Minf.Stbl.Stco.ChunkOffset = newChunkOffset[tnum] 603 | t.Mdia.Minf.Stbl.Stsc.SamplesPerChunk = newSamplesPerChunk[tnum] 604 | t.Mdia.Minf.Stbl.Stsc.SampleDescriptionID = newSampleDescriptionID[tnum] 605 | } 606 | } 607 | -------------------------------------------------------------------------------- /stream/mdat.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | // Media Data Box (mdat - optional) 9 | // 10 | // Status: not decoded 11 | // 12 | // The mdat box contains media chunks/samples. 13 | // 14 | // It is not read, only the io.Reader is stored, and will be used to Encode (io.Copy) the box to a io.Writer. 15 | type MdatBox struct { 16 | ContentSize uint32 17 | header [8]byte 18 | r io.Reader 19 | } 20 | 21 | func DecodeMdat(r io.Reader) (Box, error) { 22 | // r is a LimitedReader 23 | if lr, limited := r.(*io.LimitedReader); limited { 24 | r = lr.R 25 | } 26 | return &MdatBox{r: r}, nil 27 | } 28 | 29 | func (b *MdatBox) Type() string { 30 | return "mdat" 31 | } 32 | 33 | func (b *MdatBox) Size() int { 34 | return BoxHeaderSize + int(b.ContentSize) 35 | } 36 | 37 | func (b *MdatBox) Reader() io.Reader { 38 | return b.r 39 | } 40 | 41 | func (b *MdatBox) Encode(w io.Writer) error { 42 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 43 | copy(b.header[4:], b.Type()) 44 | _, err := w.Write(b.header[:]) 45 | if err != nil { 46 | return err 47 | } 48 | _, err = io.Copy(w, b.r) 49 | return err 50 | } 51 | -------------------------------------------------------------------------------- /stream/mdhd.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "time" 8 | ) 9 | 10 | // Media Header Box (mdhd - mandatory) 11 | // 12 | // Contained in : Media Box (mdia) 13 | // 14 | // Status : only version 0 is decoded. version 1 is not supported 15 | // 16 | // Timescale defines the timescale used for tracks. 17 | // Language is a ISO-639-2/T language code stored as 1bit padding + [3]int5 18 | type MdhdBox struct { 19 | Version byte 20 | Flags [3]byte 21 | header [8]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 := readAllO(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 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 60 | copy(b.header[4:], b.Type()) 61 | _, err := w.Write(b.header[:]) 62 | if err != nil { 63 | return err 64 | } 65 | buf := makebuf(b) 66 | buf[0] = b.Version 67 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 68 | binary.BigEndian.PutUint32(buf[4:], b.CreationTime) 69 | binary.BigEndian.PutUint32(buf[8:], b.ModificationTime) 70 | binary.BigEndian.PutUint32(buf[12:], b.Timescale) 71 | binary.BigEndian.PutUint32(buf[16:], b.Duration) 72 | binary.BigEndian.PutUint16(buf[20:], b.Language) 73 | _, err = w.Write(buf) 74 | return err 75 | } 76 | -------------------------------------------------------------------------------- /stream/mdia.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | // Media Box (mdia - mandatory) 9 | // 10 | // Contained in : Track Box (trak) 11 | // 12 | // Status: decoded 13 | // 14 | // Contains all information about the media data. 15 | type MdiaBox struct { 16 | Mdhd *MdhdBox 17 | Minf *MinfBox 18 | boxes []Box 19 | header [8]byte 20 | } 21 | 22 | func DecodeMdia(r io.Reader) (Box, error) { 23 | l, err := DecodeContainer(r) 24 | if err != nil { 25 | return nil, err 26 | } 27 | m := &MdiaBox{ 28 | boxes: make([]Box, 0, len(l)), 29 | } 30 | for _, b := range l { 31 | switch b.Type() { 32 | case "mdhd": 33 | m.Mdhd = b.(*MdhdBox) 34 | case "minf": 35 | m.Minf = b.(*MinfBox) 36 | default: 37 | m.boxes = append(m.boxes, b) 38 | } 39 | } 40 | return m, nil 41 | } 42 | 43 | func (b *MdiaBox) Type() string { 44 | return "mdia" 45 | } 46 | 47 | func (b *MdiaBox) Size() (sz int) { 48 | sz += b.Mdhd.Size() 49 | 50 | if b.Minf != nil { 51 | sz += b.Minf.Size() 52 | } 53 | 54 | for _, box := range b.boxes { 55 | sz += box.Size() 56 | } 57 | 58 | return sz + BoxHeaderSize 59 | } 60 | 61 | func (b *MdiaBox) Dump() { 62 | b.Mdhd.Dump() 63 | if b.Minf != nil { 64 | b.Minf.Dump() 65 | } 66 | } 67 | 68 | func (b *MdiaBox) Encode(w io.Writer) (err error) { 69 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 70 | copy(b.header[4:], b.Type()) 71 | _, err = w.Write(b.header[:]) 72 | if err != nil { 73 | return 74 | } 75 | err = b.Mdhd.Encode(w) 76 | if err != nil { 77 | return 78 | } 79 | 80 | for _, b := range b.boxes { 81 | if err = b.Encode(w); err != nil { 82 | return err 83 | } 84 | } 85 | 86 | return b.Minf.Encode(w) 87 | } 88 | -------------------------------------------------------------------------------- /stream/minf.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | // Media Information Box (minf - mandatory) 9 | // 10 | // Contained in : Media Box (mdia) 11 | // 12 | // Status: partially decoded (hmhd - hint tracks - and nmhd - null media - are ignored) 13 | type MinfBox struct { 14 | Stbl *StblBox 15 | boxes []Box 16 | header [8]byte 17 | } 18 | 19 | func DecodeMinf(r io.Reader) (Box, error) { 20 | l, err := DecodeContainer(r) 21 | if err != nil { 22 | return nil, err 23 | } 24 | m := &MinfBox{ 25 | boxes: make([]Box, 0, len(l)), 26 | } 27 | for _, b := range l { 28 | switch b.Type() { 29 | case "stbl": 30 | m.Stbl = b.(*StblBox) 31 | default: 32 | m.boxes = append(m.boxes, b) 33 | } 34 | } 35 | return m, nil 36 | } 37 | 38 | func (b *MinfBox) Type() string { 39 | return "minf" 40 | } 41 | 42 | func (b *MinfBox) Size() (sz int) { 43 | sz += b.Stbl.Size() 44 | 45 | for _, box := range b.boxes { 46 | sz += box.Size() 47 | } 48 | 49 | return sz + BoxHeaderSize 50 | } 51 | 52 | func (b *MinfBox) Dump() { 53 | b.Stbl.Dump() 54 | } 55 | 56 | func (b *MinfBox) Encode(w io.Writer) (err error) { 57 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 58 | copy(b.header[4:], b.Type()) 59 | _, err = w.Write(b.header[:]) 60 | if err != nil { 61 | return 62 | } 63 | 64 | for _, b := range b.boxes { 65 | if err = b.Encode(w); err != nil { 66 | return 67 | } 68 | } 69 | 70 | return b.Stbl.Encode(w) 71 | } 72 | -------------------------------------------------------------------------------- /stream/moov.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | // Movie Box (moov - mandatory) 11 | // 12 | // Status: partially decoded (anything other than mvhd, iods, trak or udta is ignored) 13 | // 14 | // Contains all meta-data. To be able to stream a file, the moov box should be placed before the mdat box. 15 | type MoovBox struct { 16 | Mvhd *MvhdBox 17 | Trak []*TrakBox 18 | boxes []Box 19 | header [8]byte 20 | } 21 | 22 | func DecodeMoov(r io.Reader) (Box, error) { 23 | l, err := DecodeContainer(bufio.NewReaderSize(r, 512*1024)) 24 | if err != nil { 25 | return nil, err 26 | } 27 | m := &MoovBox{} 28 | for _, b := range l { 29 | switch b.Type() { 30 | case "mvhd": 31 | m.Mvhd = b.(*MvhdBox) 32 | case "trak": 33 | m.Trak = append(m.Trak, b.(*TrakBox)) 34 | default: 35 | m.boxes = append(m.boxes, b) 36 | } 37 | } 38 | return m, nil 39 | } 40 | 41 | func (b *MoovBox) Type() string { 42 | return "moov" 43 | } 44 | 45 | func (b *MoovBox) Size() (sz int) { 46 | sz += b.Mvhd.Size() 47 | 48 | for _, t := range b.Trak { 49 | sz += t.Size() 50 | } 51 | 52 | for _, box := range b.boxes { 53 | sz += box.Size() 54 | } 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) (err error) { 68 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 69 | copy(b.header[4:], b.Type()) 70 | _, err = w.Write(b.header[:]) 71 | if err != nil { 72 | return 73 | } 74 | 75 | for _, t := range b.Trak { 76 | if err = t.Encode(w); err != nil { 77 | return 78 | } 79 | } 80 | 81 | for _, b := range b.boxes { 82 | if err = b.Encode(w); err != nil { 83 | return 84 | } 85 | } 86 | 87 | return b.Mvhd.Encode(w) 88 | } 89 | -------------------------------------------------------------------------------- /stream/mvhd.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "time" 8 | ) 9 | 10 | // Movie Header Box (mvhd - mandatory) 11 | // 12 | // Contained in : Movie Box (‘moov’) 13 | // 14 | // Status: version 0 is partially decoded. version 1 is not supported 15 | // 16 | // Contains all media information (duration, ...). 17 | // 18 | // Duration is measured in "time units", and timescale defines the number of time units per second. 19 | // 20 | // Only version 0 is decoded. 21 | type MvhdBox struct { 22 | Version byte 23 | Flags [3]byte 24 | header [8]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 := readAllO(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 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 67 | copy(b.header[4:], b.Type()) 68 | _, err := w.Write(b.header[:]) 69 | if err != nil { 70 | return err 71 | } 72 | buf := makebuf(b) 73 | buf[0] = b.Version 74 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 75 | binary.BigEndian.PutUint32(buf[4:], b.CreationTime) 76 | binary.BigEndian.PutUint32(buf[8:], b.ModificationTime) 77 | binary.BigEndian.PutUint32(buf[12:], b.Timescale) 78 | binary.BigEndian.PutUint32(buf[16:], b.Duration) 79 | binary.BigEndian.PutUint32(buf[20:], uint32(b.Rate)) 80 | binary.BigEndian.PutUint16(buf[24:], uint16(b.Volume)) 81 | copy(buf[26:], b.notDecoded) 82 | _, err = w.Write(buf) 83 | return err 84 | } 85 | -------------------------------------------------------------------------------- /stream/stbl.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | // Soample Table Box (stbl - mandatory) 9 | // 10 | // Contained in : Media Information Box (minf) 11 | // 12 | // Status: partially decoded (anything other than stsd, stts, stsc, stss, stsz, stco, ctts is ignored) 13 | // 14 | // The table contains all information relevant to data samples (times, chunks, sizes, ...) 15 | type StblBox struct { 16 | Stts *SttsBox 17 | Stss *StssBox 18 | Stsc *StscBox 19 | Stsz *StszBox 20 | Stco *StcoBox 21 | Ctts *CttsBox 22 | boxes []Box 23 | header [8]byte 24 | } 25 | 26 | func DecodeStbl(r io.Reader) (Box, error) { 27 | l, err := DecodeContainer(r) 28 | if err != nil { 29 | return nil, err 30 | } 31 | s := &StblBox{ 32 | boxes: make([]Box, 0, len(l)), 33 | } 34 | for _, b := range l { 35 | switch b.Type() { 36 | case "stts": 37 | s.Stts = b.(*SttsBox) 38 | case "stsc": 39 | s.Stsc = b.(*StscBox) 40 | case "stss": 41 | s.Stss = b.(*StssBox) 42 | case "stsz": 43 | s.Stsz = b.(*StszBox) 44 | case "stco": 45 | s.Stco = b.(*StcoBox) 46 | case "ctts": 47 | s.Ctts = b.(*CttsBox) 48 | default: 49 | s.boxes = append(s.boxes, b) 50 | } 51 | } 52 | return s, nil 53 | } 54 | 55 | func (b *StblBox) Type() string { 56 | return "stbl" 57 | } 58 | 59 | func (b *StblBox) Size() (sz int) { 60 | if b.Stts != nil { 61 | sz += b.Stts.Size() 62 | } 63 | if b.Stss != nil { 64 | sz += b.Stss.Size() 65 | } 66 | if b.Stsc != nil { 67 | sz += b.Stsc.Size() 68 | } 69 | if b.Stsz != nil { 70 | sz += b.Stsz.Size() 71 | } 72 | if b.Stco != nil { 73 | sz += b.Stco.Size() 74 | } 75 | if b.Ctts != nil { 76 | sz += b.Ctts.Size() 77 | } 78 | for _, box := range b.boxes { 79 | sz += box.Size() 80 | } 81 | return sz + BoxHeaderSize 82 | } 83 | 84 | func (b *StblBox) Dump() { 85 | if b.Stsc != nil { 86 | b.Stsc.Dump() 87 | } 88 | if b.Stts != nil { 89 | b.Stts.Dump() 90 | } 91 | if b.Stss != nil { 92 | b.Stss.Dump() 93 | } 94 | if b.Stco != nil { 95 | b.Stco.Dump() 96 | } 97 | } 98 | 99 | func (b *StblBox) Encode(w io.Writer) error { 100 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 101 | copy(b.header[4:], b.Type()) 102 | _, err := w.Write(b.header[:]) 103 | if err != nil { 104 | return err 105 | } 106 | err = b.Stts.Encode(w) 107 | if err != nil { 108 | return err 109 | } 110 | if b.Stss != nil { 111 | err = b.Stss.Encode(w) 112 | if err != nil { 113 | return err 114 | } 115 | } 116 | err = b.Stsc.Encode(w) 117 | if err != nil { 118 | return err 119 | } 120 | err = b.Stsz.Encode(w) 121 | if err != nil { 122 | return err 123 | } 124 | err = b.Stco.Encode(w) 125 | if err != nil { 126 | return err 127 | } 128 | for _, b := range b.boxes { 129 | if err = b.Encode(w); err != nil { 130 | return err 131 | } 132 | } 133 | if b.Ctts != nil { 134 | return b.Ctts.Encode(w) 135 | } 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /stream/stco.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // Chunk Offset Box (stco - mandatory) 10 | // 11 | // Contained in : Sample Table box (stbl) 12 | // 13 | // Status: decoded 14 | // 15 | // This is the 32bits version of the box, the 64bits version (co64) is not decoded. 16 | // 17 | // The table contains the offsets (starting at the beginning of the file) for each chunk of data for the current track. 18 | // A chunk contains samples, the table defining the allocation of samples to each chunk is stsc. 19 | type StcoBox struct { 20 | Version byte 21 | Flags [3]byte 22 | header [8]byte 23 | ChunkOffset []uint32 24 | } 25 | 26 | func DecodeStco(r io.Reader) (Box, error) { 27 | data, err := readAllO(r) 28 | 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | c := binary.BigEndian.Uint32(data[4:8]) 34 | b := &StcoBox{ 35 | Flags: [3]byte{data[1], data[2], data[3]}, 36 | Version: data[0], 37 | ChunkOffset: make([]uint32, c), 38 | } 39 | 40 | for i := 0; i < int(c); i++ { 41 | b.ChunkOffset[i] = binary.BigEndian.Uint32(data[(8 + 4*i):(12 + 4*i)]) 42 | } 43 | 44 | return b, nil 45 | } 46 | 47 | func (b *StcoBox) Type() string { 48 | return "stco" 49 | } 50 | 51 | func (b *StcoBox) Size() int { 52 | return BoxHeaderSize + 8 + len(b.ChunkOffset)*4 53 | } 54 | 55 | func (b *StcoBox) Dump() { 56 | fmt.Println("Chunk byte offsets:") 57 | for i, o := range b.ChunkOffset { 58 | fmt.Printf(" #%d : starts at %d\n", i, o) 59 | } 60 | } 61 | 62 | func (b *StcoBox) Encode(w io.Writer) error { 63 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 64 | copy(b.header[4:], b.Type()) 65 | _, err := w.Write(b.header[:]) 66 | if err != nil { 67 | return err 68 | } 69 | buf := makebuf(b) 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.ChunkOffset))) 73 | for i := range b.ChunkOffset { 74 | binary.BigEndian.PutUint32(buf[8+4*i:], b.ChunkOffset[i]) 75 | } 76 | _, err = w.Write(buf) 77 | return err 78 | } 79 | -------------------------------------------------------------------------------- /stream/stream.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "io" 5 | "time" 6 | ) 7 | 8 | // A MPEG-4 media 9 | // 10 | // A MPEG-4 media contains three main boxes : 11 | // 12 | // ftyp : the file type box 13 | // moov : the movie box (meta-data) 14 | // mdat : the media data (chunks and samples) 15 | // 16 | // Other boxes can also be present (pdin, moof, mfra, free, ...), but are not decoded. 17 | type MP4 struct { 18 | Moov *MoovBox 19 | Mdat *MdatBox 20 | boxes []Box 21 | } 22 | 23 | // Decode decodes a media from a Reader 24 | func Decode(r io.Reader) (*MP4, error) { 25 | l, err := DecodeContainer(r) 26 | if err != nil { 27 | return nil, err 28 | } 29 | v := &MP4{ 30 | boxes: make([]Box, 0, len(l)), 31 | } 32 | for _, b := range l { 33 | switch b.Type() { 34 | case "moov": 35 | v.Moov = b.(*MoovBox) 36 | case "mdat": 37 | v.Mdat = b.(*MdatBox) 38 | default: 39 | v.boxes = append(v.boxes, b) 40 | } 41 | } 42 | return v, nil 43 | } 44 | 45 | // Dump displays some information about a media 46 | func (m *MP4) Dump() { 47 | m.Moov.Dump() 48 | } 49 | 50 | // Boxes lists the top-level boxes from a media 51 | func (m *MP4) Boxes() []Box { 52 | return m.boxes 53 | } 54 | 55 | // Encode encodes a media to a Writer 56 | func (m *MP4) Encode(w io.Writer) (err error) { 57 | err = m.Moov.Encode(w) 58 | if err != nil { 59 | return err 60 | } 61 | for _, b := range m.boxes { 62 | err = b.Encode(w) 63 | if err != nil { 64 | return err 65 | } 66 | } 67 | return m.Mdat.Encode(w) 68 | } 69 | 70 | func (m *MP4) Size() (sz int) { 71 | sz += m.Moov.Size() 72 | sz += m.Mdat.Size() 73 | 74 | for _, b := range m.Boxes() { 75 | sz += b.Size() 76 | } 77 | 78 | return 79 | } 80 | 81 | func (m *MP4) Duration() time.Duration { 82 | return time.Second * time.Duration(m.Moov.Mvhd.Duration) / time.Duration(m.Moov.Mvhd.Timescale) 83 | } 84 | -------------------------------------------------------------------------------- /stream/stsc.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // Sample To Chunk Box (stsc - mandatory) 10 | // 11 | // Contained in : Sample Table box (stbl) 12 | // 13 | // Status: decoded 14 | // 15 | // A chunk contains samples. This table defines to which chunk a sample is associated. 16 | // Each entry is defined by : 17 | // 18 | // * first chunk : all chunks starting at this index up to the next first chunk have the same sample count/description 19 | // * samples per chunk : number of samples in the chunk 20 | // * description id : description (see the sample description box - stsd) 21 | type StscBox struct { 22 | Version byte 23 | Flags [3]byte 24 | header [8]byte 25 | FirstChunk []uint32 26 | SamplesPerChunk []uint32 27 | SampleDescriptionID []uint32 28 | } 29 | 30 | func DecodeStsc(r io.Reader) (Box, error) { 31 | data, err := readAllO(r) 32 | 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | c := binary.BigEndian.Uint32(data[4:8]) 38 | b := &StscBox{ 39 | Flags: [3]byte{data[1], data[2], data[3]}, 40 | Version: data[0], 41 | FirstChunk: make([]uint32, c), 42 | SamplesPerChunk: make([]uint32, c), 43 | SampleDescriptionID: make([]uint32, c), 44 | } 45 | 46 | for i := 0; i < int(c); i++ { 47 | b.FirstChunk[i] = binary.BigEndian.Uint32(data[(8 + 12*i):(12 + 12*i)]) 48 | b.SamplesPerChunk[i] = binary.BigEndian.Uint32(data[(12 + 12*i):(16 + 12*i)]) 49 | b.SampleDescriptionID[i] = binary.BigEndian.Uint32(data[(16 + 12*i):(20 + 12*i)]) 50 | } 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 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 72 | copy(b.header[4:], b.Type()) 73 | _, err := w.Write(b.header[:]) 74 | if err != nil { 75 | return err 76 | } 77 | buf := makebuf(b) 78 | buf[0] = b.Version 79 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 80 | binary.BigEndian.PutUint32(buf[4:], uint32(len(b.FirstChunk))) 81 | for i := range b.FirstChunk { 82 | binary.BigEndian.PutUint32(buf[8+12*i:], b.FirstChunk[i]) 83 | binary.BigEndian.PutUint32(buf[12+12*i:], b.SamplesPerChunk[i]) 84 | binary.BigEndian.PutUint32(buf[16+12*i:], b.SampleDescriptionID[i]) 85 | } 86 | _, err = w.Write(buf) 87 | return err 88 | } 89 | -------------------------------------------------------------------------------- /stream/stss.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // Sync Sample Box (stss - optional) 10 | // 11 | // Contained in : Sample Table box (stbl) 12 | // 13 | // Status: decoded 14 | // 15 | // This lists all sync samples (key frames for video tracks) in the data. If absent, all samples are sync samples. 16 | type StssBox struct { 17 | Version byte 18 | Flags [3]byte 19 | header [8]byte 20 | SampleNumber []uint32 21 | } 22 | 23 | func DecodeStss(r io.Reader) (Box, error) { 24 | data, err := readAllO(r) 25 | 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | c := binary.BigEndian.Uint32(data[4:8]) 31 | b := &StssBox{ 32 | Flags: [3]byte{data[1], data[2], data[3]}, 33 | Version: data[0], 34 | SampleNumber: make([]uint32, c), 35 | } 36 | 37 | for i := 0; i < int(c); i++ { 38 | b.SampleNumber[i] = binary.BigEndian.Uint32(data[(8 + 4*i):(12 + 4*i)]) 39 | } 40 | 41 | return b, nil 42 | } 43 | 44 | func (b *StssBox) Type() string { 45 | return "stss" 46 | } 47 | 48 | func (b *StssBox) Size() int { 49 | return BoxHeaderSize + 8 + len(b.SampleNumber)*4 50 | } 51 | 52 | func (b *StssBox) Dump() { 53 | fmt.Println("Key frames:") 54 | for i, n := range b.SampleNumber { 55 | fmt.Printf(" #%d : sample #%d\n", i, n) 56 | } 57 | } 58 | 59 | func (b *StssBox) Encode(w io.Writer) error { 60 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 61 | copy(b.header[4:], b.Type()) 62 | _, err := w.Write(b.header[:]) 63 | if err != nil { 64 | return err 65 | } 66 | buf := makebuf(b) 67 | buf[0] = b.Version 68 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 69 | binary.BigEndian.PutUint32(buf[4:], uint32(len(b.SampleNumber))) 70 | for i := range b.SampleNumber { 71 | binary.BigEndian.PutUint32(buf[8+4*i:], b.SampleNumber[i]) 72 | } 73 | _, err = w.Write(buf) 74 | return err 75 | } 76 | 77 | // Find closest l-frame 78 | func (b *StssBox) GetClosestSample(sample uint32) uint32 { 79 | sample++ 80 | 81 | if len(b.SampleNumber) == 0 { 82 | return sample 83 | } 84 | 85 | if sample < b.SampleNumber[0] { 86 | return b.SampleNumber[0] 87 | } 88 | 89 | if sample > b.SampleNumber[len(b.SampleNumber)-1] { 90 | return b.SampleNumber[len(b.SampleNumber)-1] 91 | } 92 | 93 | for i := 0; i < len(b.SampleNumber); i++ { 94 | if b.SampleNumber[i] > sample { 95 | if b.SampleNumber[i]-sample > sample-b.SampleNumber[i-1] { 96 | return b.SampleNumber[i-1] 97 | } else { 98 | return b.SampleNumber[i] 99 | } 100 | } 101 | } 102 | 103 | return sample 104 | } 105 | -------------------------------------------------------------------------------- /stream/stsz.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | // Sample Size Box (stsz - mandatory) 9 | // 10 | // Contained in : Sample Table box (stbl) 11 | // 12 | // Status : decoded 13 | // 14 | // For each track, either stsz of the more compact stz2 must be present. stz2 variant is not supported. 15 | // 16 | // This table lists the size of each sample. If all samples have the same size, it can be defined in the 17 | // SampleUniformSize attribute. 18 | type StszBox struct { 19 | body []byte 20 | header [8]byte 21 | 22 | SampleStart uint32 23 | SampleNumber uint32 24 | SampleUniformSize uint32 25 | } 26 | 27 | func DecodeStsz(r io.Reader) (Box, error) { 28 | data, err := readAllO(r) 29 | 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | b := &StszBox{ 35 | body: data, 36 | SampleUniformSize: binary.BigEndian.Uint32(data[4:8]), 37 | } 38 | 39 | return b, nil 40 | } 41 | 42 | func (b *StszBox) Type() string { 43 | return "stsz" 44 | } 45 | 46 | func (b *StszBox) Size() int { 47 | return BoxHeaderSize + 12 + int(b.SampleNumber)*4 48 | } 49 | 50 | func (b *StszBox) Encode(w io.Writer) (err error) { 51 | defer func() { 52 | b.body = nil 53 | }() 54 | 55 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 56 | copy(b.header[4:], b.Type()) 57 | 58 | if _, err = w.Write(b.header[:]); err != nil { 59 | return 60 | } 61 | 62 | binary.BigEndian.PutUint32(b.body[8:12], uint32(b.SampleNumber)) 63 | 64 | if _, err = w.Write(b.body[:12]); err != nil { 65 | return 66 | } 67 | 68 | if b.SampleUniformSize == 0 { 69 | if _, err = w.Write(b.body[12+4*b.SampleStart : 16+4*(b.SampleStart+b.SampleNumber-1)]); err != nil { 70 | return 71 | } 72 | } 73 | 74 | return err 75 | } 76 | 77 | // GetSampleSize returns the size (in bytes) of a sample 78 | func (b *StszBox) GetSampleSize(i int) uint32 { 79 | if b.SampleUniformSize > 0 { 80 | return b.SampleUniformSize 81 | } 82 | 83 | return binary.BigEndian.Uint32(b.body[(12 + 4*i):(16 + 4*i)]) 84 | } 85 | -------------------------------------------------------------------------------- /stream/stts.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // Decoding Time to Sample Box (stts - mandatory) 10 | // 11 | // Contained in : Sample Table box (stbl) 12 | // 13 | // Status: decoded 14 | // 15 | // This table contains the duration in time units for each sample. 16 | // 17 | // * sample count : the number of consecutive samples having the same duration 18 | // * time delta : duration in time units 19 | type SttsBox struct { 20 | Version byte 21 | Flags [3]byte 22 | header [8]byte 23 | SampleCount []uint32 24 | SampleTimeDelta []uint32 25 | } 26 | 27 | func DecodeStts(r io.Reader) (Box, error) { 28 | data, err := readAllO(r) 29 | 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | c := binary.BigEndian.Uint32(data[4:8]) 35 | b := &SttsBox{ 36 | Flags: [3]byte{data[1], data[2], data[3]}, 37 | Version: data[0], 38 | SampleCount: make([]uint32, c), 39 | SampleTimeDelta: make([]uint32, c), 40 | } 41 | 42 | for i := 0; i < int(c); i++ { 43 | b.SampleCount[i] = binary.BigEndian.Uint32(data[(8 + 8*i):(12 + 8*i)]) 44 | b.SampleTimeDelta[i] = binary.BigEndian.Uint32(data[(12 + 8*i):(16 + 8*i)]) 45 | } 46 | 47 | return b, nil 48 | } 49 | 50 | func (b *SttsBox) Type() string { 51 | return "stts" 52 | } 53 | 54 | func (b *SttsBox) Size() int { 55 | return BoxHeaderSize + 8 + len(b.SampleCount)*8 56 | } 57 | 58 | func (b *SttsBox) Dump() { 59 | fmt.Println("Time to sample:") 60 | for i := range b.SampleCount { 61 | fmt.Printf(" #%d : %d samples with duration %d units\n", i, b.SampleCount[i], b.SampleTimeDelta[i]) 62 | } 63 | } 64 | 65 | func (b *SttsBox) Encode(w io.Writer) error { 66 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 67 | copy(b.header[4:], b.Type()) 68 | _, err := w.Write(b.header[:]) 69 | if err != nil { 70 | return err 71 | } 72 | buf := makebuf(b) 73 | buf[0] = b.Version 74 | buf[1], buf[2], buf[3] = b.Flags[0], b.Flags[1], b.Flags[2] 75 | binary.BigEndian.PutUint32(buf[4:], uint32(len(b.SampleCount))) 76 | for i := range b.SampleCount { 77 | binary.BigEndian.PutUint32(buf[8+8*i:], b.SampleCount[i]) 78 | binary.BigEndian.PutUint32(buf[12+8*i:], b.SampleTimeDelta[i]) 79 | } 80 | _, err = w.Write(buf) 81 | return err 82 | } 83 | 84 | // Find sample number by timecode in units 85 | func (b *SttsBox) GetSample(units uint32) (sample uint32) { 86 | var fbs, fbm uint32 87 | 88 | for i := 0; i < len(b.SampleCount); i++ { 89 | fbm = b.SampleCount[i] * b.SampleTimeDelta[i] 90 | 91 | if fbs+fbm > units { 92 | return sample + (units-fbs)/b.SampleTimeDelta[i] - 1 93 | } 94 | 95 | fbs += fbm 96 | sample += b.SampleCount[i] 97 | } 98 | 99 | if sample > 0 { 100 | sample-- 101 | } 102 | 103 | return 104 | } 105 | 106 | // GetTimeCode returns the timecode (duration since the beginning of the media) 107 | // of the beginning of a sample 108 | func (b *SttsBox) GetTimeCode(sample uint32) (units uint32) { 109 | for i := 0; sample > 0 && i < len(b.SampleCount); i++ { 110 | if sample >= b.SampleCount[i] { 111 | units += b.SampleCount[i] * b.SampleTimeDelta[i] 112 | sample -= b.SampleCount[i] 113 | } else { 114 | units += sample * b.SampleTimeDelta[i] 115 | sample = 0 116 | } 117 | } 118 | 119 | return 120 | } 121 | -------------------------------------------------------------------------------- /stream/tkhd.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // Track Header Box (tkhd - mandatory) 10 | // 11 | // Status : only version 0 is decoded. version 1 is not supported 12 | // 13 | // This box describes the track. Duration is measured in time units (according to the time scale 14 | // defined in the movie header box). 15 | // 16 | // Volume (relevant for audio tracks) is a fixed point number (8 bits + 8 bits). Full volume is 1.0. 17 | // Width and Height (relevant for video tracks) are fixed point numbers (16 bits + 16 bits). 18 | // Video pixels are not necessarily square. 19 | type TkhdBox struct { 20 | Version byte 21 | Flags [3]byte 22 | header [8]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 := readAllO(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 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 65 | copy(b.header[4:], b.Type()) 66 | _, err := w.Write(b.header[:]) 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.TrackId) 76 | binary.BigEndian.PutUint32(buf[20:], b.Duration) 77 | binary.BigEndian.PutUint16(buf[32:], b.Layer) 78 | binary.BigEndian.PutUint16(buf[34:], b.AlternateGroup) 79 | putFixed16(buf[36:], b.Volume) 80 | copy(buf[40:], b.Matrix) 81 | putFixed32(buf[76:], b.Width) 82 | putFixed32(buf[80:], b.Height) 83 | _, err = w.Write(buf) 84 | return err 85 | } 86 | 87 | func (b *TkhdBox) Dump() { 88 | fmt.Println("Track Header:") 89 | fmt.Printf(" Duration: %d units\n WxH: %sx%s\n", b.Duration, b.Width, b.Height) 90 | } 91 | -------------------------------------------------------------------------------- /stream/trak.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | // Track Box (tkhd - mandatory) 9 | // 10 | // Contained in : Movie Box (moov) 11 | // 12 | // A media file can contain one or more tracks. 13 | type TrakBox struct { 14 | Tkhd *TkhdBox 15 | Mdia *MdiaBox 16 | boxes []Box 17 | header [8]byte 18 | } 19 | 20 | func DecodeTrak(r io.Reader) (Box, error) { 21 | l, err := DecodeContainer(r) 22 | if err != nil { 23 | return nil, err 24 | } 25 | t := &TrakBox{ 26 | boxes: make([]Box, 0, len(l)), 27 | } 28 | for _, b := range l { 29 | switch b.Type() { 30 | case "tkhd": 31 | t.Tkhd = b.(*TkhdBox) 32 | case "mdia": 33 | t.Mdia = b.(*MdiaBox) 34 | default: 35 | t.boxes = append(t.boxes, b) 36 | } 37 | } 38 | return t, nil 39 | } 40 | 41 | func (b *TrakBox) Type() string { 42 | return "trak" 43 | } 44 | 45 | func (b *TrakBox) Size() (sz int) { 46 | sz += b.Tkhd.Size() 47 | sz += b.Mdia.Size() 48 | 49 | for _, box := range b.boxes { 50 | sz += box.Size() 51 | } 52 | 53 | return sz + BoxHeaderSize 54 | } 55 | 56 | func (b *TrakBox) Dump() { 57 | b.Tkhd.Dump() 58 | b.Mdia.Dump() 59 | } 60 | 61 | func (b *TrakBox) Encode(w io.Writer) (err error) { 62 | binary.BigEndian.PutUint32(b.header[:4], uint32(b.Size())) 63 | copy(b.header[4:], b.Type()) 64 | _, err = w.Write(b.header[:]) 65 | if err != nil { 66 | return 67 | } 68 | 69 | if err = b.Tkhd.Encode(w); err != nil { 70 | return 71 | } 72 | 73 | for _, b := range b.boxes { 74 | if err = b.Encode(w); err != nil { 75 | return 76 | } 77 | } 78 | 79 | return b.Mdia.Encode(w) 80 | } 81 | -------------------------------------------------------------------------------- /stream/uni.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | // Universal not decoded Box 9 | type UniBox struct { 10 | name string 11 | buff []byte 12 | hbuf [BoxHeaderSize]byte 13 | } 14 | 15 | func DecodeUni(r io.Reader, name string) (Box, error) { 16 | data, err := readAllO(r) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return &UniBox{ 21 | name: name, 22 | buff: data, 23 | }, nil 24 | } 25 | 26 | func (b *UniBox) Type() string { 27 | return b.name 28 | } 29 | 30 | func (b *UniBox) Size() int { 31 | return BoxHeaderSize + len(b.buff) 32 | } 33 | 34 | func (b *UniBox) Encode(w io.Writer) (err error) { 35 | copy(b.hbuf[4:], b.Type()) 36 | binary.BigEndian.PutUint32(b.hbuf[:4], uint32(b.Size())) 37 | 38 | if _, err = w.Write(b.hbuf[:]); err != nil { 39 | return 40 | } 41 | 42 | _, err = w.Write(b.buff) 43 | 44 | return 45 | } 46 | --------------------------------------------------------------------------------