Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add pmp4 package #125

Merged
merged 1 commit into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading