9 changed files with 408 additions and 381 deletions
@ -0,0 +1,17 @@
@@ -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 |
||||
} |
||||
@ -0,0 +1,291 @@
@@ -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 @@
@@ -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 |
||||
} |
||||
Loading…
Reference in new issue