197 lines
3.9 KiB
Go
197 lines
3.9 KiB
Go
package playback
|
|
|
|
import (
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
|
"github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
|
|
)
|
|
|
|
const (
|
|
partDuration = 1 * time.Second
|
|
)
|
|
|
|
type muxerFMP4Track struct {
|
|
id int
|
|
timeScale uint32
|
|
firstDTS int64
|
|
lastDTS int64
|
|
samples []*fmp4.PartSample
|
|
}
|
|
|
|
func findTrack(tracks []*muxerFMP4Track, id int) *muxerFMP4Track {
|
|
for _, track := range tracks {
|
|
if track.id == id {
|
|
return track
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type muxerFMP4 struct {
|
|
w io.Writer
|
|
|
|
init *fmp4.Init
|
|
nextSequenceNumber uint32
|
|
tracks []*muxerFMP4Track
|
|
curTrack *muxerFMP4Track
|
|
outBuf seekablebuffer.Buffer
|
|
}
|
|
|
|
func (w *muxerFMP4) writeInit(init *fmp4.Init) {
|
|
w.init = init
|
|
|
|
w.tracks = make([]*muxerFMP4Track, len(init.Tracks))
|
|
|
|
for i, track := range init.Tracks {
|
|
w.tracks[i] = &muxerFMP4Track{
|
|
id: track.ID,
|
|
timeScale: track.TimeScale,
|
|
firstDTS: -1,
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *muxerFMP4) setTrack(trackID int) {
|
|
w.curTrack = findTrack(w.tracks, trackID)
|
|
}
|
|
|
|
func (w *muxerFMP4) writeSample(
|
|
dts int64,
|
|
ptsOffset int32,
|
|
isNonSyncSample bool,
|
|
_ uint32,
|
|
getPayload func() ([]byte, error),
|
|
) error {
|
|
pl, err := getPayload()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if dts >= 0 {
|
|
if w.curTrack.firstDTS < 0 {
|
|
w.curTrack.firstDTS = dts
|
|
|
|
// if frame is a IDR, remove previous GOP
|
|
if !isNonSyncSample {
|
|
w.curTrack.samples = nil
|
|
}
|
|
} else {
|
|
diff := dts - w.curTrack.lastDTS
|
|
if diff < 0 {
|
|
diff = 0
|
|
}
|
|
w.curTrack.samples[len(w.curTrack.samples)-1].Duration = uint32(diff)
|
|
}
|
|
|
|
w.curTrack.samples = append(w.curTrack.samples, &fmp4.PartSample{
|
|
PTSOffset: ptsOffset,
|
|
IsNonSyncSample: isNonSyncSample,
|
|
Payload: pl,
|
|
})
|
|
w.curTrack.lastDTS = dts
|
|
|
|
partDurationMP4 := durationGoToMp4(partDuration, w.curTrack.timeScale)
|
|
|
|
if (w.curTrack.lastDTS - w.curTrack.firstDTS) >= partDurationMP4 {
|
|
err := w.innerFlush(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
// store GOP of the first frame, and set PTSOffset = 0 and Duration = 0 in each sample
|
|
if !isNonSyncSample { // if frame is a IDR, reset GOP
|
|
w.curTrack.samples = []*fmp4.PartSample{{
|
|
IsNonSyncSample: isNonSyncSample,
|
|
Payload: pl,
|
|
}}
|
|
} else {
|
|
// append frame to current GOP
|
|
w.curTrack.samples = append(w.curTrack.samples, &fmp4.PartSample{
|
|
IsNonSyncSample: isNonSyncSample,
|
|
Payload: pl,
|
|
})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *muxerFMP4) writeFinalDTS(dts int64) {
|
|
if w.curTrack.firstDTS >= 0 {
|
|
diff := dts - w.curTrack.lastDTS
|
|
if diff < 0 {
|
|
diff = 0
|
|
}
|
|
w.curTrack.samples[len(w.curTrack.samples)-1].Duration = uint32(diff)
|
|
}
|
|
}
|
|
|
|
func (w *muxerFMP4) innerFlush(final bool) error {
|
|
var part fmp4.Part
|
|
|
|
for _, track := range w.tracks {
|
|
if track.firstDTS >= 0 && (len(track.samples) > 1 || (final && len(track.samples) != 0)) {
|
|
// do not write the final sample
|
|
// in order to allow changing its duration to compensate NTP-DTS differences
|
|
var samples []*fmp4.PartSample
|
|
if !final {
|
|
samples = track.samples[:len(track.samples)-1]
|
|
} else {
|
|
samples = track.samples
|
|
}
|
|
|
|
part.Tracks = append(part.Tracks, &fmp4.PartTrack{
|
|
ID: track.id,
|
|
BaseTime: uint64(track.firstDTS),
|
|
Samples: samples,
|
|
})
|
|
|
|
if !final {
|
|
track.samples = track.samples[len(track.samples)-1:]
|
|
track.firstDTS = track.lastDTS
|
|
}
|
|
}
|
|
}
|
|
|
|
if part.Tracks != nil {
|
|
part.SequenceNumber = w.nextSequenceNumber
|
|
w.nextSequenceNumber++
|
|
|
|
if w.init != nil {
|
|
err := w.init.Marshal(&w.outBuf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = w.w.Write(w.outBuf.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.init = nil
|
|
w.outBuf.Reset()
|
|
}
|
|
|
|
err := part.Marshal(&w.outBuf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = w.w.Write(w.outBuf.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.outBuf.Reset()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *muxerFMP4) flush() error {
|
|
return w.innerFlush(true)
|
|
}
|