diff --git a/pkg/formats/fmp4/fmp4.go b/pkg/formats/fmp4/fmp4.go index 53be064..1fe83d0 100644 --- a/pkg/formats/fmp4/fmp4.go +++ b/pkg/formats/fmp4/fmp4.go @@ -1,2 +1,2 @@ -// Package fmp4 contains a fMP4 reader and writer. +// Package fmp4 contains a fragmented-MP4 reader and writer. package fmp4 diff --git a/pkg/formats/pmp4/mp4_writer.go b/pkg/formats/pmp4/mp4_writer.go new file mode 100644 index 0000000..50d5e47 --- /dev/null +++ b/pkg/formats/pmp4/mp4_writer.go @@ -0,0 +1,83 @@ +package pmp4 + +import ( + "io" + + "github.com/abema/go-mp4" +) + +type mp4Writer struct { + w *mp4.Writer +} + +func newMP4Writer(w io.WriteSeeker) *mp4Writer { + return &mp4Writer{ + w: mp4.NewWriter(w), + } +} + +func (w *mp4Writer) writeBoxStart(box mp4.IImmutableBox) (int, error) { + bi := &mp4.BoxInfo{ + Type: box.GetType(), + } + var err error + bi, err = w.w.StartBox(bi) + if err != nil { + return 0, err + } + + _, err = mp4.Marshal(w.w, box, mp4.Context{}) + if err != nil { + return 0, err + } + + return int(bi.Offset), nil +} + +func (w *mp4Writer) writeBoxEnd() error { + _, err := w.w.EndBox() + return err +} + +func (w *mp4Writer) writeBox(box mp4.IImmutableBox) (int, error) { + off, err := w.writeBoxStart(box) + if err != nil { + return 0, err + } + + err = w.writeBoxEnd() + if err != nil { + return 0, err + } + + return off, nil +} + +func (w *mp4Writer) rewriteBox(off int, box mp4.IImmutableBox) error { + prevOff, err := w.w.Seek(0, io.SeekCurrent) + if err != nil { + return err + } + + _, err = w.w.Seek(int64(off), io.SeekStart) + if err != nil { + return err + } + + _, err = w.writeBoxStart(box) + if err != nil { + return err + } + + err = w.writeBoxEnd() + if err != nil { + return err + } + + _, err = w.w.Seek(prevOff, io.SeekStart) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/formats/pmp4/presentation.go b/pkg/formats/pmp4/presentation.go new file mode 100644 index 0000000..e6ee348 --- /dev/null +++ b/pkg/formats/pmp4/presentation.go @@ -0,0 +1,209 @@ +// Package mp4 contains a MP4 presentation muxer. +package pmp4 + +import ( + "io" + "time" + + "github.com/abema/go-mp4" + "github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer" +) + +const ( + globalTimescale = 1000 +) + +func durationMp4ToGo(v int64, timeScale uint32) time.Duration { + timeScale64 := int64(timeScale) + secs := v / timeScale64 + dec := v % timeScale64 + return time.Duration(secs)*time.Second + time.Duration(dec)*time.Second/time.Duration(timeScale64) +} + +// Presentation is timed sequence of video/audio samples. +type Presentation struct { + Tracks []*Track +} + +// Marshal encodes a Presentation. +func (p *Presentation) Marshal(w io.Writer) error { + /* + |ftyp| + |moov| + | |mvhd| + | |trak| + | |trak| + | |....| + |mdat| + */ + + dataSize, sortedSamples := p.sortSamples() + + err := p.marshalFtypAndMoov(w) + if err != nil { + return err + } + + return p.marshalMdat(w, dataSize, sortedSamples) +} + +func (p *Presentation) sortSamples() (uint32, []*Sample) { + sampleCount := 0 + for _, track := range p.Tracks { + sampleCount += len(track.Samples) + } + + processedSamples := make([]int, len(p.Tracks)) + elapsed := make([]int64, len(p.Tracks)) + offset := uint32(0) + sortedSamples := make([]*Sample, sampleCount) + pos := 0 + + for i, track := range p.Tracks { + elapsed[i] = int64(track.TimeOffset) + } + + for { + bestTrack := -1 + var bestElapsed time.Duration + + for i, track := range p.Tracks { + if processedSamples[i] < len(track.Samples) { + elapsedGo := durationMp4ToGo(elapsed[i], track.TimeScale) + + if bestTrack == -1 || elapsedGo < bestElapsed { + bestTrack = i + bestElapsed = elapsedGo + } + } + } + + if bestTrack == -1 { + break + } + + sample := p.Tracks[bestTrack].Samples[processedSamples[bestTrack]] + sample.offset = offset + + processedSamples[bestTrack]++ + elapsed[bestTrack] += int64(sample.Duration) + offset += sample.PayloadSize + sortedSamples[pos] = sample + pos++ + } + + return offset, sortedSamples +} + +func (p *Presentation) marshalFtypAndMoov(w io.Writer) error { + var outBuf seekablebuffer.Buffer + mw := newMP4Writer(&outBuf) + + _, err := mw.writeBox(&mp4.Ftyp{ // + MajorBrand: [4]byte{'i', 's', 'o', 'm'}, + MinorVersion: 1, + CompatibleBrands: []mp4.CompatibleBrandElem{ + {CompatibleBrand: [4]byte{'i', 's', 'o', 'm'}}, + {CompatibleBrand: [4]byte{'i', 's', 'o', '2'}}, + {CompatibleBrand: [4]byte{'m', 'p', '4', '1'}}, + {CompatibleBrand: [4]byte{'m', 'p', '4', '2'}}, + }, + }) + if err != nil { + return err + } + + _, err = mw.writeBoxStart(&mp4.Moov{}) // + if err != nil { + return err + } + + mvhd := &mp4.Mvhd{ // + Timescale: globalTimescale, + Rate: 65536, + Volume: 256, + Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000}, + NextTrackID: uint32(len(p.Tracks) + 1), + } + mvhdOffset, err := mw.writeBox(mvhd) + if err != nil { + return err + } + + stcos := make([]*mp4.Stco, len(p.Tracks)) + stcosOffsets := make([]int, len(p.Tracks)) + + for i, track := range p.Tracks { + var res *headerTrackMarshalResult + res, err = track.marshal(mw) + if err != nil { + return err + } + + stcos[i] = res.stco + stcosOffsets[i] = res.stcoOffset + + if res.presentationDuration > mvhd.DurationV0 { + mvhd.DurationV0 = res.presentationDuration + } + } + + err = mw.rewriteBox(mvhdOffset, mvhd) + if err != nil { + return err + } + + err = mw.writeBoxEnd() // + if err != nil { + return err + } + + moovEndOffset, err := outBuf.Seek(0, io.SeekCurrent) + if err != nil { + return err + } + + dataOffset := moovEndOffset + 8 + + for i := range p.Tracks { + for j := range stcos[i].ChunkOffset { + stcos[i].ChunkOffset[j] += uint32(dataOffset) + } + + err = mw.rewriteBox(stcosOffsets[i], stcos[i]) + if err != nil { + return err + } + } + + _, err = w.Write(outBuf.Bytes()) + return err +} + +func (p *Presentation) marshalMdat(w io.Writer, dataSize uint32, sortedSamples []*Sample) error { + mdatSize := uint32(8) + dataSize + + _, err := w.Write([]byte{byte(mdatSize >> 24), byte(mdatSize >> 16), byte(mdatSize >> 8), byte(mdatSize)}) + if err != nil { + return err + } + + _, err = w.Write([]byte{'m', 'd', 'a', 't'}) + if err != nil { + return err + } + + for _, sa := range sortedSamples { + pl, err := sa.GetPayload() + if err != nil { + return err + } + + _, err = w.Write(pl) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/formats/pmp4/presentation_test.go b/pkg/formats/pmp4/presentation_test.go new file mode 100644 index 0000000..1cedc7a --- /dev/null +++ b/pkg/formats/pmp4/presentation_test.go @@ -0,0 +1,168 @@ +package pmp4 + +import ( + "bytes" + "testing" + + "github.com/bluenviron/mediacommon/pkg/formats/fmp4" + "github.com/stretchr/testify/require" +) + +var casesPresentation = []struct { + name string + dec Presentation + enc []byte +}{ + { + "standard", + Presentation{ + Tracks: []*Track{ + { + ID: 1, + TimeScale: 90000, + TimeOffset: -90000, + Codec: &fmp4.CodecH264{ + SPS: []byte{ // 1920x1080 baseline + 0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, + 0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, + 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20, + }, + PPS: []byte{0x08, 0x06, 0x07, 0x08}, + }, + Samples: []*Sample{ + { + Duration: 90000, + PTSOffset: -45000, + PayloadSize: 2, + GetPayload: func() ([]byte, error) { + return []byte{1, 2}, nil + }, + }, + { + Duration: 90000, + PayloadSize: 2, + GetPayload: func() ([]byte, error) { + return []byte{3, 4}, nil + }, + }, + { + Duration: 90000, + PTSOffset: -45000, + PayloadSize: 2, + GetPayload: func() ([]byte, error) { + return []byte{5, 6}, nil + }, + }, + }, + }, + }, + }, + []byte{ + 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, + 0x69, 0x73, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x01, + 0x69, 0x73, 0x6f, 0x6d, 0x69, 0x73, 0x6f, 0x32, + 0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, + 0x00, 0x00, 0x02, 0xbf, 0x6d, 0x6f, 0x6f, 0x76, + 0x00, 0x00, 0x00, 0x6c, 0x6d, 0x76, 0x68, 0x64, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, + 0x00, 0x00, 0x07, 0xd0, 0x00, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x4b, + 0x74, 0x72, 0x61, 0x6b, 0x00, 0x00, 0x00, 0x5c, + 0x74, 0x6b, 0x68, 0x64, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xd0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + 0x07, 0x80, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73, + 0x00, 0x00, 0x00, 0x1c, 0x65, 0x6c, 0x73, 0x74, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x0f, 0xa0, 0x00, 0x01, 0x5f, 0x90, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc3, + 0x6d, 0x64, 0x69, 0x61, 0x00, 0x00, 0x00, 0x20, + 0x6d, 0x64, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x5f, 0x90, 0x00, 0x02, 0xbf, 0x20, + 0x55, 0xc4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, + 0x68, 0x64, 0x6c, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x76, 0x69, 0x64, 0x65, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x56, 0x69, 0x64, 0x65, + 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, + 0x00, 0x00, 0x00, 0x01, 0x6e, 0x6d, 0x69, 0x6e, + 0x66, 0x00, 0x00, 0x00, 0x14, 0x76, 0x6d, 0x68, + 0x64, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x64, 0x69, 0x6e, 0x66, 0x00, 0x00, 0x00, + 0x1c, 0x64, 0x72, 0x65, 0x66, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x0c, 0x75, 0x72, 0x6c, 0x20, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x01, 0x2e, 0x73, 0x74, 0x62, + 0x6c, 0x00, 0x00, 0x00, 0x96, 0x73, 0x74, 0x73, + 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x86, 0x61, 0x76, 0x63, + 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0x80, 0x04, 0x38, 0x00, 0x48, 0x00, + 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x18, 0xff, 0xff, 0x00, + 0x00, 0x00, 0x30, 0x61, 0x76, 0x63, 0x43, 0x01, + 0x42, 0xc0, 0x28, 0x03, 0x01, 0x00, 0x19, 0x67, + 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, 0x27, + 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00, + 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20, + 0x01, 0x00, 0x04, 0x08, 0x06, 0x07, 0x08, 0x00, + 0x00, 0x00, 0x18, 0x73, 0x74, 0x74, 0x73, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x01, 0x5f, 0x90, 0x00, + 0x00, 0x00, 0x28, 0x63, 0x74, 0x74, 0x73, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x01, 0xff, 0xff, 0x50, 0x38, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0xff, 0xff, 0x50, 0x38, 0x00, + 0x00, 0x00, 0x1c, 0x73, 0x74, 0x73, 0x63, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x73, + 0x74, 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, + 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x73, + 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0xe7, 0x00, + 0x00, 0x00, 0x0e, 0x6d, 0x64, 0x61, 0x74, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06, + }, + }, +} + +func TestPresentationMarshal(t *testing.T) { + for _, ca := range casesPresentation { + t.Run(ca.name, func(t *testing.T) { + var buf bytes.Buffer + err := ca.dec.Marshal(&buf) + require.NoError(t, err) + require.Equal(t, ca.enc, buf.Bytes()) + }) + } +} diff --git a/pkg/formats/pmp4/sample.go b/pkg/formats/pmp4/sample.go new file mode 100644 index 0000000..e7f72f6 --- /dev/null +++ b/pkg/formats/pmp4/sample.go @@ -0,0 +1,12 @@ +package pmp4 + +// Sample is a sample of a Track. +type Sample struct { + Duration uint32 + PTSOffset int32 + IsNonSyncSample bool + PayloadSize uint32 + GetPayload func() ([]byte, error) + + offset uint32 // filled by sortSamples +} diff --git a/pkg/formats/pmp4/track.go b/pkg/formats/pmp4/track.go new file mode 100644 index 0000000..21a99be --- /dev/null +++ b/pkg/formats/pmp4/track.go @@ -0,0 +1,1139 @@ +package pmp4 + +import ( + "fmt" + + "github.com/abema/go-mp4" + "github.com/bluenviron/mediacommon/pkg/codecs/av1" + "github.com/bluenviron/mediacommon/pkg/codecs/h264" + "github.com/bluenviron/mediacommon/pkg/codecs/h265" + "github.com/bluenviron/mediacommon/pkg/formats/fmp4" +) + +// Specification: ISO 14496-1, Table 5 +const ( + objectTypeIndicationVisualISO14496part2 = 0x20 + objectTypeIndicationAudioISO14496part3 = 0x40 + objectTypeIndicationVisualISO1318part2Main = 0x61 + objectTypeIndicationAudioISO11172part3 = 0x6B + objectTypeIndicationVisualISO10918part1 = 0x6C +) + +// Specification: ISO 14496-1, Table 6 +const ( + streamTypeVisualStream = 0x04 + streamTypeAudioStream = 0x05 +) + +func boolToUint8(v bool) uint8 { + if v { + return 1 + } + return 0 +} + +func allSamplesAreSync(samples []*Sample) bool { + for _, sa := range samples { + if sa.IsNonSyncSample { + return false + } + } + return true +} + +type headerTrackMarshalResult struct { + stco *mp4.Stco + stcoOffset int + presentationDuration uint32 +} + +// Track is a track of a Presentation. +type Track struct { + ID int + TimeScale uint32 + TimeOffset int32 + Codec fmp4.Codec + Samples []*Sample +} + +func (t *Track) marshal(w *mp4Writer) (*headerTrackMarshalResult, error) { + /* + |trak| + | |tkhd| + | |edts| + | | |elst| + | |mdia| + | | |mdhd| + | | |hdlr| + | | |minf| + | | | |vmhd| (video) + | | | |smhd| (audio) + | | | |dinf| + | | | | |dref| + | | | | | |url| + | | | |stbl| + | | | | |stsd| + | | | | | |av01| (AV1) + | | | | | | |av1C| + | | | | | |vp09| (VP9) + | | | | | | |vpcC| + | | | | | |hev1| (H265) + | | | | | | |hvcC| + | | | | | |avc1| (H264) + | | | | | | |avcC| + | | | | | |mp4v| (MPEG-4/2/1 video, MJPEG) + | | | | | | |esds| + | | | | | |Opus| (Opus) + | | | | | | |dOps| + | | | | | |mp4a| (MPEG-4/1 audio) + | | | | | | |esds| + | | | | | |ac-3| (AC-3) + | | | | | | |dac3| + | | | | | |ipcm| (LPCM) + | | | | | | |pcmC| + | | | | |stts| + | | | | |stss| + | | | | |ctts| + | | | | |stsc| + | | | | |stsz| + | | | | |stco| + */ + + _, err := w.writeBoxStart(&mp4.Trak{}) // + if err != nil { + return nil, err + } + + var av1SequenceHeader *av1.SequenceHeader + var h265SPS *h265.SPS + var h264SPS *h264.SPS + + var width int + var height int + + switch codec := t.Codec.(type) { + case *fmp4.CodecAV1: + av1SequenceHeader = &av1.SequenceHeader{} + err = av1SequenceHeader.Unmarshal(codec.SequenceHeader) + if err != nil { + return nil, fmt.Errorf("unable to parse AV1 sequence header: %w", err) + } + + width = av1SequenceHeader.Width() + height = av1SequenceHeader.Height() + + case *fmp4.CodecVP9: + if codec.Width == 0 { + return nil, fmt.Errorf("VP9 parameters not provided") + } + + width = codec.Width + height = codec.Height + + case *fmp4.CodecH265: + if len(codec.VPS) == 0 || len(codec.SPS) == 0 || len(codec.PPS) == 0 { + return nil, fmt.Errorf("H265 parameters not provided") + } + + h265SPS = &h265.SPS{} + err = h265SPS.Unmarshal(codec.SPS) + if err != nil { + return nil, fmt.Errorf("unable to parse H265 SPS: %w", err) + } + + width = h265SPS.Width() + height = h265SPS.Height() + + case *fmp4.CodecH264: + if len(codec.SPS) == 0 || len(codec.PPS) == 0 { + return nil, fmt.Errorf("H264 parameters not provided") + } + + h264SPS = &h264.SPS{} + err = h264SPS.Unmarshal(codec.SPS) + if err != nil { + return nil, fmt.Errorf("unable to parse H264 SPS: %w", err) + } + + width = h264SPS.Width() + height = h264SPS.Height() + + case *fmp4.CodecMPEG4Video: + if len(codec.Config) == 0 { + return nil, fmt.Errorf("MPEG-4 Video config not provided") + } + + // TODO: parse config and use real values + width = 800 + height = 600 + + case *fmp4.CodecMPEG1Video: + if len(codec.Config) == 0 { + return nil, fmt.Errorf("MPEG-1/2 Video config not provided") + } + + // TODO: parse config and use real values + width = 800 + height = 600 + + case *fmp4.CodecMJPEG: + if codec.Width == 0 { + return nil, fmt.Errorf("M-JPEG parameters not provided") + } + + width = codec.Width + height = codec.Height + } + + sampleDuration := uint32(0) + for _, sa := range t.Samples { + sampleDuration += sa.Duration + } + + presentationDuration := uint32(((int64(sampleDuration) + int64(t.TimeOffset)) * globalTimescale) / int64(t.TimeScale)) + + if t.Codec.IsVideo() { + _, err = w.writeBox(&mp4.Tkhd{ // + FullBox: mp4.FullBox{ + Flags: [3]byte{0, 0, 3}, + }, + TrackID: uint32(t.ID), + DurationV0: presentationDuration, + Width: uint32(width * 65536), + Height: uint32(height * 65536), + Matrix: [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000}, + }) + if err != nil { + return nil, err + } + } else { + _, err = w.writeBox(&mp4.Tkhd{ // + FullBox: mp4.FullBox{ + Flags: [3]byte{0, 0, 3}, + }, + TrackID: uint32(t.ID), + DurationV0: presentationDuration, + AlternateGroup: 1, + Volume: 256, + Matrix: [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000}, + }) + if err != nil { + return nil, err + } + } + + _, err = w.writeBoxStart(&mp4.Edts{}) // + if err != nil { + return nil, err + } + + err = t.marshalELST(w, sampleDuration) // + if err != nil { + return nil, err + } + + err = w.writeBoxEnd() // + if err != nil { + return nil, err + } + + _, err = w.writeBoxStart(&mp4.Mdia{}) // + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.Mdhd{ // + Timescale: t.TimeScale, + DurationV0: uint32(int64(sampleDuration) + int64(t.TimeOffset)), + Language: [3]byte{'u', 'n', 'd'}, + }) + if err != nil { + return nil, err + } + + if t.Codec.IsVideo() { + _, err = w.writeBox(&mp4.Hdlr{ // + HandlerType: [4]byte{'v', 'i', 'd', 'e'}, + Name: "VideoHandler", + }) + if err != nil { + return nil, err + } + } else { + _, err = w.writeBox(&mp4.Hdlr{ // + HandlerType: [4]byte{'s', 'o', 'u', 'n'}, + Name: "SoundHandler", + }) + if err != nil { + return nil, err + } + } + + _, err = w.writeBoxStart(&mp4.Minf{}) // + if err != nil { + return nil, err + } + + if t.Codec.IsVideo() { + _, err = w.writeBox(&mp4.Vmhd{ // + FullBox: mp4.FullBox{ + Flags: [3]byte{0, 0, 1}, + }, + }) + if err != nil { + return nil, err + } + } else { + _, err = w.writeBox(&mp4.Smhd{}) // + if err != nil { + return nil, err + } + } + + _, err = w.writeBoxStart(&mp4.Dinf{}) // + if err != nil { + return nil, err + } + + _, err = w.writeBoxStart(&mp4.Dref{ // + EntryCount: 1, + }) + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.Url{ // + FullBox: mp4.FullBox{ + Flags: [3]byte{0, 0, 1}, + }, + }) + if err != nil { + return nil, err + } + + err = w.writeBoxEnd() // + if err != nil { + return nil, err + } + + err = w.writeBoxEnd() // + if err != nil { + return nil, err + } + + _, err = w.writeBoxStart(&mp4.Stbl{}) // + if err != nil { + return nil, err + } + + _, err = w.writeBoxStart(&mp4.Stsd{ // + EntryCount: 1, + }) + if err != nil { + return nil, err + } + + switch codec := t.Codec.(type) { + case *fmp4.CodecAV1: + _, err = w.writeBoxStart(&mp4.VisualSampleEntry{ // + SampleEntry: mp4.SampleEntry{ + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeAv01(), + }, + DataReferenceIndex: 1, + }, + Width: uint16(width), + Height: uint16(height), + Horizresolution: 4718592, + Vertresolution: 4718592, + FrameCount: 1, + Depth: 24, + PreDefined3: -1, + }) + if err != nil { + return nil, err + } + + var bs []byte + bs, err = av1.BitstreamMarshal([][]byte{codec.SequenceHeader}) + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.Av1C{ // + Marker: 1, + Version: 1, + SeqProfile: av1SequenceHeader.SeqProfile, + SeqLevelIdx0: av1SequenceHeader.SeqLevelIdx[0], + SeqTier0: boolToUint8(av1SequenceHeader.SeqTier[0]), + HighBitdepth: boolToUint8(av1SequenceHeader.ColorConfig.HighBitDepth), + TwelveBit: boolToUint8(av1SequenceHeader.ColorConfig.TwelveBit), + Monochrome: boolToUint8(av1SequenceHeader.ColorConfig.MonoChrome), + ChromaSubsamplingX: boolToUint8(av1SequenceHeader.ColorConfig.SubsamplingX), + ChromaSubsamplingY: boolToUint8(av1SequenceHeader.ColorConfig.SubsamplingY), + ChromaSamplePosition: uint8(av1SequenceHeader.ColorConfig.ChromaSamplePosition), + ConfigOBUs: bs, + }) + if err != nil { + return nil, err + } + + case *fmp4.CodecVP9: + _, err = w.writeBoxStart(&mp4.VisualSampleEntry{ // + SampleEntry: mp4.SampleEntry{ + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeVp09(), + }, + DataReferenceIndex: 1, + }, + Width: uint16(width), + Height: uint16(height), + Horizresolution: 4718592, + Vertresolution: 4718592, + FrameCount: 1, + Depth: 24, + PreDefined3: -1, + }) + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.VpcC{ // + FullBox: mp4.FullBox{ + Version: 1, + }, + Profile: codec.Profile, + Level: 10, // level 1 + BitDepth: codec.BitDepth, + ChromaSubsampling: codec.ChromaSubsampling, + VideoFullRangeFlag: boolToUint8(codec.ColorRange), + }) + if err != nil { + return nil, err + } + + case *fmp4.CodecH265: + _, err = w.writeBoxStart(&mp4.VisualSampleEntry{ // + SampleEntry: mp4.SampleEntry{ + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeHev1(), + }, + DataReferenceIndex: 1, + }, + Width: uint16(width), + Height: uint16(height), + Horizresolution: 4718592, + Vertresolution: 4718592, + FrameCount: 1, + Depth: 24, + PreDefined3: -1, + }) + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.HvcC{ // + ConfigurationVersion: 1, + GeneralProfileIdc: h265SPS.ProfileTierLevel.GeneralProfileIdc, + GeneralProfileCompatibility: h265SPS.ProfileTierLevel.GeneralProfileCompatibilityFlag, + GeneralConstraintIndicator: [6]uint8{ + codec.SPS[7], codec.SPS[8], codec.SPS[9], + codec.SPS[10], codec.SPS[11], codec.SPS[12], + }, + GeneralLevelIdc: h265SPS.ProfileTierLevel.GeneralLevelIdc, + // MinSpatialSegmentationIdc + // ParallelismType + ChromaFormatIdc: uint8(h265SPS.ChromaFormatIdc), + BitDepthLumaMinus8: uint8(h265SPS.BitDepthLumaMinus8), + BitDepthChromaMinus8: uint8(h265SPS.BitDepthChromaMinus8), + // AvgFrameRate + // ConstantFrameRate + NumTemporalLayers: 1, + // TemporalIdNested + LengthSizeMinusOne: 3, + NumOfNaluArrays: 3, + NaluArrays: []mp4.HEVCNaluArray{ + { + NaluType: byte(h265.NALUType_VPS_NUT), + NumNalus: 1, + Nalus: []mp4.HEVCNalu{{ + Length: uint16(len(codec.VPS)), + NALUnit: codec.VPS, + }}, + }, + { + NaluType: byte(h265.NALUType_SPS_NUT), + NumNalus: 1, + Nalus: []mp4.HEVCNalu{{ + Length: uint16(len(codec.SPS)), + NALUnit: codec.SPS, + }}, + }, + { + NaluType: byte(h265.NALUType_PPS_NUT), + NumNalus: 1, + Nalus: []mp4.HEVCNalu{{ + Length: uint16(len(codec.PPS)), + NALUnit: codec.PPS, + }}, + }, + }, + }) + if err != nil { + return nil, err + } + + case *fmp4.CodecH264: + _, err = w.writeBoxStart(&mp4.VisualSampleEntry{ // + SampleEntry: mp4.SampleEntry{ + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeAvc1(), + }, + DataReferenceIndex: 1, + }, + Width: uint16(width), + Height: uint16(height), + Horizresolution: 4718592, + Vertresolution: 4718592, + FrameCount: 1, + Depth: 24, + PreDefined3: -1, + }) + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.AVCDecoderConfiguration{ // + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeAvcC(), + }, + ConfigurationVersion: 1, + Profile: h264SPS.ProfileIdc, + ProfileCompatibility: codec.SPS[2], + Level: h264SPS.LevelIdc, + LengthSizeMinusOne: 3, + NumOfSequenceParameterSets: 1, + SequenceParameterSets: []mp4.AVCParameterSet{ + { + Length: uint16(len(codec.SPS)), + NALUnit: codec.SPS, + }, + }, + NumOfPictureParameterSets: 1, + PictureParameterSets: []mp4.AVCParameterSet{ + { + Length: uint16(len(codec.PPS)), + NALUnit: codec.PPS, + }, + }, + }) + if err != nil { + return nil, err + } + + case *fmp4.CodecMPEG4Video: //nolint:dupl + _, err = w.writeBoxStart(&mp4.VisualSampleEntry{ // + SampleEntry: mp4.SampleEntry{ + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeMp4v(), + }, + DataReferenceIndex: 1, + }, + Width: uint16(width), + Height: uint16(height), + Horizresolution: 4718592, + Vertresolution: 4718592, + FrameCount: 1, + Depth: 24, + PreDefined3: -1, + }) + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.Esds{ // + Descriptors: []mp4.Descriptor{ + { + Tag: mp4.ESDescrTag, + Size: 32 + uint32(len(codec.Config)), + ESDescriptor: &mp4.ESDescriptor{ + ESID: uint16(t.ID), + }, + }, + { + Tag: mp4.DecoderConfigDescrTag, + Size: 18 + uint32(len(codec.Config)), + DecoderConfigDescriptor: &mp4.DecoderConfigDescriptor{ + ObjectTypeIndication: objectTypeIndicationVisualISO14496part2, + StreamType: streamTypeVisualStream, + Reserved: true, + MaxBitrate: 1000000, + AvgBitrate: 1000000, + }, + }, + { + Tag: mp4.DecSpecificInfoTag, + Size: uint32(len(codec.Config)), + Data: codec.Config, + }, + { + Tag: mp4.SLConfigDescrTag, + Size: 1, + Data: []byte{0x02}, + }, + }, + }) + if err != nil { + return nil, err + } + + case *fmp4.CodecMPEG1Video: //nolint:dupl + _, err = w.writeBoxStart(&mp4.VisualSampleEntry{ // + SampleEntry: mp4.SampleEntry{ + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeMp4v(), + }, + DataReferenceIndex: 1, + }, + Width: uint16(width), + Height: uint16(height), + Horizresolution: 4718592, + Vertresolution: 4718592, + FrameCount: 1, + Depth: 24, + PreDefined3: -1, + }) + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.Esds{ // + Descriptors: []mp4.Descriptor{ + { + Tag: mp4.ESDescrTag, + Size: 32 + uint32(len(codec.Config)), + ESDescriptor: &mp4.ESDescriptor{ + ESID: uint16(t.ID), + }, + }, + { + Tag: mp4.DecoderConfigDescrTag, + Size: 18 + uint32(len(codec.Config)), + DecoderConfigDescriptor: &mp4.DecoderConfigDescriptor{ + ObjectTypeIndication: objectTypeIndicationVisualISO1318part2Main, + StreamType: streamTypeVisualStream, + Reserved: true, + MaxBitrate: 1000000, + AvgBitrate: 1000000, + }, + }, + { + Tag: mp4.DecSpecificInfoTag, + Size: uint32(len(codec.Config)), + Data: codec.Config, + }, + { + Tag: mp4.SLConfigDescrTag, + Size: 1, + Data: []byte{0x02}, + }, + }, + }) + if err != nil { + return nil, err + } + + case *fmp4.CodecMJPEG: //nolint:dupl + _, err = w.writeBoxStart(&mp4.VisualSampleEntry{ // + SampleEntry: mp4.SampleEntry{ + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeMp4v(), + }, + DataReferenceIndex: 1, + }, + Width: uint16(width), + Height: uint16(height), + Horizresolution: 4718592, + Vertresolution: 4718592, + FrameCount: 1, + Depth: 24, + PreDefined3: -1, + }) + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.Esds{ // + Descriptors: []mp4.Descriptor{ + { + Tag: mp4.ESDescrTag, + Size: 27, + ESDescriptor: &mp4.ESDescriptor{ + ESID: uint16(t.ID), + }, + }, + { + Tag: mp4.DecoderConfigDescrTag, + Size: 13, + DecoderConfigDescriptor: &mp4.DecoderConfigDescriptor{ + ObjectTypeIndication: objectTypeIndicationVisualISO10918part1, + StreamType: streamTypeVisualStream, + Reserved: true, + MaxBitrate: 1000000, + AvgBitrate: 1000000, + }, + }, + { + Tag: mp4.SLConfigDescrTag, + Size: 1, + Data: []byte{0x02}, + }, + }, + }) + if err != nil { + return nil, err + } + + case *fmp4.CodecOpus: + _, err = w.writeBoxStart(&mp4.AudioSampleEntry{ // + SampleEntry: mp4.SampleEntry{ + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeOpus(), + }, + DataReferenceIndex: 1, + }, + ChannelCount: uint16(codec.ChannelCount), + SampleSize: 16, + SampleRate: 48000 * 65536, + }) + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.DOps{ // + OutputChannelCount: uint8(codec.ChannelCount), + PreSkip: 312, + InputSampleRate: 48000, + }) + if err != nil { + return nil, err + } + + case *fmp4.CodecMPEG4Audio: + _, err = w.writeBoxStart(&mp4.AudioSampleEntry{ // + SampleEntry: mp4.SampleEntry{ + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeMp4a(), + }, + DataReferenceIndex: 1, + }, + ChannelCount: uint16(codec.ChannelCount), + SampleSize: 16, + SampleRate: uint32(codec.SampleRate * 65536), + }) + if err != nil { + return nil, err + } + + enc, _ := codec.Config.Marshal() + + _, err = w.writeBox(&mp4.Esds{ // + Descriptors: []mp4.Descriptor{ + { + Tag: mp4.ESDescrTag, + Size: 32 + uint32(len(enc)), + ESDescriptor: &mp4.ESDescriptor{ + ESID: uint16(t.ID), + }, + }, + { + Tag: mp4.DecoderConfigDescrTag, + Size: 18 + uint32(len(enc)), + DecoderConfigDescriptor: &mp4.DecoderConfigDescriptor{ + ObjectTypeIndication: objectTypeIndicationAudioISO14496part3, + StreamType: streamTypeAudioStream, + Reserved: true, + MaxBitrate: 128825, + AvgBitrate: 128825, + }, + }, + { + Tag: mp4.DecSpecificInfoTag, + Size: uint32(len(enc)), + Data: enc, + }, + { + Tag: mp4.SLConfigDescrTag, + Size: 1, + Data: []byte{0x02}, + }, + }, + }) + if err != nil { + return nil, err + } + + case *fmp4.CodecMPEG1Audio: + _, err = w.writeBoxStart(&mp4.AudioSampleEntry{ // + SampleEntry: mp4.SampleEntry{ + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeMp4a(), + }, + DataReferenceIndex: 1, + }, + ChannelCount: uint16(codec.ChannelCount), + SampleSize: 16, + SampleRate: uint32(codec.SampleRate * 65536), + }) + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.Esds{ // + Descriptors: []mp4.Descriptor{ + { + Tag: mp4.ESDescrTag, + Size: 27, + ESDescriptor: &mp4.ESDescriptor{ + ESID: uint16(t.ID), + }, + }, + { + Tag: mp4.DecoderConfigDescrTag, + Size: 13, + DecoderConfigDescriptor: &mp4.DecoderConfigDescriptor{ + ObjectTypeIndication: objectTypeIndicationAudioISO11172part3, + StreamType: streamTypeAudioStream, + Reserved: true, + MaxBitrate: 128825, + AvgBitrate: 128825, + }, + }, + { + Tag: mp4.SLConfigDescrTag, + Size: 1, + Data: []byte{0x02}, + }, + }, + }) + if err != nil { + return nil, err + } + + case *fmp4.CodecAC3: + _, err = w.writeBoxStart(&mp4.AudioSampleEntry{ // + SampleEntry: mp4.SampleEntry{ + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeAC3(), + }, + DataReferenceIndex: 1, + }, + ChannelCount: uint16(codec.ChannelCount), + SampleSize: 16, + SampleRate: uint32(codec.SampleRate * 65536), + }) + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.Dac3{ // + Fscod: codec.Fscod, + Bsid: codec.Bsid, + Bsmod: codec.Bsmod, + Acmod: codec.Acmod, + LfeOn: func() uint8 { + if codec.LfeOn { + return 1 + } + return 0 + }(), + BitRateCode: codec.BitRateCode, + }) + if err != nil { + return nil, err + } + + case *fmp4.CodecLPCM: + _, err = w.writeBoxStart(&mp4.AudioSampleEntry{ // + SampleEntry: mp4.SampleEntry{ + AnyTypeBox: mp4.AnyTypeBox{ + Type: mp4.BoxTypeIpcm(), + }, + DataReferenceIndex: 1, + }, + ChannelCount: uint16(codec.ChannelCount), + SampleSize: uint16(codec.BitDepth), // FFmpeg leaves this to 16 instead of using real bit depth + SampleRate: uint32(codec.SampleRate * 65536), + }) + if err != nil { + return nil, err + } + + _, err = w.writeBox(&mp4.PcmC{ // + FormatFlags: func() uint8 { + if codec.LittleEndian { + return 1 + } + return 0 + }(), + PCMSampleSize: uint8(codec.BitDepth), + }) + if err != nil { + return nil, err + } + } + + err = w.writeBoxEnd() // + if err != nil { + return nil, err + } + + err = w.writeBoxEnd() // + if err != nil { + return nil, err + } + + err = t.marshalSTTS(w) // + if err != nil { + return nil, err + } + + err = t.marshalSTSS(w) // + if err != nil { + return nil, err + } + + err = t.marshalCTTS(w) // + if err != nil { + return nil, err + } + + err = t.marshalSTSC(w) // + if err != nil { + return nil, err + } + + err = t.marshalSTSZ(w) // + if err != nil { + return nil, err + } + + stco, stcoOffset, err := t.marshalSTCO(w) // + if err != nil { + return nil, err + } + + err = w.writeBoxEnd() // + if err != nil { + return nil, err + } + + err = w.writeBoxEnd() // + if err != nil { + return nil, err + } + + err = w.writeBoxEnd() // + if err != nil { + return nil, err + } + + err = w.writeBoxEnd() // + if err != nil { + return nil, err + } + + return &headerTrackMarshalResult{ + stco: stco, + stcoOffset: stcoOffset, + presentationDuration: presentationDuration, + }, nil +} + +func (t *Track) marshalELST(w *mp4Writer, sampleDuration uint32) error { + if t.TimeOffset > 0 { + _, err := w.writeBox(&mp4.Elst{ + EntryCount: 2, + Entries: []mp4.ElstEntry{ + { // pause + SegmentDurationV0: uint32((uint64(t.TimeOffset) * globalTimescale) / uint64(t.TimeScale)), + MediaTimeV0: -1, + MediaRateInteger: 1, + MediaRateFraction: 0, + }, + { // presentation + SegmentDurationV0: uint32((uint64(sampleDuration) * globalTimescale) / uint64(t.TimeScale)), + MediaTimeV0: 0, + MediaRateInteger: 1, + MediaRateFraction: 0, + }, + }, + }) + return err + } + + _, err := w.writeBox(&mp4.Elst{ + EntryCount: 1, + Entries: []mp4.ElstEntry{{ + SegmentDurationV0: uint32(((uint64(sampleDuration) + + uint64(-t.TimeOffset)) * globalTimescale) / uint64(t.TimeScale)), + MediaTimeV0: -t.TimeOffset, + MediaRateInteger: 1, + MediaRateFraction: 0, + }}, + }) + return err +} + +func (t *Track) marshalSTTS(w *mp4Writer) error { + entries := []mp4.SttsEntry{{ + SampleCount: 1, + SampleDelta: t.Samples[0].Duration, + }} + + for _, sa := range t.Samples[1:] { + if sa.Duration == entries[len(entries)-1].SampleDelta { + entries[len(entries)-1].SampleCount++ + } else { + entries = append(entries, mp4.SttsEntry{ + SampleCount: 1, + SampleDelta: sa.Duration, + }) + } + } + + _, err := w.writeBox(&mp4.Stts{ + EntryCount: uint32(len(entries)), + Entries: entries, + }) + return err +} + +func (t *Track) marshalSTSS(w *mp4Writer) error { + if allSamplesAreSync(t.Samples) { + return nil + } + + var sampleNumbers []uint32 + + for i, sa := range t.Samples { + if !sa.IsNonSyncSample { + sampleNumbers = append(sampleNumbers, uint32(i+1)) + } + } + + _, err := w.writeBox(&mp4.Stss{ + EntryCount: uint32(len(sampleNumbers)), + SampleNumber: sampleNumbers, + }) + return err +} + +func (t *Track) marshalCTTS(w *mp4Writer) error { + entries := []mp4.CttsEntry{{ + SampleCount: 1, + SampleOffsetV0: uint32(t.Samples[0].PTSOffset), + }} + + for _, sa := range t.Samples[1:] { + if uint32(sa.PTSOffset) == entries[len(entries)-1].SampleOffsetV0 { + entries[len(entries)-1].SampleCount++ + } else { + entries = append(entries, mp4.CttsEntry{ + SampleCount: 1, + SampleOffsetV0: uint32(sa.PTSOffset), + }) + } + } + + _, err := w.writeBox(&mp4.Ctts{ + FullBox: mp4.FullBox{ + Version: 0, + }, + EntryCount: uint32(len(entries)), + Entries: entries, + }) + return err +} + +func (t *Track) marshalSTSC(w *mp4Writer) error { + entries := []mp4.StscEntry{{ + FirstChunk: 1, + SamplesPerChunk: 1, + SampleDescriptionIndex: 1, + }} + + firstSample := t.Samples[0] + off := firstSample.offset + firstSample.PayloadSize + + for _, sa := range t.Samples[1:] { + if sa.offset == off { + entries[len(entries)-1].SamplesPerChunk++ + } else { + entries = append(entries, mp4.StscEntry{ + FirstChunk: uint32(len(entries) + 1), + SamplesPerChunk: 1, + SampleDescriptionIndex: 1, + }) + } + + off = sa.offset + sa.PayloadSize + } + + // further compression + for i := len(entries) - 1; i >= 1; i-- { + if entries[i].SamplesPerChunk == entries[i-1].SamplesPerChunk { + for j := i; j < len(entries)-1; j++ { + entries[j] = entries[j+1] + } + entries = entries[:len(entries)-1] + } + } + + _, err := w.writeBox(&mp4.Stsc{ + EntryCount: uint32(len(entries)), + Entries: entries, + }) + return err +} + +func (t *Track) marshalSTSZ(w *mp4Writer) error { + sampleSizes := make([]uint32, len(t.Samples)) + + for i, sa := range t.Samples { + sampleSizes[i] = sa.PayloadSize + } + + _, err := w.writeBox(&mp4.Stsz{ + SampleSize: 0, + SampleCount: uint32(len(sampleSizes)), + EntrySize: sampleSizes, + }) + return err +} + +func (t *Track) marshalSTCO(w *mp4Writer) (*mp4.Stco, int, error) { + firstSample := t.Samples[0] + off := firstSample.offset + firstSample.PayloadSize + + entries := []uint32{firstSample.offset} + + for _, sa := range t.Samples[1:] { + if sa.offset != off { + entries = append(entries, sa.offset) + } + off = sa.offset + sa.PayloadSize + } + + stco := &mp4.Stco{ + EntryCount: uint32(len(entries)), + ChunkOffset: entries, + } + + offset, err := w.writeBox(stco) + if err != nil { + return nil, 0, err + } + + return stco, offset, err +}