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
+}