* bump golangci-lint to 2.13.1 * additional changes --------- Co-authored-by: aler9 <46489434+aler9@users.noreply.github.com>
936 lines
21 KiB
Go
936 lines
21 KiB
Go
package recorder
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"slices"
|
|
"time"
|
|
|
|
rtspformat "github.com/bluenviron/gortsplib/v5/pkg/format"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/av1"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/g711"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/opus"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4"
|
|
mcodecs "github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs"
|
|
|
|
"github.com/bluenviron/mediamtx/internal/defs"
|
|
"github.com/bluenviron/mediamtx/internal/formatlabel"
|
|
"github.com/bluenviron/mediamtx/internal/logger"
|
|
"github.com/bluenviron/mediamtx/internal/unit"
|
|
)
|
|
|
|
var (
|
|
av1DefaultSequenceHeader = []byte{
|
|
8, 0, 0, 0, 66, 167, 191, 228, 96, 13, 0, 64,
|
|
}
|
|
|
|
h265DefaultVPS = []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,
|
|
}
|
|
|
|
h265DefaultSPS = []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,
|
|
}
|
|
|
|
h265DefaultPPS = []byte{
|
|
0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,
|
|
}
|
|
|
|
h264DefaultSPS = []byte{ // 1920x1080 baseline
|
|
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,
|
|
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,
|
|
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,
|
|
}
|
|
|
|
h264DefaultPPS = []byte{0x08, 0x06, 0x07, 0x08}
|
|
|
|
mpeg4VideoDefaultConfig = []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,
|
|
}
|
|
|
|
mpeg1VideoDefaultConfig = []byte{
|
|
0x00, 0x00, 0x01, 0xb3, 0x78, 0x04, 0x38, 0x35,
|
|
0xff, 0xff, 0xe0, 0x18, 0x00, 0x00, 0x01, 0xb5,
|
|
0x14, 0x4a, 0x00, 0x01, 0x00, 0x00,
|
|
}
|
|
)
|
|
|
|
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) {
|
|
image = image[2:]
|
|
|
|
for {
|
|
h1 := image[1]
|
|
image = image[2:]
|
|
|
|
switch h1 {
|
|
case 0xE0, 0xE1, 0xE2, // JFIF
|
|
jpeg.MarkerDefineHuffmanTable,
|
|
jpeg.MarkerComment,
|
|
jpeg.MarkerDefineQuantizationTable,
|
|
jpeg.MarkerDefineRestartInterval:
|
|
mlen := int(image[0])<<8 | int(image[1])
|
|
image = image[mlen:]
|
|
|
|
case jpeg.MarkerStartOfFrame1:
|
|
mlen := int(image[0])<<8 | int(image[1])
|
|
|
|
var sof jpeg.StartOfFrame1
|
|
err := sof.Unmarshal(image[2:mlen])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return sof.Width, sof.Height
|
|
}
|
|
}
|
|
}
|
|
|
|
type formatFMP4Sample struct {
|
|
*fmp4.Sample
|
|
dts int64
|
|
ntp time.Time
|
|
}
|
|
|
|
type formatFMP4 struct {
|
|
ri *recorderInstance
|
|
|
|
tracks []*formatFMP4Track
|
|
hasVideo bool
|
|
currentSegment *formatFMP4Segment
|
|
nextSegmentNumber uint64
|
|
}
|
|
|
|
func (f *formatFMP4) initialize() bool {
|
|
nextID := 1
|
|
|
|
addTrack := func(format rtspformat.Format, codec mcodecs.Codec) *formatFMP4Track {
|
|
track := &formatFMP4Track{
|
|
f: f,
|
|
id: nextID,
|
|
clockRate: uint32(format.ClockRate()),
|
|
codec: codec,
|
|
}
|
|
track.initialize()
|
|
|
|
nextID++
|
|
f.tracks = append(f.tracks, track)
|
|
return track
|
|
}
|
|
|
|
outDesc := f.ri.stream.OutDescCopy()
|
|
|
|
for i, origMedia := range f.ri.stream.OrigDesc.Medias {
|
|
for j, origFormat := range origMedia.Formats {
|
|
clockRate := origFormat.ClockRate()
|
|
|
|
switch origFormat := origFormat.(type) {
|
|
case *rtspformat.AV1:
|
|
codec := &mcodecs.AV1{
|
|
SequenceHeader: av1DefaultSequenceHeader,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
firstReceived := false
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
randomAccess := false
|
|
paramsChanged := false
|
|
|
|
for _, obu := range u.Payload.(unit.PayloadAV1) {
|
|
typ := av1.OBUType((obu[0] >> 3) & 0b1111)
|
|
|
|
if typ == av1.OBUTypeSequenceHeader {
|
|
if !bytes.Equal(codec.SequenceHeader, obu) {
|
|
codec.SequenceHeader = obu
|
|
paramsChanged = true
|
|
}
|
|
randomAccess = true
|
|
}
|
|
}
|
|
|
|
if paramsChanged {
|
|
f.updateCodecParams()
|
|
}
|
|
|
|
if !firstReceived {
|
|
if !randomAccess {
|
|
return nil
|
|
}
|
|
firstReceived = true
|
|
}
|
|
|
|
payload, err := av1.Bitstream(u.Payload.(unit.PayloadAV1)).Marshal()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
IsNonSyncSample: !randomAccess,
|
|
Payload: payload,
|
|
},
|
|
dts: u.PTS,
|
|
ntp: u.NTP,
|
|
})
|
|
})
|
|
|
|
case *rtspformat.VP9:
|
|
codec := &mcodecs.VP9{
|
|
Width: 1280,
|
|
Height: 720,
|
|
Profile: 1,
|
|
BitDepth: 8,
|
|
ChromaSubsampling: 1,
|
|
ColorRange: false,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
firstReceived := false
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
var h vp9.Header
|
|
err := h.Unmarshal(u.Payload.(unit.PayloadVP9))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
randomAccess := false
|
|
paramsChanged := false
|
|
|
|
if !h.NonKeyFrame {
|
|
randomAccess = true
|
|
|
|
if w := h.Width(); codec.Width != w {
|
|
codec.Width = w
|
|
paramsChanged = true
|
|
}
|
|
if h := h.Width(); codec.Height != h {
|
|
codec.Height = h
|
|
paramsChanged = true
|
|
}
|
|
if codec.Profile != h.Profile {
|
|
codec.Profile = h.Profile
|
|
paramsChanged = true
|
|
}
|
|
if codec.BitDepth != h.ColorConfig.BitDepth {
|
|
codec.BitDepth = h.ColorConfig.BitDepth
|
|
paramsChanged = true
|
|
}
|
|
if c := h.ChromaSubsampling(); codec.ChromaSubsampling != c {
|
|
codec.ChromaSubsampling = c
|
|
paramsChanged = true
|
|
}
|
|
if codec.ColorRange != h.ColorConfig.ColorRange {
|
|
codec.ColorRange = h.ColorConfig.ColorRange
|
|
paramsChanged = true
|
|
}
|
|
}
|
|
|
|
if paramsChanged {
|
|
f.updateCodecParams()
|
|
}
|
|
|
|
if !firstReceived {
|
|
if !randomAccess {
|
|
return nil
|
|
}
|
|
firstReceived = true
|
|
}
|
|
|
|
return track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
IsNonSyncSample: !randomAccess,
|
|
Payload: u.Payload.(unit.PayloadVP9),
|
|
},
|
|
dts: u.PTS,
|
|
ntp: u.NTP,
|
|
})
|
|
})
|
|
|
|
case *rtspformat.VP8:
|
|
// TODO
|
|
|
|
case *rtspformat.H265:
|
|
outFormat := outDesc.Medias[i].Formats[j].(*rtspformat.H265)
|
|
|
|
vps := outFormat.VPS
|
|
sps := outFormat.SPS
|
|
pps := outFormat.PPS
|
|
|
|
if vps == nil || sps == nil || pps == nil {
|
|
vps = h265DefaultVPS
|
|
sps = h265DefaultSPS
|
|
pps = h265DefaultPPS
|
|
}
|
|
|
|
codec := &mcodecs.H265{
|
|
VPS: vps,
|
|
SPS: sps,
|
|
PPS: pps,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
var dtsExtractor *h265.DTSExtractor
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
randomAccess := false
|
|
paramsChanged := false
|
|
|
|
for _, nalu := range u.Payload.(unit.PayloadH265) {
|
|
typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
|
|
|
|
switch typ {
|
|
case h265.NALUType_VPS_NUT:
|
|
if !bytes.Equal(codec.VPS, nalu) {
|
|
codec.VPS = nalu
|
|
paramsChanged = true
|
|
}
|
|
|
|
case h265.NALUType_SPS_NUT:
|
|
if !bytes.Equal(codec.SPS, nalu) {
|
|
codec.SPS = nalu
|
|
paramsChanged = true
|
|
}
|
|
|
|
case h265.NALUType_PPS_NUT:
|
|
if !bytes.Equal(codec.PPS, nalu) {
|
|
codec.PPS = nalu
|
|
paramsChanged = true
|
|
}
|
|
|
|
case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT:
|
|
randomAccess = true
|
|
}
|
|
}
|
|
|
|
if paramsChanged {
|
|
f.updateCodecParams()
|
|
}
|
|
|
|
if dtsExtractor == nil {
|
|
if !randomAccess {
|
|
return nil
|
|
}
|
|
dtsExtractor = &h265.DTSExtractor{}
|
|
dtsExtractor.Initialize()
|
|
}
|
|
|
|
dts, err := dtsExtractor.Extract(u.Payload.(unit.PayloadH265), u.PTS)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
payload, err := h264.AVCC(u.Payload.(unit.PayloadH265)).Marshal()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
PTSOffset: int32(u.PTS - dts),
|
|
IsNonSyncSample: !randomAccess,
|
|
Payload: payload,
|
|
},
|
|
dts: dts,
|
|
ntp: u.NTP,
|
|
})
|
|
})
|
|
|
|
case *rtspformat.H264:
|
|
outFormat := outDesc.Medias[i].Formats[j].(*rtspformat.H264)
|
|
|
|
sps := outFormat.SPS
|
|
pps := outFormat.PPS
|
|
|
|
if sps == nil || pps == nil {
|
|
sps = h264DefaultSPS
|
|
pps = h264DefaultPPS
|
|
}
|
|
|
|
codec := &mcodecs.H264{
|
|
SPS: sps,
|
|
PPS: pps,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
var dtsExtractor *h264.DTSExtractor
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
randomAccess := false
|
|
paramsChanged := false
|
|
|
|
for _, nalu := range u.Payload.(unit.PayloadH264) {
|
|
typ := h264.NALUType(nalu[0] & 0x1F)
|
|
switch typ {
|
|
case h264.NALUTypeSPS:
|
|
if !bytes.Equal(codec.SPS, nalu) {
|
|
codec.SPS = nalu
|
|
paramsChanged = true
|
|
}
|
|
|
|
case h264.NALUTypePPS:
|
|
if !bytes.Equal(codec.PPS, nalu) {
|
|
codec.PPS = nalu
|
|
paramsChanged = true
|
|
}
|
|
|
|
case h264.NALUTypeIDR:
|
|
randomAccess = true
|
|
}
|
|
}
|
|
|
|
if paramsChanged {
|
|
f.updateCodecParams()
|
|
}
|
|
|
|
if dtsExtractor == nil {
|
|
if !randomAccess {
|
|
return nil
|
|
}
|
|
dtsExtractor = &h264.DTSExtractor{}
|
|
dtsExtractor.Initialize()
|
|
}
|
|
|
|
dts, err := dtsExtractor.Extract(u.Payload.(unit.PayloadH264), u.PTS)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
payload, err := h264.AVCC(u.Payload.(unit.PayloadH264)).Marshal()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
PTSOffset: int32(u.PTS - dts),
|
|
IsNonSyncSample: !randomAccess,
|
|
Payload: payload,
|
|
},
|
|
dts: dts,
|
|
ntp: u.NTP,
|
|
})
|
|
})
|
|
|
|
case *rtspformat.MPEG4Video:
|
|
outFormat := outDesc.Medias[i].Formats[j].(*rtspformat.MPEG4Video)
|
|
|
|
config := outFormat.Config
|
|
|
|
if config == nil {
|
|
config = mpeg4VideoDefaultConfig
|
|
}
|
|
|
|
codec := &mcodecs.MPEG4Video{
|
|
Config: config,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
firstReceived := false
|
|
var lastPTS int64
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
randomAccess := bytes.Contains(u.Payload.(unit.PayloadMPEG4Video),
|
|
[]byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})
|
|
|
|
if bytes.HasPrefix(u.Payload.(unit.PayloadMPEG4Video),
|
|
[]byte{0, 0, 1, byte(mpeg4video.VisualObjectSequenceStartCode)}) {
|
|
end := bytes.Index(u.Payload.(unit.PayloadMPEG4Video)[4:],
|
|
[]byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})
|
|
if end >= 0 {
|
|
config2 := u.Payload.(unit.PayloadMPEG4Video)[:end+4]
|
|
|
|
if !bytes.Equal(codec.Config, config2) {
|
|
codec.Config = config2
|
|
f.updateCodecParams()
|
|
}
|
|
}
|
|
}
|
|
|
|
if !firstReceived {
|
|
if !randomAccess {
|
|
return nil
|
|
}
|
|
firstReceived = true
|
|
} else if u.PTS < lastPTS {
|
|
return fmt.Errorf("MPEG-4 Video streams with B-frames are not supported (yet)")
|
|
}
|
|
lastPTS = u.PTS
|
|
|
|
return track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
Payload: u.Payload.(unit.PayloadMPEG4Video),
|
|
IsNonSyncSample: !randomAccess,
|
|
},
|
|
dts: u.PTS,
|
|
ntp: u.NTP,
|
|
})
|
|
})
|
|
|
|
case *rtspformat.MPEG1Video:
|
|
codec := &mcodecs.MPEG1Video{
|
|
Config: mpeg1VideoDefaultConfig,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
firstReceived := false
|
|
var lastPTS int64
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
randomAccess := bytes.Contains(u.Payload.(unit.PayloadMPEG1Video), []byte{0, 0, 1, 0xB8})
|
|
|
|
if bytes.HasPrefix(u.Payload.(unit.PayloadMPEG1Video), []byte{0, 0, 1, 0xB3}) {
|
|
end := bytes.Index(u.Payload.(unit.PayloadMPEG1Video)[4:], []byte{0, 0, 1, 0xB8})
|
|
if end >= 0 {
|
|
config := u.Payload.(unit.PayloadMPEG1Video)[:end+4]
|
|
|
|
if !bytes.Equal(codec.Config, config) {
|
|
codec.Config = config
|
|
f.updateCodecParams()
|
|
}
|
|
}
|
|
}
|
|
|
|
if !firstReceived {
|
|
if !randomAccess {
|
|
return nil
|
|
}
|
|
firstReceived = true
|
|
} else if u.PTS < lastPTS {
|
|
return fmt.Errorf("MPEG-1/2 Video streams with B-frames are not supported (yet)")
|
|
}
|
|
lastPTS = u.PTS
|
|
|
|
return track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
Payload: u.Payload.(unit.PayloadMPEG1Video),
|
|
IsNonSyncSample: !randomAccess,
|
|
},
|
|
dts: u.PTS,
|
|
ntp: u.NTP,
|
|
})
|
|
})
|
|
|
|
case *rtspformat.MJPEG:
|
|
codec := &mcodecs.MJPEG{
|
|
Width: 800,
|
|
Height: 600,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
parsed := false
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
if !parsed {
|
|
parsed = true
|
|
codec.Width, codec.Height = jpegExtractSize(u.Payload.(unit.PayloadMJPEG))
|
|
f.updateCodecParams()
|
|
}
|
|
|
|
return track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
Payload: u.Payload.(unit.PayloadMJPEG),
|
|
},
|
|
dts: u.PTS,
|
|
ntp: u.NTP,
|
|
})
|
|
})
|
|
|
|
case *rtspformat.Opus:
|
|
codec := &mcodecs.Opus{
|
|
ChannelCount: origFormat.ChannelCount,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
pts := u.PTS
|
|
|
|
for _, packet := range u.Payload.(unit.PayloadOpus) {
|
|
err := track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
Payload: packet,
|
|
},
|
|
dts: pts,
|
|
ntp: u.NTP.Add(timestampToDuration(pts-u.PTS, clockRate)),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pts += opus.PacketDuration2(packet)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
case *rtspformat.MPEG4Audio:
|
|
codec := &mcodecs.MPEG4Audio{
|
|
Config: *origFormat.Config,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
for i, au := range u.Payload.(unit.PayloadMPEG4Audio) {
|
|
pts := u.PTS + int64(i)*mpeg4audio.SamplesPerAccessUnit
|
|
|
|
err := track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
Payload: au,
|
|
},
|
|
dts: pts,
|
|
ntp: u.NTP.Add(timestampToDuration(pts-u.PTS, clockRate)),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
case *rtspformat.MPEG4AudioLATM:
|
|
if !origFormat.CPresent {
|
|
codec := &mcodecs.MPEG4Audio{
|
|
Config: *origFormat.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
var ame mpeg4audio.AudioMuxElement
|
|
ame.StreamMuxConfig = origFormat.StreamMuxConfig
|
|
err := ame.Unmarshal(u.Payload.(unit.PayloadMPEG4AudioLATM))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
Payload: ame.Payloads[0][0][0],
|
|
},
|
|
dts: u.PTS,
|
|
ntp: u.NTP,
|
|
})
|
|
})
|
|
}
|
|
|
|
case *rtspformat.MPEG1Audio:
|
|
codec := &mcodecs.MPEG1Audio{
|
|
SampleRate: 32000,
|
|
ChannelCount: 2,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
parsed := false
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
var dt time.Duration
|
|
|
|
for _, frame := range u.Payload.(unit.PayloadMPEG1Audio) {
|
|
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)
|
|
f.updateCodecParams()
|
|
}
|
|
|
|
err = track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
Payload: frame,
|
|
},
|
|
dts: u.PTS + u.PTS,
|
|
ntp: u.NTP,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dt += time.Duration(h.SampleCount()) *
|
|
time.Second / time.Duration(h.SampleRate)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
case *rtspformat.AC3:
|
|
codec := &mcodecs.AC3{
|
|
SampleRate: origFormat.SampleRate,
|
|
ChannelCount: origFormat.ChannelCount,
|
|
Bsid: 8,
|
|
Bsmod: 0,
|
|
Acmod: 7,
|
|
LfeOn: true,
|
|
BitRateCode: 7,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
parsed := false
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
for i, frame := range u.Payload.(unit.PayloadAC3) {
|
|
var syncInfo ac3.SyncInfo
|
|
err := syncInfo.Unmarshal(frame)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid AC-3 frame: %w", err)
|
|
}
|
|
|
|
var bsi ac3.BSI
|
|
err = bsi.Unmarshal(frame[5:])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid AC-3 frame: %w", err)
|
|
}
|
|
|
|
if !parsed {
|
|
parsed = true
|
|
codec.SampleRate = syncInfo.SampleRate()
|
|
codec.ChannelCount = bsi.ChannelCount()
|
|
codec.Bsid = bsi.Bsid
|
|
codec.Bsmod = bsi.Bsmod
|
|
codec.Acmod = bsi.Acmod
|
|
codec.LfeOn = bsi.LfeOn
|
|
codec.BitRateCode = syncInfo.Frmsizecod >> 1
|
|
f.updateCodecParams()
|
|
}
|
|
|
|
pts := u.PTS + int64(i)*ac3.SamplesPerFrame
|
|
|
|
err = track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
Payload: frame,
|
|
},
|
|
dts: pts,
|
|
ntp: u.NTP.Add(timestampToDuration(pts-u.PTS, clockRate)),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
case *rtspformat.G722:
|
|
// TODO
|
|
|
|
case *rtspformat.G711:
|
|
codec := &mcodecs.LPCM{
|
|
LittleEndian: false,
|
|
BitDepth: 16,
|
|
SampleRate: origFormat.SampleRate,
|
|
ChannelCount: origFormat.ChannelCount,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
var lpcm []byte
|
|
if origFormat.MULaw {
|
|
var mu g711.Mulaw
|
|
mu.Unmarshal(u.Payload.(unit.PayloadG711))
|
|
lpcm = mu
|
|
} else {
|
|
var al g711.Alaw
|
|
al.Unmarshal(u.Payload.(unit.PayloadG711))
|
|
lpcm = al
|
|
}
|
|
|
|
return track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
Payload: lpcm,
|
|
},
|
|
dts: u.PTS,
|
|
ntp: u.NTP,
|
|
})
|
|
})
|
|
|
|
case *rtspformat.LPCM:
|
|
codec := &mcodecs.LPCM{
|
|
LittleEndian: false,
|
|
BitDepth: origFormat.BitDepth,
|
|
SampleRate: origFormat.SampleRate,
|
|
ChannelCount: origFormat.ChannelCount,
|
|
}
|
|
track := addTrack(origFormat, codec)
|
|
|
|
f.ri.reader.OnData(
|
|
origMedia,
|
|
origFormat,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
return track.write(&formatFMP4Sample{
|
|
Sample: &fmp4.Sample{
|
|
Payload: u.Payload.(unit.PayloadLPCM),
|
|
},
|
|
dts: u.PTS,
|
|
ntp: u.NTP,
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(f.tracks) == 0 {
|
|
f.ri.Log(logger.Warn, "no supported tracks found, skipping recording")
|
|
return false
|
|
}
|
|
|
|
setuppedFormats := f.ri.reader.Formats()
|
|
|
|
n := 1
|
|
for _, medi := range f.ri.stream.OrigDesc.Medias {
|
|
for _, forma := range medi.Formats {
|
|
if !slices.Contains(setuppedFormats, forma) {
|
|
f.ri.Log(logger.Warn, "skipping track %d (%s)", n, formatlabel.FormatToLabel(forma))
|
|
}
|
|
n++
|
|
}
|
|
}
|
|
|
|
f.ri.Log(logger.Info, "recording %s",
|
|
defs.FormatsInfo(setuppedFormats))
|
|
|
|
return true
|
|
}
|
|
|
|
func (f *formatFMP4) updateCodecParams() {
|
|
f.ri.Log(logger.Debug, "codec parameters have changed")
|
|
}
|
|
|
|
func (f *formatFMP4) close() {
|
|
if f.currentSegment != nil {
|
|
f.currentSegment.close() //nolint:errcheck
|
|
}
|
|
}
|