33 changed files with 1911 additions and 1336 deletions
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
package conf |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
) |
||||
|
||||
// RecordFormat is the recordFormat parameter.
|
||||
type RecordFormat int |
||||
|
||||
// supported values.
|
||||
const ( |
||||
RecordFormatFMP4 RecordFormat = iota |
||||
RecordFormatMPEGTS |
||||
) |
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (d RecordFormat) MarshalJSON() ([]byte, error) { |
||||
var out string |
||||
|
||||
switch d { |
||||
case RecordFormatMPEGTS: |
||||
out = "mpegts" |
||||
|
||||
default: |
||||
out = "fmp4" |
||||
} |
||||
|
||||
return json.Marshal(out) |
||||
} |
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (d *RecordFormat) UnmarshalJSON(b []byte) error { |
||||
var in string |
||||
if err := json.Unmarshal(b, &in); err != nil { |
||||
return err |
||||
} |
||||
|
||||
switch in { |
||||
case "mpegts": |
||||
*d = RecordFormatMPEGTS |
||||
|
||||
case "fmp4": |
||||
*d = RecordFormatFMP4 |
||||
|
||||
default: |
||||
return fmt.Errorf("invalid record format '%s'", in) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// UnmarshalEnv implements env.Unmarshaler.
|
||||
func (d *RecordFormat) UnmarshalEnv(_ string, v string) error { |
||||
return d.UnmarshalJSON([]byte(`"` + v + `"`)) |
||||
} |
@ -1,7 +1,16 @@
@@ -1,7 +1,16 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"github.com/bluenviron/mediamtx/internal/asyncwriter" |
||||
"github.com/bluenviron/mediamtx/internal/stream" |
||||
) |
||||
|
||||
// reader is an entity that can read a stream.
|
||||
type reader interface { |
||||
close() |
||||
apiReaderDescribe() apiPathSourceOrReader |
||||
} |
||||
|
||||
func readerMediaInfo(r *asyncwriter.Writer, stream *stream.Stream) string { |
||||
return mediaInfo(stream.MediasForReader(r)) |
||||
} |
||||
|
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
package record |
||||
|
||||
type recFormat interface { |
||||
close() |
||||
} |
@ -0,0 +1,821 @@
@@ -0,0 +1,821 @@
|
||||
package record |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/format" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/ac3" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/av1" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/h264" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/h265" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/jpeg" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg1audio" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4video" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/opus" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/vp9" |
||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4" |
||||
|
||||
"github.com/bluenviron/mediamtx/internal/logger" |
||||
"github.com/bluenviron/mediamtx/internal/unit" |
||||
) |
||||
|
||||
func durationGoToMp4(v time.Duration, timeScale uint32) uint64 { |
||||
timeScale64 := uint64(timeScale) |
||||
secs := v / time.Second |
||||
dec := v % time.Second |
||||
return uint64(secs)*timeScale64 + uint64(dec)*timeScale64/uint64(time.Second) |
||||
} |
||||
|
||||
func mpeg1audioChannelCount(cm mpeg1audio.ChannelMode) int { |
||||
switch cm { |
||||
case mpeg1audio.ChannelModeStereo, |
||||
mpeg1audio.ChannelModeJointStereo, |
||||
mpeg1audio.ChannelModeDualChannel: |
||||
return 2 |
||||
|
||||
default: |
||||
return 1 |
||||
} |
||||
} |
||||
|
||||
func jpegExtractSize(image []byte) (int, int, error) { |
||||
l := len(image) |
||||
if l < 2 || image[0] != 0xFF || image[1] != jpeg.MarkerStartOfImage { |
||||
return 0, 0, fmt.Errorf("invalid header") |
||||
} |
||||
|
||||
image = image[2:] |
||||
|
||||
for { |
||||
if len(image) < 2 { |
||||
return 0, 0, fmt.Errorf("not enough bits") |
||||
} |
||||
|
||||
h0, h1 := image[0], image[1] |
||||
image = image[2:] |
||||
|
||||
if h0 != 0xFF { |
||||
return 0, 0, fmt.Errorf("invalid image") |
||||
} |
||||
|
||||
switch h1 { |
||||
case 0xE0, 0xE1, 0xE2, // JFIF
|
||||
jpeg.MarkerDefineHuffmanTable, |
||||
jpeg.MarkerComment, |
||||
jpeg.MarkerDefineQuantizationTable, |
||||
jpeg.MarkerDefineRestartInterval: |
||||
mlen := int(image[0])<<8 | int(image[1]) |
||||
if len(image) < mlen { |
||||
return 0, 0, fmt.Errorf("not enough bits") |
||||
} |
||||
image = image[mlen:] |
||||
|
||||
case jpeg.MarkerStartOfFrame1: |
||||
mlen := int(image[0])<<8 | int(image[1]) |
||||
if len(image) < mlen { |
||||
return 0, 0, fmt.Errorf("not enough bits") |
||||
} |
||||
|
||||
var sof jpeg.StartOfFrame1 |
||||
err := sof.Unmarshal(image[2:mlen]) |
||||
if err != nil { |
||||
return 0, 0, err |
||||
} |
||||
|
||||
return sof.Width, sof.Height, nil |
||||
|
||||
case jpeg.MarkerStartOfScan: |
||||
return 0, 0, fmt.Errorf("SOF not found") |
||||
|
||||
default: |
||||
return 0, 0, fmt.Errorf("unknown marker: 0x%.2x", h1) |
||||
} |
||||
} |
||||
} |
||||
|
||||
type recFormatFMP4 struct { |
||||
a *Agent |
||||
tracks []*recFormatFMP4Track |
||||
hasVideo bool |
||||
currentSegment *recFormatFMP4Segment |
||||
nextSequenceNumber uint32 |
||||
} |
||||
|
||||
func newRecFormatFMP4(a *Agent) recFormat { |
||||
f := &recFormatFMP4{ |
||||
a: a, |
||||
} |
||||
|
||||
nextID := 1 |
||||
|
||||
addTrack := func(codec fmp4.Codec) *recFormatFMP4Track { |
||||
initTrack := &fmp4.InitTrack{ |
||||
TimeScale: 90000, |
||||
Codec: codec, |
||||
} |
||||
initTrack.ID = nextID |
||||
nextID++ |
||||
|
||||
track := newRecFormatFMP4Track(f, initTrack) |
||||
f.tracks = append(f.tracks, track) |
||||
|
||||
return track |
||||
} |
||||
|
||||
updateCodecs := func() { |
||||
// if codec parameters have been updated,
|
||||
// and current segment has already written codec parameters on disk,
|
||||
// close current segment.
|
||||
if f.currentSegment != nil && f.currentSegment.fi != nil { |
||||
f.currentSegment.close() //nolint:errcheck
|
||||
f.currentSegment = nil |
||||
} |
||||
} |
||||
|
||||
for _, media := range a.stream.Desc().Medias { |
||||
for _, forma := range media.Formats { |
||||
switch forma := forma.(type) { |
||||
case *format.AV1: |
||||
codec := &fmp4.CodecAV1{ |
||||
SequenceHeader: []byte{ |
||||
8, 0, 0, 0, 66, 167, 191, 228, 96, 13, 0, 64, |
||||
}, |
||||
} |
||||
track := addTrack(codec) |
||||
|
||||
firstReceived := false |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.AV1) |
||||
if tunit.TU == nil { |
||||
return nil |
||||
} |
||||
|
||||
randomAccess := false |
||||
|
||||
for _, obu := range tunit.TU { |
||||
var h av1.OBUHeader |
||||
err := h.Unmarshal(obu) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if h.Type == av1.OBUTypeSequenceHeader { |
||||
if !bytes.Equal(codec.SequenceHeader, obu) { |
||||
codec.SequenceHeader = obu |
||||
updateCodecs() |
||||
} |
||||
randomAccess = true |
||||
} |
||||
} |
||||
|
||||
if !firstReceived { |
||||
if !randomAccess { |
||||
return nil |
||||
} |
||||
firstReceived = true |
||||
} |
||||
|
||||
sampl, err := fmp4.NewPartSampleAV1( |
||||
randomAccess, |
||||
tunit.TU) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return track.record(&sample{ |
||||
PartSample: sampl, |
||||
dts: tunit.PTS, |
||||
}) |
||||
}) |
||||
|
||||
case *format.VP9: |
||||
codec := &fmp4.CodecVP9{ |
||||
Width: 1280, |
||||
Height: 720, |
||||
Profile: 1, |
||||
BitDepth: 8, |
||||
ChromaSubsampling: 1, |
||||
ColorRange: false, |
||||
} |
||||
track := addTrack(codec) |
||||
|
||||
firstReceived := false |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.VP9) |
||||
if tunit.Frame == nil { |
||||
return nil |
||||
} |
||||
|
||||
var h vp9.Header |
||||
err := h.Unmarshal(tunit.Frame) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
randomAccess := false |
||||
|
||||
if h.FrameType == vp9.FrameTypeKeyFrame { |
||||
randomAccess = true |
||||
|
||||
if w := h.Width(); codec.Width != w { |
||||
codec.Width = w |
||||
updateCodecs() |
||||
} |
||||
if h := h.Width(); codec.Height != h { |
||||
codec.Height = h |
||||
updateCodecs() |
||||
} |
||||
if codec.Profile != h.Profile { |
||||
codec.Profile = h.Profile |
||||
updateCodecs() |
||||
} |
||||
if codec.BitDepth != h.ColorConfig.BitDepth { |
||||
codec.BitDepth = h.ColorConfig.BitDepth |
||||
updateCodecs() |
||||
} |
||||
if c := h.ChromaSubsampling(); codec.ChromaSubsampling != c { |
||||
codec.ChromaSubsampling = c |
||||
updateCodecs() |
||||
} |
||||
if codec.ColorRange != h.ColorConfig.ColorRange { |
||||
codec.ColorRange = h.ColorConfig.ColorRange |
||||
updateCodecs() |
||||
} |
||||
} |
||||
|
||||
if !firstReceived { |
||||
if !randomAccess { |
||||
return nil |
||||
} |
||||
firstReceived = true |
||||
} |
||||
|
||||
return track.record(&sample{ |
||||
PartSample: &fmp4.PartSample{ |
||||
IsNonSyncSample: !randomAccess, |
||||
Payload: tunit.Frame, |
||||
}, |
||||
dts: tunit.PTS, |
||||
}) |
||||
}) |
||||
|
||||
case *format.VP8: |
||||
// TODO
|
||||
|
||||
case *format.H265: |
||||
vps, sps, pps := forma.SafeParams() |
||||
|
||||
if vps == nil || sps == nil || pps == nil { |
||||
vps = []byte{ |
||||
0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20, |
||||
0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03, |
||||
0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24, |
||||
} |
||||
|
||||
sps = []byte{ |
||||
0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, |
||||
0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, |
||||
0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d, |
||||
0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88, |
||||
0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9, |
||||
0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc, |
||||
0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a, |
||||
0x02, 0x02, 0x02, 0x01, |
||||
} |
||||
|
||||
pps = []byte{ |
||||
0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40, |
||||
} |
||||
} |
||||
|
||||
codec := &fmp4.CodecH265{ |
||||
VPS: vps, |
||||
SPS: sps, |
||||
PPS: pps, |
||||
} |
||||
track := addTrack(codec) |
||||
|
||||
var dtsExtractor *h265.DTSExtractor |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.H265) |
||||
if tunit.AU == nil { |
||||
return nil |
||||
} |
||||
|
||||
randomAccess := false |
||||
|
||||
for _, nalu := range tunit.AU { |
||||
typ := h265.NALUType((nalu[0] >> 1) & 0b111111) |
||||
|
||||
switch typ { |
||||
case h265.NALUType_VPS_NUT: |
||||
if !bytes.Equal(codec.VPS, nalu) { |
||||
codec.VPS = nalu |
||||
updateCodecs() |
||||
} |
||||
|
||||
case h265.NALUType_SPS_NUT: |
||||
if !bytes.Equal(codec.SPS, nalu) { |
||||
codec.SPS = nalu |
||||
updateCodecs() |
||||
} |
||||
|
||||
case h265.NALUType_PPS_NUT: |
||||
if !bytes.Equal(codec.PPS, nalu) { |
||||
codec.PPS = nalu |
||||
updateCodecs() |
||||
} |
||||
|
||||
case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT: |
||||
randomAccess = true |
||||
} |
||||
} |
||||
|
||||
if dtsExtractor == nil { |
||||
if !randomAccess { |
||||
return nil |
||||
} |
||||
dtsExtractor = h265.NewDTSExtractor() |
||||
} |
||||
|
||||
dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
sampl, err := fmp4.NewPartSampleH26x( |
||||
int32(durationGoToMp4(tunit.PTS-dts, 90000)), |
||||
randomAccess, |
||||
tunit.AU) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return track.record(&sample{ |
||||
PartSample: sampl, |
||||
dts: dts, |
||||
}) |
||||
}) |
||||
|
||||
case *format.H264: |
||||
sps, pps := forma.SafeParams() |
||||
|
||||
if sps == nil || pps == nil { |
||||
sps = []byte{ |
||||
0x67, 0x42, 0xc0, 0x1f, 0xd9, 0x00, 0xf0, 0x11, |
||||
0x7e, 0xf0, 0x11, 0x00, 0x00, 0x03, 0x00, 0x01, |
||||
0x00, 0x00, 0x03, 0x00, 0x30, 0x8f, 0x18, 0x32, |
||||
0x48, |
||||
} |
||||
|
||||
pps = []byte{ |
||||
0x68, 0xcb, 0x8c, 0xb2, |
||||
} |
||||
} |
||||
|
||||
codec := &fmp4.CodecH264{ |
||||
SPS: sps, |
||||
PPS: pps, |
||||
} |
||||
track := addTrack(codec) |
||||
|
||||
var dtsExtractor *h264.DTSExtractor |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.H264) |
||||
if tunit.AU == nil { |
||||
return nil |
||||
} |
||||
|
||||
randomAccess := false |
||||
|
||||
for _, nalu := range tunit.AU { |
||||
typ := h264.NALUType(nalu[0] & 0x1F) |
||||
switch typ { |
||||
case h264.NALUTypeSPS: |
||||
if !bytes.Equal(codec.SPS, nalu) { |
||||
codec.SPS = nalu |
||||
updateCodecs() |
||||
} |
||||
|
||||
case h264.NALUTypePPS: |
||||
if !bytes.Equal(codec.PPS, nalu) { |
||||
codec.PPS = nalu |
||||
updateCodecs() |
||||
} |
||||
|
||||
case h264.NALUTypeIDR: |
||||
randomAccess = true |
||||
} |
||||
} |
||||
|
||||
if dtsExtractor == nil { |
||||
if !randomAccess { |
||||
return nil |
||||
} |
||||
dtsExtractor = h264.NewDTSExtractor() |
||||
} |
||||
|
||||
dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
sampl, err := fmp4.NewPartSampleH26x( |
||||
int32(durationGoToMp4(tunit.PTS-dts, 90000)), |
||||
randomAccess, |
||||
tunit.AU) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return track.record(&sample{ |
||||
PartSample: sampl, |
||||
dts: dts, |
||||
}) |
||||
}) |
||||
|
||||
case *format.MPEG4Video: |
||||
config := forma.SafeParams() |
||||
|
||||
if config == nil { |
||||
config = []byte{ |
||||
0x00, 0x00, 0x01, 0xb0, 0x01, 0x00, 0x00, 0x01, |
||||
0xb5, 0x89, 0x13, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x01, 0x20, 0x00, 0xc4, 0x8d, 0x88, 0x00, |
||||
0xf5, 0x3c, 0x04, 0x87, 0x14, 0x63, 0x00, 0x00, |
||||
0x01, 0xb2, 0x4c, 0x61, 0x76, 0x63, 0x35, 0x38, |
||||
0x2e, 0x31, 0x33, 0x34, 0x2e, 0x31, 0x30, 0x30, |
||||
} |
||||
} |
||||
|
||||
codec := &fmp4.CodecMPEG4Video{ |
||||
Config: config, |
||||
} |
||||
track := addTrack(codec) |
||||
|
||||
firstReceived := false |
||||
var lastPTS time.Duration |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.MPEG4Video) |
||||
if tunit.Frame == nil { |
||||
return nil |
||||
} |
||||
|
||||
randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)}) |
||||
|
||||
if bytes.HasPrefix(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.VisualObjectSequenceStartCode)}) { |
||||
end := bytes.Index(tunit.Frame[4:], []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)}) |
||||
if end >= 0 { |
||||
config := tunit.Frame[:end+4] |
||||
|
||||
if !bytes.Equal(codec.Config, config) { |
||||
codec.Config = config |
||||
updateCodecs() |
||||
} |
||||
} |
||||
} |
||||
|
||||
if !firstReceived { |
||||
if !randomAccess { |
||||
return nil |
||||
} |
||||
firstReceived = true |
||||
} else if tunit.PTS < lastPTS { |
||||
return fmt.Errorf("MPEG-4 Video streams with B-frames are not supported (yet)") |
||||
} |
||||
lastPTS = tunit.PTS |
||||
|
||||
return track.record(&sample{ |
||||
PartSample: &fmp4.PartSample{ |
||||
Payload: tunit.Frame, |
||||
IsNonSyncSample: !randomAccess, |
||||
}, |
||||
dts: tunit.PTS, |
||||
}) |
||||
}) |
||||
|
||||
case *format.MPEG1Video: |
||||
codec := &fmp4.CodecMPEG1Video{ |
||||
Config: []byte{ |
||||
0x00, 0x00, 0x01, 0xb3, 0x78, 0x04, 0x38, 0x35, |
||||
0xff, 0xff, 0xe0, 0x18, 0x00, 0x00, 0x01, 0xb5, |
||||
0x14, 0x4a, 0x00, 0x01, 0x00, 0x00, |
||||
}, |
||||
} |
||||
track := addTrack(codec) |
||||
|
||||
firstReceived := false |
||||
var lastPTS time.Duration |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.MPEG1Video) |
||||
if tunit.Frame == nil { |
||||
return nil |
||||
} |
||||
|
||||
randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, 0xB8}) |
||||
|
||||
if bytes.HasPrefix(tunit.Frame, []byte{0, 0, 1, 0xB3}) { |
||||
end := bytes.Index(tunit.Frame[4:], []byte{0, 0, 1, 0xB8}) |
||||
if end >= 0 { |
||||
config := tunit.Frame[:end+4] |
||||
|
||||
if !bytes.Equal(codec.Config, config) { |
||||
codec.Config = config |
||||
updateCodecs() |
||||
} |
||||
} |
||||
} |
||||
|
||||
if !firstReceived { |
||||
if !randomAccess { |
||||
return nil |
||||
} |
||||
firstReceived = true |
||||
} else if tunit.PTS < lastPTS { |
||||
return fmt.Errorf("MPEG-1 Video streams with B-frames are not supported (yet)") |
||||
} |
||||
lastPTS = tunit.PTS |
||||
|
||||
return track.record(&sample{ |
||||
PartSample: &fmp4.PartSample{ |
||||
Payload: tunit.Frame, |
||||
IsNonSyncSample: !randomAccess, |
||||
}, |
||||
dts: tunit.PTS, |
||||
}) |
||||
}) |
||||
|
||||
case *format.MJPEG: |
||||
codec := &fmp4.CodecMJPEG{ |
||||
Width: 800, |
||||
Height: 600, |
||||
} |
||||
track := addTrack(codec) |
||||
|
||||
parsed := false |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.MJPEG) |
||||
if tunit.Frame == nil { |
||||
return nil |
||||
} |
||||
|
||||
if !parsed { |
||||
parsed = true |
||||
width, height, err := jpegExtractSize(tunit.Frame) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
codec.Width = width |
||||
codec.Height = height |
||||
updateCodecs() |
||||
} |
||||
|
||||
return track.record(&sample{ |
||||
PartSample: &fmp4.PartSample{ |
||||
Payload: tunit.Frame, |
||||
}, |
||||
dts: tunit.PTS, |
||||
}) |
||||
}) |
||||
|
||||
case *format.Opus: |
||||
codec := &fmp4.CodecOpus{ |
||||
ChannelCount: func() int { |
||||
if forma.IsStereo { |
||||
return 2 |
||||
} |
||||
return 1 |
||||
}(), |
||||
} |
||||
track := addTrack(codec) |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.Opus) |
||||
if tunit.Packets == nil { |
||||
return nil |
||||
} |
||||
|
||||
pts := tunit.PTS |
||||
|
||||
for _, packet := range tunit.Packets { |
||||
err := track.record(&sample{ |
||||
PartSample: &fmp4.PartSample{ |
||||
Payload: packet, |
||||
}, |
||||
dts: pts, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
pts += opus.PacketDuration(packet) |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
|
||||
case *format.MPEG4Audio: |
||||
codec := &fmp4.CodecMPEG4Audio{ |
||||
Config: *forma.GetConfig(), |
||||
} |
||||
track := addTrack(codec) |
||||
|
||||
sampleRate := time.Duration(forma.ClockRate()) |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.MPEG4Audio) |
||||
if tunit.AUs == nil { |
||||
return nil |
||||
} |
||||
|
||||
for i, au := range tunit.AUs { |
||||
auPTS := tunit.PTS + time.Duration(i)*mpeg4audio.SamplesPerAccessUnit* |
||||
time.Second/sampleRate |
||||
|
||||
err := track.record(&sample{ |
||||
PartSample: &fmp4.PartSample{ |
||||
Payload: au, |
||||
}, |
||||
dts: auPTS, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
|
||||
case *format.MPEG1Audio: |
||||
codec := &fmp4.CodecMPEG1Audio{ |
||||
SampleRate: 32000, |
||||
ChannelCount: 2, |
||||
} |
||||
track := addTrack(codec) |
||||
|
||||
parsed := false |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.MPEG1Audio) |
||||
if tunit.Frames == nil { |
||||
return nil |
||||
} |
||||
|
||||
pts := tunit.PTS |
||||
|
||||
for _, frame := range tunit.Frames { |
||||
var h mpeg1audio.FrameHeader |
||||
err := h.Unmarshal(frame) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if !parsed { |
||||
parsed = true |
||||
codec.SampleRate = h.SampleRate |
||||
codec.ChannelCount = mpeg1audioChannelCount(h.ChannelMode) |
||||
updateCodecs() |
||||
} |
||||
|
||||
err = track.record(&sample{ |
||||
PartSample: &fmp4.PartSample{ |
||||
Payload: frame, |
||||
}, |
||||
dts: pts, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
pts += time.Duration(h.SampleCount()) * |
||||
time.Second / time.Duration(h.SampleRate) |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
|
||||
case *format.AC3: |
||||
codec := &fmp4.CodecAC3{ |
||||
SampleRate: forma.SampleRate, |
||||
ChannelCount: forma.ChannelCount, |
||||
Fscod: 0, |
||||
Bsid: 8, |
||||
Bsmod: 0, |
||||
Acmod: 7, |
||||
LfeOn: true, |
||||
BitRateCode: 7, |
||||
} |
||||
track := addTrack(codec) |
||||
|
||||
parsed := false |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.AC3) |
||||
if tunit.Frames == nil { |
||||
return nil |
||||
} |
||||
|
||||
pts := tunit.PTS |
||||
|
||||
for _, frame := range tunit.Frames { |
||||
var syncInfo ac3.SyncInfo |
||||
err := syncInfo.Unmarshal(frame) |
||||
if err != nil { |
||||
return fmt.Errorf("invalid AC-3 frame: %s", err) |
||||
} |
||||
|
||||
var bsi ac3.BSI |
||||
err = bsi.Unmarshal(frame[5:]) |
||||
if err != nil { |
||||
return fmt.Errorf("invalid AC-3 frame: %s", err) |
||||
} |
||||
|
||||
if !parsed { |
||||
parsed = true |
||||
codec.SampleRate = syncInfo.SampleRate() |
||||
codec.ChannelCount = bsi.ChannelCount() |
||||
codec.Fscod = syncInfo.Fscod |
||||
codec.Bsid = bsi.Bsid |
||||
codec.Bsmod = bsi.Bsmod |
||||
codec.Acmod = bsi.Acmod |
||||
codec.LfeOn = bsi.LfeOn |
||||
codec.BitRateCode = syncInfo.Frmsizecod >> 1 |
||||
updateCodecs() |
||||
} |
||||
|
||||
err = track.record(&sample{ |
||||
PartSample: &fmp4.PartSample{ |
||||
Payload: frame, |
||||
}, |
||||
dts: pts, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
pts += time.Duration(ac3.SamplesPerFrame) * |
||||
time.Second / time.Duration(codec.SampleRate) |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
|
||||
case *format.G722: |
||||
// TODO
|
||||
|
||||
case *format.G711: |
||||
// TODO
|
||||
|
||||
case *format.LPCM: |
||||
codec := &fmp4.CodecLPCM{ |
||||
LittleEndian: false, |
||||
BitDepth: forma.BitDepth, |
||||
SampleRate: forma.SampleRate, |
||||
ChannelCount: forma.ChannelCount, |
||||
} |
||||
track := addTrack(codec) |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.LPCM) |
||||
if tunit.Samples == nil { |
||||
return nil |
||||
} |
||||
|
||||
return track.record(&sample{ |
||||
PartSample: &fmp4.PartSample{ |
||||
Payload: tunit.Samples, |
||||
}, |
||||
dts: tunit.PTS, |
||||
}) |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
|
||||
a.Log(logger.Info, "recording %d %s", |
||||
len(f.tracks), |
||||
func() string { |
||||
if len(f.tracks) == 1 { |
||||
return "track" |
||||
} |
||||
return "tracks" |
||||
}()) |
||||
|
||||
return f |
||||
} |
||||
|
||||
func (f *recFormatFMP4) close() { |
||||
if f.currentSegment != nil { |
||||
f.currentSegment.close() //nolint:errcheck
|
||||
} |
||||
} |
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
package record |
||||
|
||||
import ( |
||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4" |
||||
) |
||||
|
||||
type recFormatFMP4Track struct { |
||||
f *recFormatFMP4 |
||||
initTrack *fmp4.InitTrack |
||||
|
||||
nextSample *sample |
||||
} |
||||
|
||||
func newRecFormatFMP4Track( |
||||
f *recFormatFMP4, |
||||
initTrack *fmp4.InitTrack, |
||||
) *recFormatFMP4Track { |
||||
return &recFormatFMP4Track{ |
||||
f: f, |
||||
initTrack: initTrack, |
||||
} |
||||
} |
||||
|
||||
func (t *recFormatFMP4Track) record(sample *sample) error { |
||||
// wait the first video sample before setting hasVideo
|
||||
if t.initTrack.Codec.IsVideo() { |
||||
t.f.hasVideo = true |
||||
} |
||||
|
||||
if t.f.currentSegment == nil { |
||||
t.f.currentSegment = newRecFormatFMP4Segment(t.f, sample.dts) |
||||
} |
||||
|
||||
sample, t.nextSample = t.nextSample, sample |
||||
if sample == nil { |
||||
return nil |
||||
} |
||||
sample.Duration = uint32(durationGoToMp4(t.nextSample.dts-sample.dts, t.initTrack.TimeScale)) |
||||
|
||||
err := t.f.currentSegment.record(t, sample) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if (!t.f.hasVideo || t.initTrack.Codec.IsVideo()) && |
||||
!t.nextSample.IsNonSyncSample && |
||||
(t.nextSample.dts-t.f.currentSegment.startDTS) >= t.f.a.segmentDuration { |
||||
err := t.f.currentSegment.close() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
t.f.currentSegment = newRecFormatFMP4Segment(t.f, t.nextSample.dts) |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,332 @@
@@ -0,0 +1,332 @@
|
||||
package record |
||||
|
||||
import ( |
||||
"bufio" |
||||
"bytes" |
||||
"fmt" |
||||
"io" |
||||
"time" |
||||
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/format" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/ac3" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/h264" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/h265" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4video" |
||||
"github.com/bluenviron/mediacommon/pkg/formats/mpegts" |
||||
|
||||
"github.com/bluenviron/mediamtx/internal/logger" |
||||
"github.com/bluenviron/mediamtx/internal/unit" |
||||
) |
||||
|
||||
const ( |
||||
mpegtsMaxBufferSize = 64 * 1024 |
||||
) |
||||
|
||||
func durationGoToMPEGTS(v time.Duration) int64 { |
||||
return int64(v.Seconds() * 90000) |
||||
} |
||||
|
||||
type dynamicWriter struct { |
||||
w io.Writer |
||||
} |
||||
|
||||
func (d *dynamicWriter) Write(p []byte) (int, error) { |
||||
return d.w.Write(p) |
||||
} |
||||
|
||||
func (d *dynamicWriter) setTarget(w io.Writer) { |
||||
d.w = w |
||||
} |
||||
|
||||
type recFormatMPEGTS struct { |
||||
a *Agent |
||||
|
||||
dw *dynamicWriter |
||||
bw *bufio.Writer |
||||
mw *mpegts.Writer |
||||
hasVideo bool |
||||
currentSegment *recFormatMPEGTSSegment |
||||
} |
||||
|
||||
func newRecFormatMPEGTS(a *Agent) recFormat { |
||||
f := &recFormatMPEGTS{ |
||||
a: a, |
||||
} |
||||
|
||||
var tracks []*mpegts.Track |
||||
|
||||
addTrack := func(codec mpegts.Codec) *mpegts.Track { |
||||
track := &mpegts.Track{ |
||||
Codec: codec, |
||||
} |
||||
tracks = append(tracks, track) |
||||
return track |
||||
} |
||||
|
||||
for _, media := range a.stream.Desc().Medias { |
||||
for _, forma := range media.Formats { |
||||
switch forma := forma.(type) { |
||||
case *format.H265: |
||||
track := addTrack(&mpegts.CodecH265{}) |
||||
|
||||
var dtsExtractor *h265.DTSExtractor |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.H265) |
||||
if tunit.AU == nil { |
||||
return nil |
||||
} |
||||
|
||||
randomAccess := h265.IsRandomAccess(tunit.AU) |
||||
|
||||
if dtsExtractor == nil { |
||||
if !randomAccess { |
||||
return nil |
||||
} |
||||
dtsExtractor = h265.NewDTSExtractor() |
||||
} |
||||
|
||||
dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return f.recordH26x(track, dts, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU) |
||||
}) |
||||
|
||||
case *format.H264: |
||||
track := addTrack(&mpegts.CodecH264{}) |
||||
|
||||
var dtsExtractor *h264.DTSExtractor |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.H264) |
||||
if tunit.AU == nil { |
||||
return nil |
||||
} |
||||
|
||||
idrPresent := h264.IDRPresent(tunit.AU) |
||||
|
||||
if dtsExtractor == nil { |
||||
if !idrPresent { |
||||
return nil |
||||
} |
||||
dtsExtractor = h264.NewDTSExtractor() |
||||
} |
||||
|
||||
dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return f.recordH26x(track, dts, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), idrPresent, tunit.AU) |
||||
}) |
||||
|
||||
case *format.MPEG4Video: |
||||
track := addTrack(&mpegts.CodecMPEG4Video{}) |
||||
|
||||
firstReceived := false |
||||
var lastPTS time.Duration |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.MPEG4Video) |
||||
if tunit.Frame == nil { |
||||
return nil |
||||
} |
||||
|
||||
if !firstReceived { |
||||
firstReceived = true |
||||
} else if tunit.PTS < lastPTS { |
||||
return fmt.Errorf("MPEG-4 Video streams with B-frames are not supported (yet)") |
||||
} |
||||
lastPTS = tunit.PTS |
||||
|
||||
f.hasVideo = true |
||||
randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)}) |
||||
|
||||
err := f.setupSegment(tunit.PTS, true, randomAccess) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return f.mw.WriteMPEG4Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame) |
||||
}) |
||||
|
||||
case *format.MPEG1Video: |
||||
track := addTrack(&mpegts.CodecMPEG1Video{}) |
||||
|
||||
firstReceived := false |
||||
var lastPTS time.Duration |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.MPEG1Video) |
||||
if tunit.Frame == nil { |
||||
return nil |
||||
} |
||||
|
||||
if !firstReceived { |
||||
firstReceived = true |
||||
} else if tunit.PTS < lastPTS { |
||||
return fmt.Errorf("MPEG-1 Video streams with B-frames are not supported (yet)") |
||||
} |
||||
lastPTS = tunit.PTS |
||||
|
||||
f.hasVideo = true |
||||
randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, 0xB8}) |
||||
|
||||
err := f.setupSegment(tunit.PTS, true, randomAccess) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return f.mw.WriteMPEG1Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame) |
||||
}) |
||||
|
||||
case *format.Opus: |
||||
track := addTrack(&mpegts.CodecOpus{ |
||||
ChannelCount: func() int { |
||||
if forma.IsStereo { |
||||
return 2 |
||||
} |
||||
return 1 |
||||
}(), |
||||
}) |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.Opus) |
||||
if tunit.Packets == nil { |
||||
return nil |
||||
} |
||||
|
||||
err := f.setupSegment(tunit.PTS, false, true) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return f.mw.WriteOpus(track, durationGoToMPEGTS(tunit.PTS), tunit.Packets) |
||||
}) |
||||
|
||||
case *format.MPEG4Audio: |
||||
track := addTrack(&mpegts.CodecMPEG4Audio{ |
||||
Config: *forma.GetConfig(), |
||||
}) |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.MPEG4Audio) |
||||
if tunit.AUs == nil { |
||||
return nil |
||||
} |
||||
|
||||
err := f.setupSegment(tunit.PTS, false, true) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return f.mw.WriteMPEG4Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.AUs) |
||||
}) |
||||
|
||||
case *format.MPEG1Audio: |
||||
track := addTrack(&mpegts.CodecMPEG1Audio{}) |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.MPEG1Audio) |
||||
if tunit.Frames == nil { |
||||
return nil |
||||
} |
||||
|
||||
err := f.setupSegment(tunit.PTS, false, true) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return f.mw.WriteMPEG1Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.Frames) |
||||
}) |
||||
|
||||
case *format.AC3: |
||||
track := addTrack(&mpegts.CodecAC3{}) |
||||
|
||||
sampleRate := time.Duration(forma.SampleRate) |
||||
|
||||
a.stream.AddReader(a.writer, media, forma, func(u unit.Unit) error { |
||||
tunit := u.(*unit.AC3) |
||||
if tunit.Frames == nil { |
||||
return nil |
||||
} |
||||
|
||||
for i, frame := range tunit.Frames { |
||||
framePTS := tunit.PTS + time.Duration(i)*ac3.SamplesPerFrame* |
||||
time.Second/sampleRate |
||||
|
||||
err := f.mw.WriteAC3(track, durationGoToMPEGTS(framePTS), frame) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
|
||||
f.dw = &dynamicWriter{} |
||||
f.bw = bufio.NewWriterSize(f.dw, mpegtsMaxBufferSize) |
||||
f.mw = mpegts.NewWriter(f.bw, tracks) |
||||
|
||||
a.Log(logger.Info, "recording %d %s", |
||||
len(tracks), |
||||
func() string { |
||||
if len(tracks) == 1 { |
||||
return "track" |
||||
} |
||||
return "tracks" |
||||
}()) |
||||
|
||||
return f |
||||
} |
||||
|
||||
func (f *recFormatMPEGTS) close() { |
||||
if f.currentSegment != nil { |
||||
f.currentSegment.close() //nolint:errcheck
|
||||
} |
||||
} |
||||
|
||||
func (f *recFormatMPEGTS) setupSegment(dts time.Duration, isVideo bool, randomAccess bool) error { |
||||
switch { |
||||
case f.currentSegment == nil: |
||||
f.currentSegment = newRecFormatMPEGTSSegment(f, dts) |
||||
|
||||
case (!f.hasVideo || isVideo) && |
||||
randomAccess && |
||||
(dts-f.currentSegment.startDTS) >= f.a.segmentDuration: |
||||
err := f.currentSegment.close() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
f.currentSegment = newRecFormatMPEGTSSegment(f, dts) |
||||
|
||||
case (dts - f.currentSegment.lastFlush) >= f.a.partDuration: |
||||
err := f.bw.Flush() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
f.currentSegment.lastFlush = dts |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (f *recFormatMPEGTS) recordH26x(track *mpegts.Track, goDTS time.Duration, |
||||
pts int64, dts int64, randomAccess bool, au [][]byte, |
||||
) error { |
||||
f.hasVideo = true |
||||
|
||||
err := f.setupSegment(goDTS, true, randomAccess) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return f.mw.WriteH26x(track, pts, dts, randomAccess, au) |
||||
} |
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
package record |
||||
|
||||
import ( |
||||
"os" |
||||
"path/filepath" |
||||
"time" |
||||
|
||||
"github.com/bluenviron/mediamtx/internal/logger" |
||||
) |
||||
|
||||
type recFormatMPEGTSSegment struct { |
||||
f *recFormatMPEGTS |
||||
startDTS time.Duration |
||||
lastFlush time.Duration |
||||
|
||||
created time.Time |
||||
fpath string |
||||
fi *os.File |
||||
} |
||||
|
||||
func newRecFormatMPEGTSSegment(f *recFormatMPEGTS, startDTS time.Duration) *recFormatMPEGTSSegment { |
||||
s := &recFormatMPEGTSSegment{ |
||||
f: f, |
||||
startDTS: startDTS, |
||||
lastFlush: startDTS, |
||||
created: timeNow(), |
||||
} |
||||
|
||||
f.dw.setTarget(s) |
||||
|
||||
return s |
||||
} |
||||
|
||||
func (s *recFormatMPEGTSSegment) close() error { |
||||
err := s.f.bw.Flush() |
||||
|
||||
if s.fi != nil { |
||||
s.f.a.Log(logger.Debug, "closing segment %s", s.fpath) |
||||
err2 := s.fi.Close() |
||||
if err == nil { |
||||
err = err2 |
||||
} |
||||
|
||||
if err2 == nil { |
||||
s.f.a.onSegmentComplete(s.fpath) |
||||
} |
||||
} |
||||
|
||||
return err |
||||
} |
||||
|
||||
func (s *recFormatMPEGTSSegment) Write(p []byte) (int, error) { |
||||
if s.fi == nil { |
||||
s.fpath = encodeRecordPath(&recordPathParams{time: s.created}, s.f.a.path) |
||||
s.f.a.Log(logger.Debug, "creating segment %s", s.fpath) |
||||
|
||||
err := os.MkdirAll(filepath.Dir(s.fpath), 0o755) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
fi, err := os.Create(s.fpath) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
s.f.a.onSegmentCreate(s.fpath) |
||||
|
||||
s.fi = fi |
||||
} |
||||
|
||||
return s.fi.Write(p) |
||||
} |
@ -1,57 +0,0 @@
@@ -1,57 +0,0 @@
|
||||
package record |
||||
|
||||
import ( |
||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4" |
||||
) |
||||
|
||||
type track struct { |
||||
r *Agent |
||||
initTrack *fmp4.InitTrack |
||||
|
||||
nextSample *sample |
||||
} |
||||
|
||||
func newTrack( |
||||
r *Agent, |
||||
initTrack *fmp4.InitTrack, |
||||
) *track { |
||||
return &track{ |
||||
r: r, |
||||
initTrack: initTrack, |
||||
} |
||||
} |
||||
|
||||
func (t *track) record(sample *sample) error { |
||||
// wait the first video sample before setting hasVideo
|
||||
if t.initTrack.Codec.IsVideo() { |
||||
t.r.hasVideo = true |
||||
} |
||||
|
||||
if t.r.currentSegment == nil { |
||||
t.r.currentSegment = newSegment(t.r, sample.dts) |
||||
} |
||||
|
||||
sample, t.nextSample = t.nextSample, sample |
||||
if sample == nil { |
||||
return nil |
||||
} |
||||
sample.Duration = uint32(durationGoToMp4(t.nextSample.dts-sample.dts, t.initTrack.TimeScale)) |
||||
|
||||
err := t.r.currentSegment.record(t, sample) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if (!t.r.hasVideo || t.initTrack.Codec.IsVideo()) && |
||||
!t.nextSample.IsNonSyncSample && |
||||
(t.nextSample.dts-t.r.currentSegment.startDTS) >= t.r.segmentDuration { |
||||
err := t.r.currentSegment.close() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
t.r.currentSegment = newSegment(t.r, t.nextSample.dts) |
||||
} |
||||
|
||||
return nil |
||||
} |
Loading…
Reference in new issue