hls: move fMP4 generator into dedicated folder
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package fmp4
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AudioSample is an audio sample.
|
||||
type AudioSample struct {
|
||||
AU []byte
|
||||
PTS time.Duration
|
||||
Next *AudioSample
|
||||
}
|
||||
|
||||
// Duration returns the sample duration.
|
||||
func (s AudioSample) Duration() time.Duration {
|
||||
return s.Next.PTS - s.PTS
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package hls
|
||||
package fmp4
|
||||
|
||||
import (
|
||||
gomp4 "github.com/abema/go-mp4"
|
||||
"github.com/aler9/gortsplib"
|
||||
"github.com/aler9/gortsplib/pkg/h264"
|
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/mp4"
|
||||
)
|
||||
|
||||
type myEsds struct {
|
||||
@@ -21,7 +19,7 @@ func init() { //nolint:gochecknoinits
|
||||
gomp4.AddBoxDef(&myEsds{}, 0)
|
||||
}
|
||||
|
||||
func mp4InitGenerateVideoTrack(w *mp4.Writer, trackID int, videoTrack *gortsplib.TrackH264) error {
|
||||
func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.TrackH264) error {
|
||||
/*
|
||||
trak
|
||||
- tkhd
|
||||
@@ -81,7 +79,7 @@ func mp4InitGenerateVideoTrack(w *mp4.Writer, trackID int, videoTrack *gortsplib
|
||||
}
|
||||
|
||||
_, err = w.WriteBox(&gomp4.Mdhd{ // <mdhd/>
|
||||
Timescale: fmp4VideoTimescale, // the number of time units that pass per second
|
||||
Timescale: videoTimescale, // the number of time units that pass per second
|
||||
Language: [3]byte{'u', 'n', 'd'},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -265,7 +263,7 @@ func mp4InitGenerateVideoTrack(w *mp4.Writer, trackID int, videoTrack *gortsplib
|
||||
return nil
|
||||
}
|
||||
|
||||
func mp4InitGenerateAudioTrack(w *mp4.Writer, trackID int, audioTrack *gortsplib.TrackMPEG4Audio) error {
|
||||
func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.TrackMPEG4Audio) error {
|
||||
/*
|
||||
trak
|
||||
- tkhd
|
||||
@@ -511,7 +509,8 @@ func mp4InitGenerateAudioTrack(w *mp4.Writer, trackID int, audioTrack *gortsplib
|
||||
return nil
|
||||
}
|
||||
|
||||
func mp4InitGenerate(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.TrackMPEG4Audio) ([]byte, error) {
|
||||
// GenerateInit generates a FMP4 initialization file.
|
||||
func GenerateInit(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.TrackMPEG4Audio) ([]byte, error) {
|
||||
/*
|
||||
- ftyp
|
||||
- moov
|
||||
@@ -523,7 +522,7 @@ func mp4InitGenerate(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.Trac
|
||||
- trex (audio)
|
||||
*/
|
||||
|
||||
w := mp4.NewWriter()
|
||||
w := newMP4Writer()
|
||||
|
||||
_, err := w.WriteBox(&gomp4.Ftyp{ // <ftyp/>
|
||||
MajorBrand: [4]byte{'m', 'p', '4', '2'},
|
||||
@@ -558,7 +557,7 @@ func mp4InitGenerate(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.Trac
|
||||
trackID := 1
|
||||
|
||||
if videoTrack != nil {
|
||||
err := mp4InitGenerateVideoTrack(w, trackID, videoTrack)
|
||||
err := generateInitVideoTrack(w, trackID, videoTrack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -567,7 +566,7 @@ func mp4InitGenerate(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.Trac
|
||||
}
|
||||
|
||||
if audioTrack != nil {
|
||||
err := mp4InitGenerateAudioTrack(w, trackID, audioTrack)
|
||||
err := generateInitAudioTrack(w, trackID, audioTrack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package mp4
|
||||
package fmp4
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -7,15 +7,15 @@ import (
|
||||
"github.com/orcaman/writerseeker"
|
||||
)
|
||||
|
||||
// Writer is a MP4 writer.
|
||||
type Writer struct {
|
||||
// mp4Writer is a MP4 writer.
|
||||
type mp4Writer struct {
|
||||
buf *writerseeker.WriterSeeker
|
||||
w *gomp4.Writer
|
||||
}
|
||||
|
||||
// NewWriter allocates a Writer.
|
||||
func NewWriter() *Writer {
|
||||
w := &Writer{
|
||||
// newMP4Writer allocates a mp4Writer.
|
||||
func newMP4Writer() *mp4Writer {
|
||||
w := &mp4Writer{
|
||||
buf: &writerseeker.WriterSeeker{},
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func NewWriter() *Writer {
|
||||
}
|
||||
|
||||
// WriteBoxStart writes a box start.
|
||||
func (w *Writer) WriteBoxStart(box gomp4.IImmutableBox) (int, error) {
|
||||
func (w *mp4Writer) WriteBoxStart(box gomp4.IImmutableBox) (int, error) {
|
||||
bi := &gomp4.BoxInfo{
|
||||
Type: box.GetType(),
|
||||
}
|
||||
@@ -44,13 +44,13 @@ func (w *Writer) WriteBoxStart(box gomp4.IImmutableBox) (int, error) {
|
||||
}
|
||||
|
||||
// WriteBoxEnd writes a box end.
|
||||
func (w *Writer) WriteBoxEnd() error {
|
||||
func (w *mp4Writer) WriteBoxEnd() error {
|
||||
_, err := w.w.EndBox()
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteBox writes a self-closing box.
|
||||
func (w *Writer) WriteBox(box gomp4.IImmutableBox) (int, error) {
|
||||
func (w *mp4Writer) WriteBox(box gomp4.IImmutableBox) (int, error) {
|
||||
off, err := w.WriteBoxStart(box)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -65,7 +65,7 @@ func (w *Writer) WriteBox(box gomp4.IImmutableBox) (int, error) {
|
||||
}
|
||||
|
||||
// RewriteBox rewrites a box.
|
||||
func (w *Writer) RewriteBox(off int, box gomp4.IImmutableBox) error {
|
||||
func (w *mp4Writer) RewriteBox(off int, box gomp4.IImmutableBox) error {
|
||||
prevOff, err := w.w.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -95,6 +95,6 @@ func (w *Writer) RewriteBox(off int, box gomp4.IImmutableBox) error {
|
||||
}
|
||||
|
||||
// Bytes returns the MP4 content.
|
||||
func (w *Writer) Bytes() []byte {
|
||||
func (w *mp4Writer) Bytes() []byte {
|
||||
return w.buf.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
package fmp4
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
gomp4 "github.com/abema/go-mp4"
|
||||
"github.com/aler9/gortsplib"
|
||||
)
|
||||
|
||||
func durationGoToMp4(v time.Duration, timescale time.Duration) int64 {
|
||||
return int64(math.Round(float64(v*timescale) / float64(time.Second)))
|
||||
}
|
||||
|
||||
func generatePartVideoTraf(
|
||||
w *mp4Writer,
|
||||
trackID int,
|
||||
videoSamples []*VideoSample,
|
||||
) (*gomp4.Trun, int, error) {
|
||||
/*
|
||||
traf
|
||||
- tfhd
|
||||
- tfdt
|
||||
- trun
|
||||
*/
|
||||
|
||||
_, err := w.WriteBoxStart(&gomp4.Traf{}) // <traf>
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
flags := 0
|
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfhd{ // <tfhd/>
|
||||
FullBox: gomp4.FullBox{
|
||||
Flags: [3]byte{2, byte(flags >> 8), byte(flags)},
|
||||
},
|
||||
TrackID: uint32(trackID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfdt{ // <tfdt/>
|
||||
FullBox: gomp4.FullBox{
|
||||
Version: 1,
|
||||
},
|
||||
// sum of decode durations of all earlier samples
|
||||
BaseMediaDecodeTimeV1: uint64(durationGoToMp4(videoSamples[0].DTS, videoTimescale)),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
flags = 0
|
||||
flags |= 0x01 // data offset present
|
||||
flags |= 0x100 // sample duration present
|
||||
flags |= 0x200 // sample size present
|
||||
flags |= 0x400 // sample flags present
|
||||
flags |= 0x800 // sample composition time offset present or v1
|
||||
|
||||
trun := &gomp4.Trun{ // <trun/>
|
||||
FullBox: gomp4.FullBox{
|
||||
Version: 1,
|
||||
Flags: [3]byte{0, byte(flags >> 8), byte(flags)},
|
||||
},
|
||||
SampleCount: uint32(len(videoSamples)),
|
||||
}
|
||||
|
||||
for _, e := range videoSamples {
|
||||
off := e.PTS - e.DTS
|
||||
|
||||
flags := uint32(0)
|
||||
if !e.IDRPresent {
|
||||
flags |= 1 << 16 // sample_is_non_sync_sample
|
||||
}
|
||||
|
||||
trun.Entries = append(trun.Entries, gomp4.TrunEntry{
|
||||
SampleDuration: uint32(durationGoToMp4(e.Duration(), videoTimescale)),
|
||||
SampleSize: uint32(len(e.AVCC)),
|
||||
SampleFlags: flags,
|
||||
SampleCompositionTimeOffsetV1: int32(durationGoToMp4(off, videoTimescale)),
|
||||
})
|
||||
}
|
||||
|
||||
trunOffset, err := w.WriteBox(trun)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = w.WriteBoxEnd() // </traf>
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return trun, trunOffset, nil
|
||||
}
|
||||
|
||||
func generatePartAudioTraf(
|
||||
w *mp4Writer,
|
||||
trackID int,
|
||||
audioTrack *gortsplib.TrackMPEG4Audio,
|
||||
audioSamples []*AudioSample,
|
||||
) (*gomp4.Trun, int, error) {
|
||||
/*
|
||||
traf
|
||||
- tfhd
|
||||
- tfdt
|
||||
- trun
|
||||
*/
|
||||
|
||||
if len(audioSamples) == 0 {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
_, err := w.WriteBoxStart(&gomp4.Traf{}) // <traf>
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
flags := 0
|
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfhd{ // <tfhd/>
|
||||
FullBox: gomp4.FullBox{
|
||||
Flags: [3]byte{2, byte(flags >> 8), byte(flags)},
|
||||
},
|
||||
TrackID: uint32(trackID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfdt{ // <tfdt/>
|
||||
FullBox: gomp4.FullBox{
|
||||
Version: 1,
|
||||
},
|
||||
// sum of decode durations of all earlier samples
|
||||
BaseMediaDecodeTimeV1: uint64(durationGoToMp4(audioSamples[0].PTS, time.Duration(audioTrack.ClockRate()))),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
flags = 0
|
||||
flags |= 0x01 // data offset present
|
||||
flags |= 0x100 // sample duration present
|
||||
flags |= 0x200 // sample size present
|
||||
|
||||
trun := &gomp4.Trun{ // <trun/>
|
||||
FullBox: gomp4.FullBox{
|
||||
Version: 0,
|
||||
Flags: [3]byte{0, byte(flags >> 8), byte(flags)},
|
||||
},
|
||||
SampleCount: uint32(len(audioSamples)),
|
||||
}
|
||||
|
||||
for _, e := range audioSamples {
|
||||
trun.Entries = append(trun.Entries, gomp4.TrunEntry{
|
||||
SampleDuration: uint32(durationGoToMp4(e.Duration(), time.Duration(audioTrack.ClockRate()))),
|
||||
SampleSize: uint32(len(e.AU)),
|
||||
})
|
||||
}
|
||||
|
||||
trunOffset, err := w.WriteBox(trun)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = w.WriteBoxEnd() // </traf>
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return trun, trunOffset, nil
|
||||
}
|
||||
|
||||
// GeneratePart generates a FMP4 part file.
|
||||
func GeneratePart(
|
||||
videoTrack *gortsplib.TrackH264,
|
||||
audioTrack *gortsplib.TrackMPEG4Audio,
|
||||
videoSamples []*VideoSample,
|
||||
audioSamples []*AudioSample,
|
||||
) ([]byte, error) {
|
||||
/*
|
||||
moof
|
||||
- mfhd
|
||||
- traf (video)
|
||||
- traf (audio)
|
||||
mdat
|
||||
*/
|
||||
|
||||
w := newMP4Writer()
|
||||
|
||||
moofOffset, err := w.WriteBoxStart(&gomp4.Moof{}) // <moof>
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = w.WriteBox(&gomp4.Mfhd{ // <mfhd/>
|
||||
SequenceNumber: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trackID := 1
|
||||
|
||||
var videoTrun *gomp4.Trun
|
||||
var videoTrunOffset int
|
||||
if videoTrack != nil {
|
||||
var err error
|
||||
videoTrun, videoTrunOffset, err = generatePartVideoTraf(
|
||||
w, trackID, videoSamples)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trackID++
|
||||
}
|
||||
|
||||
var audioTrun *gomp4.Trun
|
||||
var audioTrunOffset int
|
||||
if audioTrack != nil {
|
||||
var err error
|
||||
audioTrun, audioTrunOffset, err = generatePartAudioTraf(w, trackID, audioTrack, audioSamples)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = w.WriteBoxEnd() // </moof>
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mdat := &gomp4.Mdat{} // <mdat/>
|
||||
|
||||
dataSize := 0
|
||||
videoDataSize := 0
|
||||
|
||||
if videoTrack != nil {
|
||||
for _, e := range videoSamples {
|
||||
dataSize += len(e.AVCC)
|
||||
}
|
||||
videoDataSize = dataSize
|
||||
}
|
||||
|
||||
if audioTrack != nil {
|
||||
for _, e := range audioSamples {
|
||||
dataSize += len(e.AU)
|
||||
}
|
||||
}
|
||||
|
||||
mdat.Data = make([]byte, dataSize)
|
||||
pos := 0
|
||||
|
||||
if videoTrack != nil {
|
||||
for _, e := range videoSamples {
|
||||
pos += copy(mdat.Data[pos:], e.AVCC)
|
||||
}
|
||||
}
|
||||
|
||||
if audioTrack != nil {
|
||||
for _, e := range audioSamples {
|
||||
pos += copy(mdat.Data[pos:], e.AU)
|
||||
}
|
||||
}
|
||||
|
||||
mdatOffset, err := w.WriteBox(mdat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if videoTrack != nil {
|
||||
videoTrun.DataOffset = int32(mdatOffset - moofOffset + 8)
|
||||
err = w.RewriteBox(videoTrunOffset, videoTrun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if audioTrack != nil && audioTrun != nil {
|
||||
audioTrun.DataOffset = int32(videoDataSize + mdatOffset - moofOffset + 8)
|
||||
err = w.RewriteBox(audioTrunOffset, audioTrun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return w.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package fmp4
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
videoTimescale = 90000
|
||||
)
|
||||
|
||||
// VideoSample is a video sample.
|
||||
type VideoSample struct {
|
||||
NALUs [][]byte
|
||||
PTS time.Duration
|
||||
DTS time.Duration
|
||||
AVCC []byte
|
||||
IDRPresent bool
|
||||
Next *VideoSample
|
||||
}
|
||||
|
||||
// Duration returns the sample duration.
|
||||
func (s VideoSample) Duration() time.Duration {
|
||||
return s.Next.DTS - s.DTS
|
||||
}
|
||||
@@ -7,35 +7,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/aler9/gortsplib"
|
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
|
||||
)
|
||||
|
||||
const (
|
||||
fmp4VideoTimescale = 90000
|
||||
)
|
||||
|
||||
type fmp4VideoSample struct {
|
||||
nalus [][]byte
|
||||
pts time.Duration
|
||||
dts time.Duration
|
||||
avcc []byte
|
||||
idrPresent bool
|
||||
next *fmp4VideoSample
|
||||
}
|
||||
|
||||
func (s fmp4VideoSample) duration() time.Duration {
|
||||
return s.next.dts - s.dts
|
||||
}
|
||||
|
||||
type fmp4AudioSample struct {
|
||||
au []byte
|
||||
pts time.Duration
|
||||
next *fmp4AudioSample
|
||||
}
|
||||
|
||||
func (s fmp4AudioSample) duration() time.Duration {
|
||||
return s.next.pts - s.pts
|
||||
}
|
||||
|
||||
type muxerVariantFMP4 struct {
|
||||
playlist *muxerVariantFMP4Playlist
|
||||
segmenter *muxerVariantFMP4Segmenter
|
||||
@@ -110,7 +85,7 @@ func (v *muxerVariantFMP4) file(name string, msn string, part string, skip strin
|
||||
|
||||
if v.initContent == nil ||
|
||||
(v.videoTrack != nil && (!bytes.Equal(v.videoLastSPS, sps) || !bytes.Equal(v.videoLastPPS, pps))) {
|
||||
initContent, err := mp4InitGenerate(v.videoTrack, v.audioTrack)
|
||||
initContent, err := fmp4.GenerateInit(v.videoTrack, v.audioTrack)
|
||||
if err != nil {
|
||||
return &MuxerFileResponse{Status: http.StatusInternalServerError}
|
||||
}
|
||||
|
||||
@@ -3,298 +3,15 @@ package hls
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
gomp4 "github.com/abema/go-mp4"
|
||||
"github.com/aler9/gortsplib"
|
||||
"github.com/aler9/gortsplib/pkg/mpeg4audio"
|
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/mp4"
|
||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
|
||||
)
|
||||
|
||||
func durationGoToMp4(v time.Duration, timescale time.Duration) int64 {
|
||||
return int64(math.Round(float64(v*timescale) / float64(time.Second)))
|
||||
}
|
||||
|
||||
func mp4PartGenerateVideoTraf(
|
||||
w *mp4.Writer,
|
||||
trackID int,
|
||||
videoSamples []*fmp4VideoSample,
|
||||
) (*gomp4.Trun, int, error) {
|
||||
/*
|
||||
traf
|
||||
- tfhd
|
||||
- tfdt
|
||||
- trun
|
||||
*/
|
||||
|
||||
_, err := w.WriteBoxStart(&gomp4.Traf{}) // <traf>
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
flags := 0
|
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfhd{ // <tfhd/>
|
||||
FullBox: gomp4.FullBox{
|
||||
Flags: [3]byte{2, byte(flags >> 8), byte(flags)},
|
||||
},
|
||||
TrackID: uint32(trackID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfdt{ // <tfdt/>
|
||||
FullBox: gomp4.FullBox{
|
||||
Version: 1,
|
||||
},
|
||||
// sum of decode durations of all earlier samples
|
||||
BaseMediaDecodeTimeV1: uint64(durationGoToMp4(videoSamples[0].dts, fmp4VideoTimescale)),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
flags = 0
|
||||
flags |= 0x01 // data offset present
|
||||
flags |= 0x100 // sample duration present
|
||||
flags |= 0x200 // sample size present
|
||||
flags |= 0x400 // sample flags present
|
||||
flags |= 0x800 // sample composition time offset present or v1
|
||||
|
||||
trun := &gomp4.Trun{ // <trun/>
|
||||
FullBox: gomp4.FullBox{
|
||||
Version: 1,
|
||||
Flags: [3]byte{0, byte(flags >> 8), byte(flags)},
|
||||
},
|
||||
SampleCount: uint32(len(videoSamples)),
|
||||
}
|
||||
|
||||
for _, e := range videoSamples {
|
||||
off := e.pts - e.dts
|
||||
|
||||
flags := uint32(0)
|
||||
if !e.idrPresent {
|
||||
flags |= 1 << 16 // sample_is_non_sync_sample
|
||||
}
|
||||
|
||||
trun.Entries = append(trun.Entries, gomp4.TrunEntry{
|
||||
SampleDuration: uint32(durationGoToMp4(e.duration(), fmp4VideoTimescale)),
|
||||
SampleSize: uint32(len(e.avcc)),
|
||||
SampleFlags: flags,
|
||||
SampleCompositionTimeOffsetV1: int32(durationGoToMp4(off, fmp4VideoTimescale)),
|
||||
})
|
||||
}
|
||||
|
||||
trunOffset, err := w.WriteBox(trun)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = w.WriteBoxEnd() // </traf>
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return trun, trunOffset, nil
|
||||
}
|
||||
|
||||
func mp4PartGenerateAudioTraf(
|
||||
w *mp4.Writer,
|
||||
trackID int,
|
||||
audioTrack *gortsplib.TrackMPEG4Audio,
|
||||
audioSamples []*fmp4AudioSample,
|
||||
) (*gomp4.Trun, int, error) {
|
||||
/*
|
||||
traf
|
||||
- tfhd
|
||||
- tfdt
|
||||
- trun
|
||||
*/
|
||||
|
||||
if len(audioSamples) == 0 {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
_, err := w.WriteBoxStart(&gomp4.Traf{}) // <traf>
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
flags := 0
|
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfhd{ // <tfhd/>
|
||||
FullBox: gomp4.FullBox{
|
||||
Flags: [3]byte{2, byte(flags >> 8), byte(flags)},
|
||||
},
|
||||
TrackID: uint32(trackID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfdt{ // <tfdt/>
|
||||
FullBox: gomp4.FullBox{
|
||||
Version: 1,
|
||||
},
|
||||
// sum of decode durations of all earlier samples
|
||||
BaseMediaDecodeTimeV1: uint64(durationGoToMp4(audioSamples[0].pts, time.Duration(audioTrack.ClockRate()))),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
flags = 0
|
||||
flags |= 0x01 // data offset present
|
||||
flags |= 0x100 // sample duration present
|
||||
flags |= 0x200 // sample size present
|
||||
|
||||
trun := &gomp4.Trun{ // <trun/>
|
||||
FullBox: gomp4.FullBox{
|
||||
Version: 0,
|
||||
Flags: [3]byte{0, byte(flags >> 8), byte(flags)},
|
||||
},
|
||||
SampleCount: uint32(len(audioSamples)),
|
||||
}
|
||||
|
||||
for _, e := range audioSamples {
|
||||
trun.Entries = append(trun.Entries, gomp4.TrunEntry{
|
||||
SampleDuration: uint32(durationGoToMp4(e.duration(), time.Duration(audioTrack.ClockRate()))),
|
||||
SampleSize: uint32(len(e.au)),
|
||||
})
|
||||
}
|
||||
|
||||
trunOffset, err := w.WriteBox(trun)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = w.WriteBoxEnd() // </traf>
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return trun, trunOffset, nil
|
||||
}
|
||||
|
||||
func mp4PartGenerate(
|
||||
videoTrack *gortsplib.TrackH264,
|
||||
audioTrack *gortsplib.TrackMPEG4Audio,
|
||||
videoSamples []*fmp4VideoSample,
|
||||
audioSamples []*fmp4AudioSample,
|
||||
) ([]byte, error) {
|
||||
/*
|
||||
moof
|
||||
- mfhd
|
||||
- traf (video)
|
||||
- traf (audio)
|
||||
mdat
|
||||
*/
|
||||
|
||||
w := mp4.NewWriter()
|
||||
|
||||
moofOffset, err := w.WriteBoxStart(&gomp4.Moof{}) // <moof>
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = w.WriteBox(&gomp4.Mfhd{ // <mfhd/>
|
||||
SequenceNumber: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trackID := 1
|
||||
|
||||
var videoTrun *gomp4.Trun
|
||||
var videoTrunOffset int
|
||||
if videoTrack != nil {
|
||||
var err error
|
||||
videoTrun, videoTrunOffset, err = mp4PartGenerateVideoTraf(
|
||||
w, trackID, videoSamples)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trackID++
|
||||
}
|
||||
|
||||
var audioTrun *gomp4.Trun
|
||||
var audioTrunOffset int
|
||||
if audioTrack != nil {
|
||||
var err error
|
||||
audioTrun, audioTrunOffset, err = mp4PartGenerateAudioTraf(w, trackID, audioTrack, audioSamples)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = w.WriteBoxEnd() // </moof>
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mdat := &gomp4.Mdat{} // <mdat/>
|
||||
|
||||
dataSize := 0
|
||||
videoDataSize := 0
|
||||
|
||||
if videoTrack != nil {
|
||||
for _, e := range videoSamples {
|
||||
dataSize += len(e.avcc)
|
||||
}
|
||||
videoDataSize = dataSize
|
||||
}
|
||||
|
||||
if audioTrack != nil {
|
||||
for _, e := range audioSamples {
|
||||
dataSize += len(e.au)
|
||||
}
|
||||
}
|
||||
|
||||
mdat.Data = make([]byte, dataSize)
|
||||
pos := 0
|
||||
|
||||
if videoTrack != nil {
|
||||
for _, e := range videoSamples {
|
||||
pos += copy(mdat.Data[pos:], e.avcc)
|
||||
}
|
||||
}
|
||||
|
||||
if audioTrack != nil {
|
||||
for _, e := range audioSamples {
|
||||
pos += copy(mdat.Data[pos:], e.au)
|
||||
}
|
||||
}
|
||||
|
||||
mdatOffset, err := w.WriteBox(mdat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if videoTrack != nil {
|
||||
videoTrun.DataOffset = int32(mdatOffset - moofOffset + 8)
|
||||
err = w.RewriteBox(videoTrunOffset, videoTrun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if audioTrack != nil && audioTrun != nil {
|
||||
audioTrun.DataOffset = int32(videoDataSize + mdatOffset - moofOffset + 8)
|
||||
err = w.RewriteBox(audioTrunOffset, audioTrun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return w.Bytes(), nil
|
||||
}
|
||||
|
||||
func fmp4PartName(id uint64) string {
|
||||
return "part" + strconv.FormatUint(id, 10)
|
||||
}
|
||||
@@ -305,8 +22,8 @@ type muxerVariantFMP4Part struct {
|
||||
id uint64
|
||||
|
||||
isIndependent bool
|
||||
videoSamples []*fmp4VideoSample
|
||||
audioSamples []*fmp4AudioSample
|
||||
videoSamples []*fmp4.VideoSample
|
||||
audioSamples []*fmp4.AudioSample
|
||||
renderedContent []byte
|
||||
renderedDuration time.Duration
|
||||
}
|
||||
@@ -341,7 +58,7 @@ func (p *muxerVariantFMP4Part) duration() time.Duration {
|
||||
if p.videoTrack != nil {
|
||||
ret := time.Duration(0)
|
||||
for _, e := range p.videoSamples {
|
||||
ret += e.duration()
|
||||
ret += e.Duration()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -356,7 +73,7 @@ func (p *muxerVariantFMP4Part) duration() time.Duration {
|
||||
func (p *muxerVariantFMP4Part) finalize() error {
|
||||
if len(p.videoSamples) > 0 || len(p.audioSamples) > 0 {
|
||||
var err error
|
||||
p.renderedContent, err = mp4PartGenerate(
|
||||
p.renderedContent, err = fmp4.GeneratePart(
|
||||
p.videoTrack,
|
||||
p.audioTrack,
|
||||
p.videoSamples,
|
||||
@@ -374,13 +91,13 @@ func (p *muxerVariantFMP4Part) finalize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *muxerVariantFMP4Part) writeH264(sample *fmp4VideoSample) {
|
||||
if sample.idrPresent {
|
||||
func (p *muxerVariantFMP4Part) writeH264(sample *fmp4.VideoSample) {
|
||||
if sample.IDRPresent {
|
||||
p.isIndependent = true
|
||||
}
|
||||
p.videoSamples = append(p.videoSamples, sample)
|
||||
}
|
||||
|
||||
func (p *muxerVariantFMP4Part) writeAAC(sample *fmp4AudioSample) {
|
||||
func (p *muxerVariantFMP4Part) writeAAC(sample *fmp4.AudioSample) {
|
||||
p.audioSamples = append(p.audioSamples, sample)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/aler9/gortsplib"
|
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
|
||||
)
|
||||
|
||||
type partsReader struct {
|
||||
@@ -101,8 +103,8 @@ func (s *muxerVariantFMP4Segment) getRenderedDuration() time.Duration {
|
||||
}
|
||||
|
||||
func (s *muxerVariantFMP4Segment) finalize(
|
||||
nextVideoSample *fmp4VideoSample,
|
||||
nextAudioSample *fmp4AudioSample,
|
||||
nextVideoSample *fmp4.VideoSample,
|
||||
nextAudioSample *fmp4.AudioSample,
|
||||
) error {
|
||||
err := s.currentPart.finalize()
|
||||
if err != nil {
|
||||
@@ -117,7 +119,7 @@ func (s *muxerVariantFMP4Segment) finalize(
|
||||
s.currentPart = nil
|
||||
|
||||
if s.videoTrack != nil {
|
||||
s.renderedDuration = nextVideoSample.dts - s.startDTS
|
||||
s.renderedDuration = nextVideoSample.DTS - s.startDTS
|
||||
} else {
|
||||
s.renderedDuration = 0
|
||||
for _, pa := range s.parts {
|
||||
@@ -128,8 +130,8 @@ func (s *muxerVariantFMP4Segment) finalize(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *muxerVariantFMP4Segment) writeH264(sample *fmp4VideoSample, adjustedPartDuration time.Duration) error {
|
||||
size := uint64(len(sample.avcc))
|
||||
func (s *muxerVariantFMP4Segment) writeH264(sample *fmp4.VideoSample, adjustedPartDuration time.Duration) error {
|
||||
size := uint64(len(sample.AVCC))
|
||||
|
||||
if (s.size + size) > s.segmentMaxSize {
|
||||
return fmt.Errorf("reached maximum segment size")
|
||||
@@ -160,8 +162,8 @@ func (s *muxerVariantFMP4Segment) writeH264(sample *fmp4VideoSample, adjustedPar
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *muxerVariantFMP4Segment) writeAAC(sample *fmp4AudioSample, adjustedPartDuration time.Duration) error {
|
||||
size := uint64(len(sample.au))
|
||||
func (s *muxerVariantFMP4Segment) writeAAC(sample *fmp4.AudioSample, adjustedPartDuration time.Duration) error {
|
||||
size := uint64(len(sample.AU))
|
||||
|
||||
if (s.size + size) > s.segmentMaxSize {
|
||||
return fmt.Errorf("reached maximum segment size")
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/aler9/gortsplib"
|
||||
"github.com/aler9/gortsplib/pkg/h264"
|
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
|
||||
)
|
||||
|
||||
func partDurationIsCompatible(partDuration time.Duration, sampleDuration time.Duration) bool {
|
||||
@@ -60,8 +62,8 @@ type muxerVariantFMP4Segmenter struct {
|
||||
currentSegment *muxerVariantFMP4Segment
|
||||
nextSegmentID uint64
|
||||
nextPartID uint64
|
||||
nextVideoSample *fmp4VideoSample
|
||||
nextAudioSample *fmp4AudioSample
|
||||
nextVideoSample *fmp4.VideoSample
|
||||
nextAudioSample *fmp4.AudioSample
|
||||
firstSegmentFinalized bool
|
||||
sampleDurations map[time.Duration]struct{}
|
||||
adjustedPartDuration time.Duration
|
||||
@@ -144,18 +146,18 @@ func (m *muxerVariantFMP4Segmenter) writeH264(pts time.Duration, nalus [][]byte)
|
||||
return err
|
||||
}
|
||||
|
||||
return m.writeH264Entry(&fmp4VideoSample{
|
||||
pts: pts,
|
||||
nalus: nalus,
|
||||
avcc: avcc,
|
||||
idrPresent: idrPresent,
|
||||
return m.writeH264Entry(&fmp4.VideoSample{
|
||||
PTS: pts,
|
||||
NALUs: nalus,
|
||||
AVCC: avcc,
|
||||
IDRPresent: idrPresent,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *muxerVariantFMP4Segmenter) writeH264Entry(sample *fmp4VideoSample) error {
|
||||
func (m *muxerVariantFMP4Segmenter) writeH264Entry(sample *fmp4.VideoSample) error {
|
||||
if !m.videoFirstIDRReceived {
|
||||
// skip sample silently until we find one with an IDR
|
||||
if !sample.idrPresent {
|
||||
if !sample.IDRPresent {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -164,25 +166,25 @@ func (m *muxerVariantFMP4Segmenter) writeH264Entry(sample *fmp4VideoSample) erro
|
||||
m.videoSPS = append([]byte(nil), m.videoTrack.SafeSPS()...)
|
||||
|
||||
var err error
|
||||
sample.dts, err = m.videoDTSExtractor.Extract(sample.nalus, sample.pts)
|
||||
sample.DTS, err = m.videoDTSExtractor.Extract(sample.NALUs, sample.PTS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sample.nalus = nil
|
||||
sample.NALUs = nil
|
||||
|
||||
m.startDTS = sample.dts
|
||||
sample.dts = 0
|
||||
sample.pts -= m.startDTS
|
||||
m.startDTS = sample.DTS
|
||||
sample.DTS = 0
|
||||
sample.PTS -= m.startDTS
|
||||
} else {
|
||||
var err error
|
||||
sample.dts, err = m.videoDTSExtractor.Extract(sample.nalus, sample.pts)
|
||||
sample.DTS, err = m.videoDTSExtractor.Extract(sample.NALUs, sample.PTS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sample.nalus = nil
|
||||
sample.NALUs = nil
|
||||
|
||||
sample.dts -= m.startDTS
|
||||
sample.pts -= m.startDTS
|
||||
sample.DTS -= m.startDTS
|
||||
sample.PTS -= m.startDTS
|
||||
}
|
||||
|
||||
// put samples into a queue in order to
|
||||
@@ -192,7 +194,7 @@ func (m *muxerVariantFMP4Segmenter) writeH264Entry(sample *fmp4VideoSample) erro
|
||||
if sample == nil {
|
||||
return nil
|
||||
}
|
||||
sample.next = m.nextVideoSample
|
||||
sample.Next = m.nextVideoSample
|
||||
|
||||
now := time.Now()
|
||||
|
||||
@@ -202,7 +204,7 @@ func (m *muxerVariantFMP4Segmenter) writeH264Entry(sample *fmp4VideoSample) erro
|
||||
m.lowLatency,
|
||||
m.genSegmentID(),
|
||||
now,
|
||||
sample.dts,
|
||||
sample.DTS,
|
||||
m.segmentMaxSize,
|
||||
m.videoTrack,
|
||||
m.audioTrack,
|
||||
@@ -211,7 +213,7 @@ func (m *muxerVariantFMP4Segmenter) writeH264Entry(sample *fmp4VideoSample) erro
|
||||
)
|
||||
}
|
||||
|
||||
m.adjustPartDuration(sample.duration())
|
||||
m.adjustPartDuration(sample.Duration())
|
||||
|
||||
err := m.currentSegment.writeH264(sample, m.adjustedPartDuration)
|
||||
if err != nil {
|
||||
@@ -219,13 +221,13 @@ func (m *muxerVariantFMP4Segmenter) writeH264Entry(sample *fmp4VideoSample) erro
|
||||
}
|
||||
|
||||
// switch segment
|
||||
if sample.next.idrPresent {
|
||||
if sample.Next.IDRPresent {
|
||||
sps := m.videoTrack.SafeSPS()
|
||||
spsChanged := !bytes.Equal(m.videoSPS, sps)
|
||||
|
||||
if (sample.next.dts-m.currentSegment.startDTS) >= m.segmentDuration ||
|
||||
if (sample.Next.DTS-m.currentSegment.startDTS) >= m.segmentDuration ||
|
||||
spsChanged {
|
||||
err := m.currentSegment.finalize(sample.next, nil)
|
||||
err := m.currentSegment.finalize(sample.Next, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -237,7 +239,7 @@ func (m *muxerVariantFMP4Segmenter) writeH264Entry(sample *fmp4VideoSample) erro
|
||||
m.lowLatency,
|
||||
m.genSegmentID(),
|
||||
now,
|
||||
sample.next.dts,
|
||||
sample.Next.DTS,
|
||||
m.segmentMaxSize,
|
||||
m.videoTrack,
|
||||
m.audioTrack,
|
||||
@@ -258,20 +260,20 @@ func (m *muxerVariantFMP4Segmenter) writeH264Entry(sample *fmp4VideoSample) erro
|
||||
}
|
||||
|
||||
func (m *muxerVariantFMP4Segmenter) writeAAC(pts time.Duration, au []byte) error {
|
||||
return m.writeAACEntry(&fmp4AudioSample{
|
||||
pts: pts,
|
||||
au: au,
|
||||
return m.writeAACEntry(&fmp4.AudioSample{
|
||||
PTS: pts,
|
||||
AU: au,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *muxerVariantFMP4Segmenter) writeAACEntry(sample *fmp4AudioSample) error {
|
||||
func (m *muxerVariantFMP4Segmenter) writeAACEntry(sample *fmp4.AudioSample) error {
|
||||
if m.videoTrack != nil {
|
||||
// wait for the video track
|
||||
if !m.videoFirstIDRReceived {
|
||||
return nil
|
||||
}
|
||||
|
||||
sample.pts -= m.startDTS
|
||||
sample.PTS -= m.startDTS
|
||||
}
|
||||
|
||||
// put samples into a queue in order to
|
||||
@@ -280,7 +282,7 @@ func (m *muxerVariantFMP4Segmenter) writeAACEntry(sample *fmp4AudioSample) error
|
||||
if sample == nil {
|
||||
return nil
|
||||
}
|
||||
sample.next = m.nextAudioSample
|
||||
sample.Next = m.nextAudioSample
|
||||
|
||||
now := time.Now()
|
||||
|
||||
@@ -291,7 +293,7 @@ func (m *muxerVariantFMP4Segmenter) writeAACEntry(sample *fmp4AudioSample) error
|
||||
m.lowLatency,
|
||||
m.genSegmentID(),
|
||||
now,
|
||||
sample.pts,
|
||||
sample.PTS,
|
||||
m.segmentMaxSize,
|
||||
m.videoTrack,
|
||||
m.audioTrack,
|
||||
@@ -313,8 +315,8 @@ func (m *muxerVariantFMP4Segmenter) writeAACEntry(sample *fmp4AudioSample) error
|
||||
|
||||
// switch segment
|
||||
if m.videoTrack == nil &&
|
||||
(sample.next.pts-m.currentSegment.startDTS) >= m.segmentDuration {
|
||||
err := m.currentSegment.finalize(nil, sample.next)
|
||||
(sample.Next.PTS-m.currentSegment.startDTS) >= m.segmentDuration {
|
||||
err := m.currentSegment.finalize(nil, sample.Next)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -326,7 +328,7 @@ func (m *muxerVariantFMP4Segmenter) writeAACEntry(sample *fmp4AudioSample) error
|
||||
m.lowLatency,
|
||||
m.genSegmentID(),
|
||||
now,
|
||||
sample.next.pts,
|
||||
sample.Next.PTS,
|
||||
m.segmentMaxSize,
|
||||
m.videoTrack,
|
||||
m.audioTrack,
|
||||
|
||||
Reference in New Issue
Block a user