diff --git a/README.md b/README.md index 33008ada..338b05fd 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ And can be recorded with: |format|video codecs|audio codecs| |------|------------|------------| -|[fMP4](#record-streams-to-disk)|AV1, VP9, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, LPCM| +|[fMP4](#record-streams-to-disk)|AV1, VP9, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711, LPCM| |[MPEG-TS](#record-streams-to-disk)|H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3| **Features** diff --git a/go.mod b/go.mod index ffbf6a1f..971afdb7 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/aler9/writerseeker v1.1.0 github.com/bluenviron/gohlslib v1.1.0 github.com/bluenviron/gortsplib/v4 v4.6.2 - github.com/bluenviron/mediacommon v1.6.0 + github.com/bluenviron/mediacommon v1.6.1-0.20231228213201-7bb211dba7e1 github.com/datarhei/gosrt v0.5.5 github.com/fsnotify/fsnotify v1.7.0 github.com/gin-gonic/gin v1.9.1 diff --git a/go.sum b/go.sum index 14b185f9..630a98f4 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/bluenviron/gohlslib v1.1.0 h1:NL8CYg1BqHpy9tUugd/SbXH5p5vEi7Im/zaw0dp github.com/bluenviron/gohlslib v1.1.0/go.mod h1:kG/Sjebsxnf5asMGaGcQ0aSvtFGNChJPgctds2wDHOI= github.com/bluenviron/gortsplib/v4 v4.6.2 h1:CGIsxpnUFvSlIxnSFS0oFSSfwsHMmBCmYcrGAtIcwXc= github.com/bluenviron/gortsplib/v4 v4.6.2/go.mod h1:dN1YjyPNMfy/NwC17Ga6MiIMiUoQfg5GL7LGsVHa0Jo= -github.com/bluenviron/mediacommon v1.6.0 h1:xJgwbOKKwxyyrEONnSb5J5Sq7NLjNhVQoR/5gs2IDdQ= -github.com/bluenviron/mediacommon v1.6.0/go.mod h1:Ij/kE1LEucSjryNBVTyPL/gBI0d6/Css3f5PyrM957w= +github.com/bluenviron/mediacommon v1.6.1-0.20231228213201-7bb211dba7e1 h1:f8XDAHvgPbT+n5qf73REdUo9kLmGpP4HNgphKI/8fGE= +github.com/bluenviron/mediacommon v1.6.1-0.20231228213201-7bb211dba7e1/go.mod h1:Ij/kE1LEucSjryNBVTyPL/gBI0d6/Css3f5PyrM957w= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= diff --git a/internal/formatprocessor/g711.go b/internal/formatprocessor/g711.go new file mode 100644 index 00000000..ffa39d8c --- /dev/null +++ b/internal/formatprocessor/g711.go @@ -0,0 +1,107 @@ +package formatprocessor //nolint:dupl + +import ( + "fmt" + "time" + + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio" + "github.com/pion/rtp" + + "github.com/bluenviron/mediamtx/internal/unit" +) + +type formatProcessorG711 struct { + udpMaxPayloadSize int + format *format.G711 + encoder *rtpsimpleaudio.Encoder + decoder *rtpsimpleaudio.Decoder +} + +func newG711( + udpMaxPayloadSize int, + forma *format.G711, + generateRTPPackets bool, +) (*formatProcessorG711, error) { + t := &formatProcessorG711{ + udpMaxPayloadSize: udpMaxPayloadSize, + format: forma, + } + + if generateRTPPackets { + err := t.createEncoder() + if err != nil { + return nil, err + } + } + + return t, nil +} + +func (t *formatProcessorG711) createEncoder() error { + t.encoder = &rtpsimpleaudio.Encoder{ + PayloadMaxSize: t.udpMaxPayloadSize - 12, + } + return t.encoder.Init() +} + +func (t *formatProcessorG711) ProcessUnit(uu unit.Unit) error { //nolint:dupl + u := uu.(*unit.G711) + + pkt, err := t.encoder.Encode(u.Samples) + if err != nil { + return err + } + + ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second)) + pkt.Timestamp += ts + + u.RTPPackets = []*rtp.Packet{pkt} + + return nil +} + +func (t *formatProcessorG711) ProcessRTPPacket( //nolint:dupl + pkt *rtp.Packet, + ntp time.Time, + pts time.Duration, + hasNonRTSPReaders bool, +) (Unit, error) { + u := &unit.G711{ + Base: unit.Base{ + RTPPackets: []*rtp.Packet{pkt}, + NTP: ntp, + PTS: pts, + }, + } + + // remove padding + pkt.Header.Padding = false + pkt.PaddingSize = 0 + + if pkt.MarshalSize() > t.udpMaxPayloadSize { + return nil, fmt.Errorf("payload size (%d) is greater than maximum allowed (%d)", + pkt.MarshalSize(), t.udpMaxPayloadSize) + } + + // decode from RTP + if hasNonRTSPReaders || t.decoder != nil { + if t.decoder == nil { + var err error + t.decoder, err = t.format.CreateDecoder() + if err != nil { + return nil, err + } + } + + samples, err := t.decoder.Decode(pkt) + if err != nil { + return nil, err + } + + u.Samples = samples + } + + // route packet as is + return u, nil +} diff --git a/internal/formatprocessor/processor.go b/internal/formatprocessor/processor.go index d603b7ad..76713a3f 100644 --- a/internal/formatprocessor/processor.go +++ b/internal/formatprocessor/processor.go @@ -75,6 +75,9 @@ func New( case *format.AC3: return newAC3(udpMaxPayloadSize, forma, generateRTPPackets) + case *format.G711: + return newG711(udpMaxPayloadSize, forma, generateRTPPackets) + case *format.LPCM: return newLPCM(udpMaxPayloadSize, forma, generateRTPPackets) diff --git a/internal/record/agent_test.go b/internal/record/agent_test.go index a2ee4ea7..eae58d87 100644 --- a/internal/record/agent_test.go +++ b/internal/record/agent_test.go @@ -53,6 +53,18 @@ func TestAgent(t *testing.T) { IndexDeltaLength: 3, }}, }, + { + Type: description.MediaTypeAudio, + Formats: []rtspformat.Format{&rtspformat.G711{ + MULaw: false, + }}, + }, + { + Type: description.MediaTypeAudio, + Formats: []rtspformat.Format{&rtspformat.G711{ + MULaw: true, + }}, + }, }} writeToStream := func(stream *stream.Stream) { @@ -107,6 +119,20 @@ func TestAgent(t *testing.T) { }, AUs: [][]byte{{1, 2, 3, 4}}, }) + + stream.WriteUnit(desc.Medias[3], desc.Medias[3].Formats[0], &unit.G711{ + Base: unit.Base{ + PTS: (50 + time.Duration(i)) * time.Second, + }, + Samples: []byte{1, 2, 3, 4}, + }) + + stream.WriteUnit(desc.Medias[4], desc.Medias[4].Formats[0], &unit.G711{ + Base: unit.Base{ + PTS: (50 + time.Duration(i)) * time.Second, + }, + Samples: []byte{1, 2, 3, 4}, + }) } } diff --git a/internal/record/format_fmp4.go b/internal/record/format_fmp4.go index 83ff16d1..57db81ea 100644 --- a/internal/record/format_fmp4.go +++ b/internal/record/format_fmp4.go @@ -8,6 +8,7 @@ import ( rtspformat "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/g711" "github.com/bluenviron/mediacommon/pkg/codecs/h264" "github.com/bluenviron/mediacommon/pkg/codecs/h265" "github.com/bluenviron/mediacommon/pkg/codecs/jpeg" @@ -771,7 +772,34 @@ func (f *formatFMP4) initialize() { // TODO case *rtspformat.G711: - // TODO + codec := &fmp4.CodecLPCM{ + LittleEndian: false, + BitDepth: 16, + SampleRate: 8000, + ChannelCount: 1, + } + track := addTrack(codec) + + f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error { + tunit := u.(*unit.G711) + if tunit.Samples == nil { + return nil + } + + var out []byte + if forma.MULaw { + out = g711.DecodeMulaw(tunit.Samples) + } else { + out = g711.DecodeAlaw(tunit.Samples) + } + + return track.record(&sample{ + PartSample: &fmp4.PartSample{ + Payload: out, + }, + dts: tunit.PTS, + }) + }) case *rtspformat.LPCM: codec := &fmp4.CodecLPCM{ diff --git a/internal/unit/g711.go b/internal/unit/g711.go new file mode 100644 index 00000000..2169baff --- /dev/null +++ b/internal/unit/g711.go @@ -0,0 +1,7 @@ +package unit + +// G711 is a G711 data unit. +type G711 struct { + Base + Samples []byte +}