diff --git a/README.md b/README.md index d7e07274..1da64abc 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ Features: * [client-read-aac](examples/client-read-aac/main.go) * [client-read-republish](examples/client-read-republish/main.go) * [client-publish-h264](examples/client-publish-h264/main.go) +* [client-publish-pcmu](examples/client-publish-pcmu/main.go) * [client-publish-aac](examples/client-publish-aac/main.go) * [client-publish-opus](examples/client-publish-opus/main.go) * [client-publish-options](examples/client-publish-options/main.go) diff --git a/examples/client-publish-aac/main.go b/examples/client-publish-aac/main.go index ebdb6bb6..f434a880 100644 --- a/examples/client-publish-aac/main.go +++ b/examples/client-publish-aac/main.go @@ -23,8 +23,7 @@ func main() { log.Println("Waiting for a RTP/AAC stream on UDP port 9000 - you can send one with GStreamer:\n" + "gst-launch-1.0 audiotestsrc freq=300 ! audioconvert ! audioresample ! audio/x-raw,rate=48000" + - " ! avenc_aac bitrate=128000" + - " ! rtpmp4gpay ! udpsink host=127.0.0.1 port=9000") + " ! avenc_aac bitrate=128000 ! rtpmp4gpay ! udpsink host=127.0.0.1 port=9000") // wait for first packet buf := make([]byte, 2048) diff --git a/examples/client-publish-opus/main.go b/examples/client-publish-opus/main.go index 9fe93b9b..54c59325 100644 --- a/examples/client-publish-opus/main.go +++ b/examples/client-publish-opus/main.go @@ -23,8 +23,7 @@ func main() { log.Println("Waiting for a RTP/Opus stream on UDP port 9000 - you can send one with GStreamer:\n" + "gst-launch-1.0 audiotestsrc freq=300 ! audioconvert ! audioresample ! audio/x-raw,rate=48000" + - " ! opusenc" + - " ! rtpopuspay ! udpsink host=127.0.0.1 port=9000") + " ! opusenc ! rtpopuspay ! udpsink host=127.0.0.1 port=9000") // wait for first packet buf := make([]byte, 2048) diff --git a/examples/client-publish-pcmu/main.go b/examples/client-publish-pcmu/main.go new file mode 100644 index 00000000..4a90b6ba --- /dev/null +++ b/examples/client-publish-pcmu/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "log" + "net" + + "github.com/aler9/gortsplib" + "github.com/pion/rtp/v2" +) + +// This example shows how to +// 1. generate RTP/PCMU packets with GStreamer +// 2. connect to a RTSP server, announce a PCMU track +// 3. route the packets from GStreamer to the server + +func main() { + // open a listener to receive RTP/PCMU packets + pc, err := net.ListenPacket("udp", "localhost:9000") + if err != nil { + panic(err) + } + defer pc.Close() + + log.Println("Waiting for a RTP/PCMU stream on UDP port 9000 - you can send one with GStreamer:\n" + + "gst-launch-1.0 audiotestsrc freq=300 ! audioconvert ! audioresample ! audio/x-raw,rate=8000" + + " ! mulawenc ! rtppcmupay ! udpsink host=127.0.0.1 port=9000") + + // wait for first packet + buf := make([]byte, 2048) + _, _, err = pc.ReadFrom(buf) + if err != nil { + panic(err) + } + log.Println("stream connected") + + // create a PCMU track + track := gortsplib.NewTrackPCMU() + + 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() + + buf = make([]byte, 2048) + var pkt rtp.Packet + for { + // read packets from the source + n, _, err := pc.ReadFrom(buf) + if err != nil { + panic(err) + } + + // marshal RTP packets + err = pkt.Unmarshal(buf[:n]) + if err != nil { + panic(err) + } + + // route RTP packets to the server + err = c.WritePacketRTP(0, &pkt) + if err != nil { + panic(err) + } + } +} diff --git a/track.go b/track.go index 6cc28ce1..db233fb7 100644 --- a/track.go +++ b/track.go @@ -25,30 +25,43 @@ type Track interface { } func newTrackFromMediaDescription(md *psdp.MediaDescription) (Track, error) { - if rtpmap, ok := md.Attribute("rtpmap"); ok { + rtpmapPart1, payloadType := func() (string, uint8) { + rtpmap, ok := md.Attribute("rtpmap") + if !ok { + return "", 0 + } rtpmap = strings.TrimSpace(rtpmap) - if rtpmapParts := strings.Split(rtpmap, " "); len(rtpmapParts) == 2 { - tmp, err := strconv.ParseInt(rtpmapParts[0], 10, 64) - if err == nil { - payloadType := uint8(tmp) + rtpmapParts := strings.Split(rtpmap, " ") + if len(rtpmapParts) != 2 { + return "", 0 + } - switch { - case md.MediaName.Media == "video": - if rtpmapParts[1] == "H264/90000" { - return newTrackH264FromMediaDescription(payloadType, md) - } + tmp, err := strconv.ParseInt(rtpmapParts[0], 10, 64) + if err != nil { + return "", 0 + } + payloadType := uint8(tmp) - case md.MediaName.Media == "audio": - switch { - case strings.HasPrefix(strings.ToLower(rtpmapParts[1]), "mpeg4-generic/"): - return newTrackAACFromMediaDescription(payloadType, md) + return rtpmapParts[1], payloadType + }() - case strings.HasPrefix(rtpmapParts[1], "opus/"): - return newTrackOpusFromMediaDescription(payloadType, rtpmapParts[1], md) - } - } - } + switch { + case md.MediaName.Media == "video": + if rtpmapPart1 == "H264/90000" { + return newTrackH264FromMediaDescription(payloadType, md) + } + + case md.MediaName.Media == "audio": + switch { + case len(md.MediaName.Formats) == 1 && md.MediaName.Formats[0] == "0": + return newTrackPCMUFromMediaDescription(rtpmapPart1, md) + + case strings.HasPrefix(strings.ToLower(rtpmapPart1), "mpeg4-generic/"): + return newTrackAACFromMediaDescription(payloadType, md) + + case strings.HasPrefix(rtpmapPart1, "opus/"): + return newTrackOpusFromMediaDescription(payloadType, rtpmapPart1, md) } } diff --git a/track_opus.go b/track_opus.go index eb14a4f0..225f7c53 100644 --- a/track_opus.go +++ b/track_opus.go @@ -32,6 +32,7 @@ func newTrackOpusFromMediaDescription( rtpmapPart1 string, md *psdp.MediaDescription) (*TrackOpus, error) { control := trackFindControl(md) + tmp := strings.SplitN(rtpmapPart1, "/", 3) if len(tmp) != 3 { return nil, fmt.Errorf("invalid rtpmap (%v)", rtpmapPart1) diff --git a/track_pcmu.go b/track_pcmu.go new file mode 100644 index 00000000..f90750e2 --- /dev/null +++ b/track_pcmu.go @@ -0,0 +1,79 @@ +package gortsplib + +import ( + "fmt" + "strings" + + psdp "github.com/pion/sdp/v3" + + "github.com/aler9/gortsplib/pkg/base" +) + +// TrackPCMU is a PCMU track. +type TrackPCMU struct { + control string +} + +// NewTrackPCMU allocates a TrackPCMU. +func NewTrackPCMU() *TrackPCMU { + return &TrackPCMU{} +} + +func newTrackPCMUFromMediaDescription(rtpmapPart1 string, + md *psdp.MediaDescription) (*TrackPCMU, error, +) { + control := trackFindControl(md) + + tmp := strings.Split(rtpmapPart1, "/") + if len(tmp) >= 3 && tmp[2] != "1" { + return nil, fmt.Errorf("PCMU tracks must have only one channel") + } + + return &TrackPCMU{ + control: control, + }, nil +} + +// ClockRate returns the track clock rate. +func (t *TrackPCMU) ClockRate() int { + return 8000 +} + +func (t *TrackPCMU) clone() Track { + return &TrackPCMU{} +} + +// GetControl returns the track control. +func (t *TrackPCMU) GetControl() string { + return t.control +} + +// SetControl sets the track control. +func (t *TrackPCMU) SetControl(c string) { + t.control = c +} + +func (t *TrackPCMU) url(contentBase *base.URL) (*base.URL, error) { + return trackURL(t, contentBase) +} + +// MediaDescription returns the media description in SDP format. +func (t *TrackPCMU) MediaDescription() *psdp.MediaDescription { + return &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"0"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "0 PCMU/8000", + }, + { + Key: "control", + Value: t.control, + }, + }, + } +} diff --git a/track_test.go b/track_test.go index 46cf83e2..bfb714a3 100644 --- a/track_test.go +++ b/track_test.go @@ -40,11 +40,7 @@ func TestTrackNewFromMediaDescription(t *testing.T) { Formats: []string{"0"}, }, }, - &TrackGeneric{ - clockRate: 8000, - media: "audio", - formats: []string{"0"}, - }, + &TrackPCMU{}, }, { "aac", diff --git a/tracks_test.go b/tracks_test.go index 8105a5b2..6194c440 100644 --- a/tracks_test.go +++ b/tracks_test.go @@ -76,12 +76,8 @@ func TestTracksReadSkipGenericTracksWithoutClockRate(t *testing.T) { sps: []byte{0x67, 0x64, 0x00, 0x28, 0xac, 0xb4, 0x03, 0xc0, 0x11, 0x3f, 0x2a}, pps: []byte{0x68, 0xee, 0x01, 0x9e, 0x2c}, }, - &TrackGeneric{ - control: "rtsp://10.0.100.50/profile5/media.smp/trackID=a", - clockRate: 8000, - media: "audio", - formats: []string{"0"}, - rtpmap: "0 PCMU/8000", + &TrackPCMU{ + control: "rtsp://10.0.100.50/profile5/media.smp/trackID=a", }, }, tracks) }