Skip to content

Commit

Permalink
add pmp4 package
Browse files Browse the repository at this point in the history
  • Loading branch information
aler9 committed May 18, 2024
1 parent 0b44f66 commit 87459c0
Show file tree
Hide file tree
Showing 6 changed files with 1,981 additions and 1 deletion.
2 changes: 1 addition & 1 deletion pkg/formats/fmp4/fmp4.go
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Package fmp4 contains a fMP4 reader and writer.
// Package fmp4 contains a fragmented-MP4 reader and writer.
package fmp4
83 changes: 83 additions & 0 deletions pkg/formats/pmp4/mp4_writer.go
Original file line number Diff line number Diff line change
@@ -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
}

Check warning on line 27 in pkg/formats/pmp4/mp4_writer.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/mp4_writer.go#L26-L27

Added lines #L26 - L27 were not covered by tests

_, err = mp4.Marshal(w.w, box, mp4.Context{})
if err != nil {
return 0, err
}

Check warning on line 32 in pkg/formats/pmp4/mp4_writer.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/mp4_writer.go#L31-L32

Added lines #L31 - L32 were not covered by tests

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
}

Check warning on line 46 in pkg/formats/pmp4/mp4_writer.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/mp4_writer.go#L45-L46

Added lines #L45 - L46 were not covered by tests

err = w.writeBoxEnd()
if err != nil {
return 0, err
}

Check warning on line 51 in pkg/formats/pmp4/mp4_writer.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/mp4_writer.go#L50-L51

Added lines #L50 - L51 were not covered by tests

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
}

Check warning on line 60 in pkg/formats/pmp4/mp4_writer.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/mp4_writer.go#L59-L60

Added lines #L59 - L60 were not covered by tests

_, err = w.w.Seek(int64(off), io.SeekStart)
if err != nil {
return err
}

Check warning on line 65 in pkg/formats/pmp4/mp4_writer.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/mp4_writer.go#L64-L65

Added lines #L64 - L65 were not covered by tests

_, err = w.writeBoxStart(box)
if err != nil {
return err
}

Check warning on line 70 in pkg/formats/pmp4/mp4_writer.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/mp4_writer.go#L69-L70

Added lines #L69 - L70 were not covered by tests

err = w.writeBoxEnd()
if err != nil {
return err
}

Check warning on line 75 in pkg/formats/pmp4/mp4_writer.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/mp4_writer.go#L74-L75

Added lines #L74 - L75 were not covered by tests

_, err = w.w.Seek(prevOff, io.SeekStart)
if err != nil {
return err
}

Check warning on line 80 in pkg/formats/pmp4/mp4_writer.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/mp4_writer.go#L79-L80

Added lines #L79 - L80 were not covered by tests

return nil
}
209 changes: 209 additions & 0 deletions pkg/formats/pmp4/presentation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// Package pmp4 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
}

Check warning on line 45 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L44-L45

Added lines #L44 - L45 were not covered by tests

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{ // <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
}

Check warning on line 114 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L113-L114

Added lines #L113 - L114 were not covered by tests

_, err = mw.writeBoxStart(&mp4.Moov{}) // <moov>
if err != nil {
return err
}

Check warning on line 119 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L118-L119

Added lines #L118 - L119 were not covered by tests

mvhd := &mp4.Mvhd{ // <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
}

Check warning on line 131 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L130-L131

Added lines #L130 - L131 were not covered by tests

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
}

Check warning on line 141 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L140-L141

Added lines #L140 - L141 were not covered by tests

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
}

Check warning on line 154 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L153-L154

Added lines #L153 - L154 were not covered by tests

err = mw.writeBoxEnd() // </moov>
if err != nil {
return err
}

Check warning on line 159 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L158-L159

Added lines #L158 - L159 were not covered by tests

moovEndOffset, err := outBuf.Seek(0, io.SeekCurrent)
if err != nil {
return err
}

Check warning on line 164 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L163-L164

Added lines #L163 - L164 were not covered by tests

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
}

Check warning on line 176 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L175-L176

Added lines #L175 - L176 were not covered by tests
}

_, 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
}

Check warning on line 189 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L188-L189

Added lines #L188 - L189 were not covered by tests

_, err = w.Write([]byte{'m', 'd', 'a', 't'})
if err != nil {
return err
}

Check warning on line 194 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L193-L194

Added lines #L193 - L194 were not covered by tests

for _, sa := range sortedSamples {
pl, err := sa.GetPayload()
if err != nil {
return err
}

Check warning on line 200 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L199-L200

Added lines #L199 - L200 were not covered by tests

_, err = w.Write(pl)
if err != nil {
return err
}

Check warning on line 205 in pkg/formats/pmp4/presentation.go

View check run for this annotation

Codecov / codecov/patch

pkg/formats/pmp4/presentation.go#L204-L205

Added lines #L204 - L205 were not covered by tests
}

return nil
}
Loading

0 comments on commit 87459c0

Please sign in to comment.