2024-09-08 18:33:18 +00:00
|
|
|
package recorder
|
2023-10-14 20:52:10 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
rtspformat "github.com/bluenviron/gortsplib/v4/pkg/format"
|
2023-10-14 20:52:10 +00:00
|
|
|
"github.com/bluenviron/mediacommon/pkg/codecs/ac3"
|
|
|
|
"github.com/bluenviron/mediacommon/pkg/codecs/av1"
|
2023-12-28 22:17:50 +00:00
|
|
|
"github.com/bluenviron/mediacommon/pkg/codecs/g711"
|
2023-10-14 20:52:10 +00:00
|
|
|
"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"
|
|
|
|
|
2024-01-15 11:08:14 +00:00
|
|
|
"github.com/bluenviron/mediamtx/internal/defs"
|
2024-08-01 14:42:53 +00:00
|
|
|
"github.com/bluenviron/mediamtx/internal/formatprocessor"
|
2023-10-14 20:52:10 +00:00
|
|
|
"github.com/bluenviron/mediamtx/internal/logger"
|
|
|
|
"github.com/bluenviron/mediamtx/internal/unit"
|
|
|
|
)
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
type formatFMP4 struct {
|
2024-10-04 22:49:44 +00:00
|
|
|
ai *recorderInstance
|
2023-10-26 19:40:44 +00:00
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
tracks []*formatFMP4Track
|
2023-10-14 20:52:10 +00:00
|
|
|
hasVideo bool
|
2023-12-02 14:03:37 +00:00
|
|
|
currentSegment *formatFMP4Segment
|
2023-10-14 20:52:10 +00:00
|
|
|
nextSequenceNumber uint32
|
|
|
|
}
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
func (f *formatFMP4) initialize() {
|
2023-10-14 20:52:10 +00:00
|
|
|
nextID := 1
|
2024-09-26 12:42:48 +00:00
|
|
|
var setuppedFormats []rtspformat.Format
|
|
|
|
setuppedFormatsMap := make(map[rtspformat.Format]struct{})
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-01-15 11:08:14 +00:00
|
|
|
addTrack := func(format rtspformat.Format, codec fmp4.Codec) *formatFMP4Track {
|
2023-10-14 20:52:10 +00:00
|
|
|
initTrack := &fmp4.InitTrack{
|
2024-04-08 22:16:21 +00:00
|
|
|
TimeScale: uint32(format.ClockRate()),
|
2023-10-14 20:52:10 +00:00
|
|
|
Codec: codec,
|
|
|
|
}
|
|
|
|
initTrack.ID = nextID
|
|
|
|
nextID++
|
|
|
|
|
2024-01-15 11:08:14 +00:00
|
|
|
track := &formatFMP4Track{
|
|
|
|
f: f,
|
|
|
|
initTrack: initTrack,
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-01-15 11:08:14 +00:00
|
|
|
f.tracks = append(f.tracks, track)
|
2024-09-26 12:42:48 +00:00
|
|
|
setuppedFormats = append(setuppedFormats, format)
|
|
|
|
setuppedFormatsMap[format] = struct{}{}
|
2023-10-14 20:52:10 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-01 14:34:40 +00:00
|
|
|
for _, media := range f.ai.agent.Stream.Desc().Medias {
|
2023-10-14 20:52:10 +00:00
|
|
|
for _, forma := range media.Formats {
|
2024-10-07 15:59:32 +00:00
|
|
|
clockRate := forma.ClockRate()
|
|
|
|
|
2023-10-14 20:52:10 +00:00
|
|
|
switch forma := forma.(type) {
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.AV1:
|
2023-10-14 20:52:10 +00:00
|
|
|
codec := &fmp4.CodecAV1{
|
2024-08-01 14:42:53 +00:00
|
|
|
SequenceHeader: formatprocessor.AV1DefaultSequenceHeader,
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
2024-01-15 11:08:14 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
|
|
|
firstReceived := false
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.AV1)
|
|
|
|
if tunit.TU == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
randomAccess := false
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
for _, obu := range tunit.TU {
|
|
|
|
var h av1.OBUHeader
|
|
|
|
err := h.Unmarshal(obu)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
if h.Type == av1.OBUTypeSequenceHeader {
|
|
|
|
if !bytes.Equal(codec.SequenceHeader, obu) {
|
|
|
|
codec.SequenceHeader = obu
|
|
|
|
updateCodecs()
|
|
|
|
}
|
|
|
|
randomAccess = true
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
if !firstReceived {
|
|
|
|
if !randomAccess {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
firstReceived = true
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
sampl, err := fmp4.NewPartSampleAV1(
|
|
|
|
randomAccess,
|
|
|
|
tunit.TU)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
return track.write(&sample{
|
|
|
|
PartSample: sampl,
|
|
|
|
dts: tunit.PTS,
|
|
|
|
ntp: tunit.NTP,
|
|
|
|
})
|
2023-10-14 20:52:10 +00:00
|
|
|
})
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.VP9:
|
2023-10-14 20:52:10 +00:00
|
|
|
codec := &fmp4.CodecVP9{
|
|
|
|
Width: 1280,
|
|
|
|
Height: 720,
|
|
|
|
Profile: 1,
|
|
|
|
BitDepth: 8,
|
|
|
|
ChromaSubsampling: 1,
|
|
|
|
ColorRange: false,
|
|
|
|
}
|
2024-01-15 11:08:14 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
|
|
|
firstReceived := false
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.VP9)
|
|
|
|
if tunit.Frame == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
var h vp9.Header
|
|
|
|
err := h.Unmarshal(tunit.Frame)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
randomAccess := false
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
if !h.NonKeyFrame {
|
|
|
|
randomAccess = true
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
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()
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
if !firstReceived {
|
|
|
|
if !randomAccess {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
firstReceived = true
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
return track.write(&sample{
|
|
|
|
PartSample: &fmp4.PartSample{
|
|
|
|
IsNonSyncSample: !randomAccess,
|
|
|
|
Payload: tunit.Frame,
|
|
|
|
},
|
|
|
|
dts: tunit.PTS,
|
|
|
|
ntp: tunit.NTP,
|
|
|
|
})
|
2023-10-14 20:52:10 +00:00
|
|
|
})
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.VP8:
|
2023-10-14 20:52:10 +00:00
|
|
|
// TODO
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.H265:
|
2023-10-14 20:52:10 +00:00
|
|
|
vps, sps, pps := forma.SafeParams()
|
|
|
|
|
|
|
|
if vps == nil || sps == nil || pps == nil {
|
2024-08-01 14:42:53 +00:00
|
|
|
vps = formatprocessor.H265DefaultVPS
|
|
|
|
sps = formatprocessor.H265DefaultSPS
|
|
|
|
pps = formatprocessor.H265DefaultPPS
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
codec := &fmp4.CodecH265{
|
|
|
|
VPS: vps,
|
|
|
|
SPS: sps,
|
|
|
|
PPS: pps,
|
|
|
|
}
|
2024-01-15 11:08:14 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-07 15:59:32 +00:00
|
|
|
var dtsExtractor *h265.DTSExtractor2
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.H265)
|
|
|
|
if tunit.AU == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
randomAccess := false
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
for _, nalu := range tunit.AU {
|
|
|
|
typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
switch typ {
|
|
|
|
case h265.NALUType_VPS_NUT:
|
|
|
|
if !bytes.Equal(codec.VPS, nalu) {
|
|
|
|
codec.VPS = nalu
|
|
|
|
updateCodecs()
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
case h265.NALUType_SPS_NUT:
|
|
|
|
if !bytes.Equal(codec.SPS, nalu) {
|
|
|
|
codec.SPS = nalu
|
|
|
|
updateCodecs()
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
case h265.NALUType_PPS_NUT:
|
|
|
|
if !bytes.Equal(codec.PPS, nalu) {
|
|
|
|
codec.PPS = nalu
|
|
|
|
updateCodecs()
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT:
|
|
|
|
randomAccess = true
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
if dtsExtractor == nil {
|
|
|
|
if !randomAccess {
|
|
|
|
return nil
|
|
|
|
}
|
2024-10-07 15:59:32 +00:00
|
|
|
dtsExtractor = h265.NewDTSExtractor2()
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
sampl, err := fmp4.NewPartSampleH26x(
|
2024-10-07 15:59:32 +00:00
|
|
|
int32(tunit.PTS-dts),
|
2024-10-04 22:49:44 +00:00
|
|
|
randomAccess,
|
|
|
|
tunit.AU)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
return track.write(&sample{
|
|
|
|
PartSample: sampl,
|
|
|
|
dts: dts,
|
|
|
|
ntp: tunit.NTP,
|
|
|
|
})
|
2023-10-14 20:52:10 +00:00
|
|
|
})
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.H264:
|
2023-10-14 20:52:10 +00:00
|
|
|
sps, pps := forma.SafeParams()
|
|
|
|
|
|
|
|
if sps == nil || pps == nil {
|
2024-08-01 14:42:53 +00:00
|
|
|
sps = formatprocessor.H264DefaultSPS
|
|
|
|
pps = formatprocessor.H264DefaultPPS
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
codec := &fmp4.CodecH264{
|
|
|
|
SPS: sps,
|
|
|
|
PPS: pps,
|
|
|
|
}
|
2024-01-15 11:08:14 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-07 15:59:32 +00:00
|
|
|
var dtsExtractor *h264.DTSExtractor2
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.H264)
|
|
|
|
if tunit.AU == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
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
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
2024-10-04 22:49:44 +00:00
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
if dtsExtractor == nil {
|
|
|
|
if !randomAccess {
|
|
|
|
return nil
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
2024-10-07 15:59:32 +00:00
|
|
|
dtsExtractor = h264.NewDTSExtractor2()
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
dts, err := dtsExtractor.Extract(tunit.AU, tunit.PTS)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
sampl, err := fmp4.NewPartSampleH26x(
|
2024-10-07 15:59:32 +00:00
|
|
|
int32(tunit.PTS-dts),
|
2024-10-04 22:49:44 +00:00
|
|
|
randomAccess,
|
|
|
|
tunit.AU)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
return track.write(&sample{
|
|
|
|
PartSample: sampl,
|
|
|
|
dts: dts,
|
|
|
|
ntp: tunit.NTP,
|
|
|
|
})
|
2023-10-14 20:52:10 +00:00
|
|
|
})
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.MPEG4Video:
|
2023-10-14 20:52:10 +00:00
|
|
|
config := forma.SafeParams()
|
|
|
|
|
|
|
|
if config == nil {
|
2024-08-01 14:42:53 +00:00
|
|
|
config = formatprocessor.MPEG4VideoDefaultConfig
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
codec := &fmp4.CodecMPEG4Video{
|
|
|
|
Config: config,
|
|
|
|
}
|
2024-01-15 11:08:14 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
|
|
|
firstReceived := false
|
2024-10-07 15:59:32 +00:00
|
|
|
var lastPTS int64
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.MPEG4Video)
|
|
|
|
if tunit.Frame == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
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]
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
if !bytes.Equal(codec.Config, config) {
|
|
|
|
codec.Config = config
|
|
|
|
updateCodecs()
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
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)")
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
2024-10-04 22:49:44 +00:00
|
|
|
lastPTS = tunit.PTS
|
|
|
|
|
|
|
|
return track.write(&sample{
|
|
|
|
PartSample: &fmp4.PartSample{
|
|
|
|
Payload: tunit.Frame,
|
|
|
|
IsNonSyncSample: !randomAccess,
|
|
|
|
},
|
|
|
|
dts: tunit.PTS,
|
|
|
|
ntp: tunit.NTP,
|
|
|
|
})
|
2023-10-14 20:52:10 +00:00
|
|
|
})
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.MPEG1Video:
|
2023-10-14 20:52:10 +00:00
|
|
|
codec := &fmp4.CodecMPEG1Video{
|
2024-08-01 14:42:53 +00:00
|
|
|
Config: formatprocessor.MPEG1VideoDefaultConfig,
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
2024-01-15 11:08:14 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
|
|
|
firstReceived := false
|
2024-10-07 15:59:32 +00:00
|
|
|
var lastPTS int64
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.MPEG1Video)
|
|
|
|
if tunit.Frame == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, 0xB8})
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
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]
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
if !bytes.Equal(codec.Config, config) {
|
|
|
|
codec.Config = config
|
|
|
|
updateCodecs()
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
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)")
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
2024-10-04 22:49:44 +00:00
|
|
|
lastPTS = tunit.PTS
|
|
|
|
|
|
|
|
return track.write(&sample{
|
|
|
|
PartSample: &fmp4.PartSample{
|
|
|
|
Payload: tunit.Frame,
|
|
|
|
IsNonSyncSample: !randomAccess,
|
|
|
|
},
|
|
|
|
dts: tunit.PTS,
|
|
|
|
ntp: tunit.NTP,
|
|
|
|
})
|
2023-10-14 20:52:10 +00:00
|
|
|
})
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.MJPEG:
|
2023-10-14 20:52:10 +00:00
|
|
|
codec := &fmp4.CodecMJPEG{
|
|
|
|
Width: 800,
|
|
|
|
Height: 600,
|
|
|
|
}
|
2024-01-15 11:08:14 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
|
|
|
parsed := false
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.MJPEG)
|
|
|
|
if tunit.Frame == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
if !parsed {
|
|
|
|
parsed = true
|
|
|
|
width, height, err := jpegExtractSize(tunit.Frame)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
codec.Width = width
|
|
|
|
codec.Height = height
|
|
|
|
updateCodecs()
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
return track.write(&sample{
|
|
|
|
PartSample: &fmp4.PartSample{
|
|
|
|
Payload: tunit.Frame,
|
|
|
|
},
|
|
|
|
dts: tunit.PTS,
|
|
|
|
ntp: tunit.NTP,
|
|
|
|
})
|
2023-10-14 20:52:10 +00:00
|
|
|
})
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.Opus:
|
2023-10-14 20:52:10 +00:00
|
|
|
codec := &fmp4.CodecOpus{
|
2024-05-19 12:38:57 +00:00
|
|
|
ChannelCount: forma.ChannelCount,
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
2024-01-15 11:08:14 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.Opus)
|
|
|
|
if tunit.Packets == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-07 15:59:32 +00:00
|
|
|
pts := tunit.PTS
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
for _, packet := range tunit.Packets {
|
|
|
|
err := track.write(&sample{
|
|
|
|
PartSample: &fmp4.PartSample{
|
|
|
|
Payload: packet,
|
|
|
|
},
|
2024-10-07 15:59:32 +00:00
|
|
|
dts: pts,
|
|
|
|
ntp: tunit.NTP.Add(timestampToDuration(pts, clockRate)),
|
2024-10-04 22:49:44 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-07 15:59:32 +00:00
|
|
|
pts += int64(opus.PacketDuration(packet)) * int64(clockRate) / int64(time.Second)
|
2024-10-04 22:49:44 +00:00
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
return nil
|
|
|
|
})
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.MPEG4Audio:
|
2024-08-01 14:34:40 +00:00
|
|
|
co := forma.GetConfig()
|
2024-09-26 12:42:48 +00:00
|
|
|
if co != nil {
|
2024-08-01 14:34:40 +00:00
|
|
|
codec := &fmp4.CodecMPEG4Audio{
|
|
|
|
Config: *co,
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
2024-08-01 14:34:40 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.MPEG4Audio)
|
|
|
|
if tunit.AUs == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2024-08-01 14:34:40 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
for i, au := range tunit.AUs {
|
2024-10-07 15:59:32 +00:00
|
|
|
pts := tunit.PTS + int64(i)*mpeg4audio.SamplesPerAccessUnit
|
2024-10-04 22:49:44 +00:00
|
|
|
|
|
|
|
err := track.write(&sample{
|
|
|
|
PartSample: &fmp4.PartSample{
|
|
|
|
Payload: au,
|
|
|
|
},
|
2024-10-07 15:59:32 +00:00
|
|
|
dts: pts,
|
|
|
|
ntp: tunit.NTP.Add(timestampToDuration(pts, clockRate)),
|
2024-10-04 22:49:44 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-08-01 14:34:40 +00:00
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
return nil
|
|
|
|
})
|
2024-08-01 14:34:40 +00:00
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.MPEG1Audio:
|
2023-10-14 20:52:10 +00:00
|
|
|
codec := &fmp4.CodecMPEG1Audio{
|
|
|
|
SampleRate: 32000,
|
|
|
|
ChannelCount: 2,
|
|
|
|
}
|
2024-01-15 11:08:14 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
|
|
|
parsed := false
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.MPEG1Audio)
|
|
|
|
if tunit.Frames == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
var dt time.Duration
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
for _, frame := range tunit.Frames {
|
|
|
|
var h mpeg1audio.FrameHeader
|
|
|
|
err := h.Unmarshal(frame)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
if !parsed {
|
|
|
|
parsed = true
|
|
|
|
codec.SampleRate = h.SampleRate
|
|
|
|
codec.ChannelCount = mpeg1audioChannelCount(h.ChannelMode)
|
|
|
|
updateCodecs()
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
err = track.write(&sample{
|
|
|
|
PartSample: &fmp4.PartSample{
|
|
|
|
Payload: frame,
|
|
|
|
},
|
|
|
|
dts: tunit.PTS + tunit.PTS,
|
|
|
|
ntp: tunit.NTP,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
dt += time.Duration(h.SampleCount()) *
|
|
|
|
time.Second / time.Duration(h.SampleRate)
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
return nil
|
|
|
|
})
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.AC3:
|
2023-10-14 20:52:10 +00:00
|
|
|
codec := &fmp4.CodecAC3{
|
|
|
|
SampleRate: forma.SampleRate,
|
|
|
|
ChannelCount: forma.ChannelCount,
|
|
|
|
Fscod: 0,
|
|
|
|
Bsid: 8,
|
|
|
|
Bsmod: 0,
|
|
|
|
Acmod: 7,
|
|
|
|
LfeOn: true,
|
|
|
|
BitRateCode: 7,
|
|
|
|
}
|
2024-01-15 11:08:14 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
|
|
|
parsed := false
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.AC3)
|
|
|
|
if tunit.Frames == nil {
|
|
|
|
return nil
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
for i, frame := range tunit.Frames {
|
|
|
|
var syncInfo ac3.SyncInfo
|
|
|
|
err := syncInfo.Unmarshal(frame)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("invalid AC-3 frame: %w", err)
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
var bsi ac3.BSI
|
|
|
|
err = bsi.Unmarshal(frame[5:])
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("invalid AC-3 frame: %w", err)
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
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()
|
|
|
|
}
|
2024-01-21 18:15:31 +00:00
|
|
|
|
2024-10-07 15:59:32 +00:00
|
|
|
pts := tunit.PTS + int64(i)*ac3.SamplesPerFrame
|
2024-10-04 22:49:44 +00:00
|
|
|
|
|
|
|
err = track.write(&sample{
|
|
|
|
PartSample: &fmp4.PartSample{
|
|
|
|
Payload: frame,
|
|
|
|
},
|
2024-10-07 15:59:32 +00:00
|
|
|
dts: pts,
|
|
|
|
ntp: tunit.NTP.Add(timestampToDuration(pts, clockRate)),
|
2024-10-04 22:49:44 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
return nil
|
|
|
|
})
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.G722:
|
2023-10-14 20:52:10 +00:00
|
|
|
// TODO
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.G711:
|
2023-12-28 22:17:50 +00:00
|
|
|
codec := &fmp4.CodecLPCM{
|
|
|
|
LittleEndian: false,
|
|
|
|
BitDepth: 16,
|
2024-01-13 10:40:26 +00:00
|
|
|
SampleRate: forma.SampleRate,
|
|
|
|
ChannelCount: forma.ChannelCount,
|
2023-12-28 22:17:50 +00:00
|
|
|
}
|
2024-01-15 11:08:14 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-12-28 22:17:50 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.G711)
|
|
|
|
if tunit.Samples == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-12-28 22:17:50 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
var out []byte
|
|
|
|
if forma.MULaw {
|
|
|
|
out = g711.DecodeMulaw(tunit.Samples)
|
|
|
|
} else {
|
|
|
|
out = g711.DecodeAlaw(tunit.Samples)
|
|
|
|
}
|
2023-12-28 22:17:50 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
return track.write(&sample{
|
|
|
|
PartSample: &fmp4.PartSample{
|
|
|
|
Payload: out,
|
|
|
|
},
|
|
|
|
dts: tunit.PTS,
|
|
|
|
ntp: tunit.NTP,
|
|
|
|
})
|
2023-12-28 22:17:50 +00:00
|
|
|
})
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
case *rtspformat.LPCM:
|
2023-10-14 20:52:10 +00:00
|
|
|
codec := &fmp4.CodecLPCM{
|
|
|
|
LittleEndian: false,
|
|
|
|
BitDepth: forma.BitDepth,
|
|
|
|
SampleRate: forma.SampleRate,
|
|
|
|
ChannelCount: forma.ChannelCount,
|
|
|
|
}
|
2024-01-15 11:08:14 +00:00
|
|
|
track := addTrack(forma, codec)
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
f.ai.agent.Stream.AddReader(
|
|
|
|
f.ai,
|
|
|
|
media,
|
|
|
|
forma,
|
|
|
|
func(u unit.Unit) error {
|
|
|
|
tunit := u.(*unit.LPCM)
|
|
|
|
if tunit.Samples == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-10-14 20:52:10 +00:00
|
|
|
|
2024-10-04 22:49:44 +00:00
|
|
|
return track.write(&sample{
|
|
|
|
PartSample: &fmp4.PartSample{
|
|
|
|
Payload: tunit.Samples,
|
|
|
|
},
|
|
|
|
dts: tunit.PTS,
|
|
|
|
ntp: tunit.NTP,
|
|
|
|
})
|
2023-10-14 20:52:10 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-26 12:42:48 +00:00
|
|
|
if len(setuppedFormats) == 0 {
|
2024-09-15 17:28:05 +00:00
|
|
|
f.ai.Log(logger.Warn, "no supported tracks found, skipping recording")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-09-26 12:42:48 +00:00
|
|
|
n := 1
|
|
|
|
for _, medi := range f.ai.agent.Stream.Desc().Medias {
|
|
|
|
for _, forma := range medi.Formats {
|
|
|
|
if _, ok := setuppedFormatsMap[forma]; !ok {
|
|
|
|
f.ai.Log(logger.Warn, "skipping track %d (%s)", n, forma.Codec())
|
|
|
|
}
|
|
|
|
n++
|
|
|
|
}
|
2024-09-15 17:28:05 +00:00
|
|
|
}
|
|
|
|
|
2024-08-01 14:34:40 +00:00
|
|
|
f.ai.Log(logger.Info, "recording %s",
|
2024-09-26 12:42:48 +00:00
|
|
|
defs.FormatsInfo(setuppedFormats))
|
2023-10-14 20:52:10 +00:00
|
|
|
}
|
|
|
|
|
2023-12-02 14:03:37 +00:00
|
|
|
func (f *formatFMP4) close() {
|
2023-10-14 20:52:10 +00:00
|
|
|
if f.currentSegment != nil {
|
2024-06-11 16:30:40 +00:00
|
|
|
for _, track := range f.tracks {
|
2024-10-07 15:59:32 +00:00
|
|
|
if track.nextSample != nil &&
|
|
|
|
timestampToDuration(track.nextSample.dts, int(track.initTrack.TimeScale)) > f.currentSegment.lastDTS {
|
|
|
|
f.currentSegment.lastDTS = timestampToDuration(track.nextSample.dts, int(track.initTrack.TimeScale))
|
2024-06-11 16:30:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-14 20:52:10 +00:00
|
|
|
f.currentSegment.close() //nolint:errcheck
|
|
|
|
}
|
|
|
|
}
|