diff --git a/README.md b/README.md index 0aa1bf61..910891fa 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Features: * [client-read-republish](examples/client-read-republish/main.go) * [client-publish-codec-g722](examples/client-publish-codec-g722/main.go) * [client-publish-codec-h264](examples/client-publish-codec-h264/main.go) +* [client-publish-codec-lpcm](examples/client-publish-codec-lpcm/main.go) * [client-publish-codec-mpeg4audio](examples/client-publish-codec-mpeg4audio/main.go) * [client-publish-codec-opus](examples/client-publish-codec-opus/main.go) * [client-publish-codec-pcma](examples/client-publish-codec-pcma/main.go) diff --git a/examples/client-publish-codec-lpcm/main.go b/examples/client-publish-codec-lpcm/main.go new file mode 100644 index 00000000..918f2279 --- /dev/null +++ b/examples/client-publish-codec-lpcm/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "log" + "net" + + "github.com/aler9/gortsplib" + "github.com/pion/rtp" +) + +// This example shows how to +// 1. generate RTP/LPCM packets with GStreamer +// 2. connect to a RTSP server, announce an LPCM track +// 3. route the packets from GStreamer to the server + +func main() { + // open a listener to receive RTP/LPCM packets + pc, err := net.ListenPacket("udp", "localhost:9000") + if err != nil { + panic(err) + } + defer pc.Close() + + log.Println("Waiting for a RTP/LPCM stream on UDP port 9000 - you can send one with GStreamer:\n" + + "gst-launch-1.0 audiotestsrc freq=300 ! audioconvert ! audioresample ! audio/x-raw,format=S16BE,rate=44100" + + " ! rtpL16pay ! udpsink host=127.0.0.1 port=9000") + + // wait for first packet + buf := make([]byte, 2048) + n, _, err := pc.ReadFrom(buf) + if err != nil { + panic(err) + } + log.Println("stream connected") + + // create an LPCM track + track := &gortsplib.TrackLPCM{ + PayloadType: 96, + BitDepth: 16, + SampleRate: 44100, + ChannelCount: 1, + } + + c := gortsplib.Client{} + + // connect to the server and start publishing the track + err = c.StartPublishing("rtsp://localhost:8554/mystream", + gortsplib.Tracks{track}) + if err != nil { + panic(err) + } + defer c.Close() + + var pkt rtp.Packet + for { + // parse RTP packet + err = pkt.Unmarshal(buf[:n]) + if err != nil { + panic(err) + } + + // route RTP packet to the server + err = c.WritePacketRTP(0, &pkt) + if err != nil { + panic(err) + } + + // read another RTP packet from source + n, _, err = pc.ReadFrom(buf) + if err != nil { + panic(err) + } + } +} diff --git a/track.go b/track.go index 654f7495..c50b5a4d 100644 --- a/track.go +++ b/track.go @@ -130,11 +130,13 @@ func newTrackFromMediaDescription(md *psdp.MediaDescription) (Track, error) { case payloadType == 14: return newTrackMPEG2AudioFromMediaDescription(control) + case codec == "L8", codec == "L16", codec == "L24": + return newTrackLPCMFromMediaDescription(control, payloadType, codec, clock) case strings.ToLower(codec) == "mpeg4-generic": return newTrackMPEG4AudioFromMediaDescription(control, payloadType, md) case codec == "opus": - return newTrackOpusFromMediaDescription(control, payloadType, clock, md) + return newTrackOpusFromMediaDescription(control, payloadType, clock) } } } diff --git a/track_g722.go b/track_g722.go index 42929df2..d53dd7fc 100644 --- a/track_g722.go +++ b/track_g722.go @@ -14,9 +14,10 @@ type TrackG722 struct { func newTrackG722FromMediaDescription( control string, - rtpmapPart1 string) (*TrackG722, error, + clock string, +) (*TrackG722, error, ) { - tmp := strings.Split(rtpmapPart1, "/") + tmp := strings.Split(clock, "/") if len(tmp) == 2 && tmp[1] != "1" { return nil, fmt.Errorf("G722 tracks can have only one channel") } diff --git a/track_jpeg.go b/track_jpeg.go index f5907bec..c5b3a943 100644 --- a/track_jpeg.go +++ b/track_jpeg.go @@ -10,7 +10,8 @@ type TrackJPEG struct { } func newTrackJPEGFromMediaDescription( - control string) (*TrackJPEG, error, + control string, +) (*TrackJPEG, error, ) { return &TrackJPEG{ trackBase: trackBase{ diff --git a/track_lpcm.go b/track_lpcm.go new file mode 100644 index 00000000..533cbab6 --- /dev/null +++ b/track_lpcm.go @@ -0,0 +1,115 @@ +package gortsplib //nolint:dupl + +import ( + "fmt" + "strconv" + "strings" + + psdp "github.com/pion/sdp/v3" +) + +// TrackLPCM is an uncompressed, Linear PCM track. +type TrackLPCM struct { + PayloadType uint8 + BitDepth int + SampleRate int + ChannelCount int + + trackBase +} + +func newTrackLPCMFromMediaDescription( + control string, + payloadType uint8, + codec string, + clock string, +) (*TrackLPCM, error, +) { + var bitDepth int + switch codec { + case "L8": + bitDepth = 8 + + case "L16": + bitDepth = 16 + + case "L24": + bitDepth = 24 + } + + tmp := strings.SplitN(clock, "/", 32) + if len(tmp) != 2 { + return nil, fmt.Errorf("invalid clock (%v)", clock) + } + + sampleRate, err := strconv.ParseInt(tmp[0], 10, 64) + if err != nil { + return nil, err + } + + channelCount, err := strconv.ParseInt(tmp[1], 10, 64) + if err != nil { + return nil, err + } + + return &TrackLPCM{ + PayloadType: payloadType, + BitDepth: bitDepth, + SampleRate: int(sampleRate), + ChannelCount: int(channelCount), + trackBase: trackBase{ + control: control, + }, + }, nil +} + +// ClockRate returns the track clock rate. +func (t *TrackLPCM) ClockRate() int { + return t.SampleRate +} + +// MediaDescription returns the track media description in SDP format. +func (t *TrackLPCM) MediaDescription() *psdp.MediaDescription { + typ := strconv.FormatInt(int64(t.PayloadType), 10) + + var codec string + switch t.BitDepth { + case 8: + codec = "L8" + + case 16: + codec = "L16" + + case 24: + codec = "L24" + } + + return &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{typ}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: typ + " " + codec + "/" + strconv.FormatInt(int64(t.SampleRate), 10) + + "/" + strconv.FormatInt(int64(t.ChannelCount), 10), + }, + { + Key: "control", + Value: t.control, + }, + }, + } +} + +func (t *TrackLPCM) clone() Track { + return &TrackLPCM{ + PayloadType: t.PayloadType, + BitDepth: t.BitDepth, + SampleRate: t.SampleRate, + ChannelCount: t.ChannelCount, + trackBase: t.trackBase, + } +} diff --git a/track_lpcm_test.go b/track_lpcm_test.go new file mode 100644 index 00000000..51cb20ca --- /dev/null +++ b/track_lpcm_test.go @@ -0,0 +1,59 @@ +package gortsplib + +import ( + "testing" + + psdp "github.com/pion/sdp/v3" + "github.com/stretchr/testify/require" +) + +func TestTrackLPCMAttributes(t *testing.T) { + track := &TrackLPCM{ + PayloadType: 96, + BitDepth: 24, + SampleRate: 44100, + ChannelCount: 2, + } + require.Equal(t, 44100, track.ClockRate()) + require.Equal(t, "", track.GetControl()) +} + +func TestTracLPCMClone(t *testing.T) { + track := &TrackLPCM{ + PayloadType: 96, + BitDepth: 16, + SampleRate: 48000, + ChannelCount: 2, + } + + clone := track.clone() + require.NotSame(t, track, clone) + require.Equal(t, track, clone) +} + +func TestTrackLPCMMediaDescription(t *testing.T) { + track := &TrackLPCM{ + PayloadType: 96, + BitDepth: 24, + SampleRate: 96000, + ChannelCount: 2, + } + + require.Equal(t, &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 L24/96000/2", + }, + { + Key: "control", + Value: "", + }, + }, + }, track.MediaDescription()) +} diff --git a/track_mpeg2audio.go b/track_mpeg2audio.go index 41a9d3ae..d552fef8 100644 --- a/track_mpeg2audio.go +++ b/track_mpeg2audio.go @@ -10,7 +10,8 @@ type TrackMPEG2Audio struct { } func newTrackMPEG2AudioFromMediaDescription( - control string) (*TrackMPEG2Audio, error, + control string, +) (*TrackMPEG2Audio, error, ) { return &TrackMPEG2Audio{ trackBase: trackBase{ diff --git a/track_mpeg2video.go b/track_mpeg2video.go index d71e48fa..d87243de 100644 --- a/track_mpeg2video.go +++ b/track_mpeg2video.go @@ -10,7 +10,8 @@ type TrackMPEG2Video struct { } func newTrackMPEG2VideoFromMediaDescription( - control string) (*TrackMPEG2Video, error, + control string, +) (*TrackMPEG2Video, error, ) { return &TrackMPEG2Video{ trackBase: trackBase{ diff --git a/track_opus.go b/track_opus.go index 285cf06e..3123bb2a 100644 --- a/track_opus.go +++ b/track_opus.go @@ -23,7 +23,6 @@ func newTrackOpusFromMediaDescription( control string, payloadType uint8, clock string, - md *psdp.MediaDescription, ) (*TrackOpus, error) { tmp := strings.SplitN(clock, "/", 32) if len(tmp) != 2 { diff --git a/track_pcma.go b/track_pcma.go index ac34e9f9..4b1dabbc 100644 --- a/track_pcma.go +++ b/track_pcma.go @@ -14,9 +14,10 @@ type TrackPCMA struct { func newTrackPCMAFromMediaDescription( control string, - rtpmapPart1 string) (*TrackPCMA, error, + clock string, +) (*TrackPCMA, error, ) { - tmp := strings.Split(rtpmapPart1, "/") + tmp := strings.Split(clock, "/") if len(tmp) == 2 && tmp[1] != "1" { return nil, fmt.Errorf("PCMA tracks can have only one channel") } diff --git a/track_pcmu.go b/track_pcmu.go index 1ceaf965..e16a4470 100644 --- a/track_pcmu.go +++ b/track_pcmu.go @@ -14,7 +14,8 @@ type TrackPCMU struct { func newTrackPCMUFromMediaDescription( control string, - clock string) (*TrackPCMU, error, + clock string, +) (*TrackPCMU, error, ) { tmp := strings.SplitN(clock, "/", 2) if len(tmp) == 2 && tmp[1] != "1" { diff --git a/track_test.go b/track_test.go index fbd92c2d..7e260a76 100644 --- a/track_test.go +++ b/track_test.go @@ -49,6 +49,72 @@ func TestTrackNewFromMediaDescription(t *testing.T) { }, &TrackG722{}, }, + { + "lpcm 8", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"97"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "97 L8/48000/2", + }, + }, + }, + &TrackLPCM{ + PayloadType: 97, + BitDepth: 8, + SampleRate: 48000, + ChannelCount: 2, + }, + }, + { + "lpcm 16", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"97"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "97 L16/96000/2", + }, + }, + }, + &TrackLPCM{ + PayloadType: 97, + BitDepth: 16, + SampleRate: 96000, + ChannelCount: 2, + }, + }, + { + "lpcm 24", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"98"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "98 L24/44100/4", + }, + }, + }, + &TrackLPCM{ + PayloadType: 98, + BitDepth: 24, + SampleRate: 44100, + ChannelCount: 4, + }, + }, { "mpeg audio", &psdp.MediaDescription{