diff --git a/Makefile b/Makefile index 565fcc72..ce974224 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,12 @@ help: @echo " test run available tests" @echo "" +blank := +define NL + +$(blank) +endef + mod-tidy: docker run --rm -it -v $(PWD):/s $(BASE_IMAGE) \ sh -c "apk add git && cd /s && go get && go mod tidy" diff --git a/README.md b/README.md index ff0cef98..33df2a70 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ RTSP 1.0 library for the Go programming language, written for [rtsp-simple-serve ## Examples [client-tcp.go](examples/client-tcp.go) +[client-udp.go](examples/client-tcp.go) ## Documentation diff --git a/conn-client.go b/conn-client.go index 56b4179d..952d5b07 100644 --- a/conn-client.go +++ b/conn-client.go @@ -181,7 +181,7 @@ func (c *ConnClient) Options(u *url.URL) (*Response, error) { } if res.StatusCode != StatusOK && res.StatusCode != StatusNotFound { - return nil, fmt.Errorf("bad status code: %d (%s)", res.StatusCode, res.StatusMessage) + return nil, fmt.Errorf("OPTIONS: bad status code: %d (%s)", res.StatusCode, res.StatusMessage) } return res, nil @@ -199,16 +199,16 @@ func (c *ConnClient) Describe(u *url.URL) (*sdp.SessionDescription, *Response, e } if res.StatusCode != StatusOK { - return nil, nil, fmt.Errorf("bad status code: %d (%s)", res.StatusCode, res.StatusMessage) + return nil, nil, fmt.Errorf("DESCRIBE: bad status code: %d (%s)", res.StatusCode, res.StatusMessage) } contentType, ok := res.Header["Content-Type"] if !ok || len(contentType) != 1 { - return nil, nil, fmt.Errorf("Content-Type not provided") + return nil, nil, fmt.Errorf("DESCRIBE: Content-Type not provided") } if contentType[0] != "application/sdp" { - return nil, nil, fmt.Errorf("wrong Content-Type, expected application/sdp") + return nil, nil, fmt.Errorf("DESCRIBE: wrong Content-Type, expected application/sdp") } sdpd := &sdp.SessionDescription{} @@ -220,10 +220,7 @@ func (c *ConnClient) Describe(u *url.URL) (*sdp.SessionDescription, *Response, e return sdpd, res, nil } -// Setup writes a SETUP request, that indicates that we want to read -// a stream described by the given media, with the given transport, -// and reads a response. -func (c *ConnClient) Setup(u *url.URL, media *sdp.MediaDescription, transport []string) (*Response, error) { +func (c *ConnClient) setup(u *url.URL, media *sdp.MediaDescription, transport []string) (*Response, error) { // build an URL with the control attribute from media u = func() *url.URL { control := func() string { @@ -285,7 +282,65 @@ func (c *ConnClient) Setup(u *url.URL, media *sdp.MediaDescription, transport [] } if res.StatusCode != StatusOK { - return nil, fmt.Errorf("bad status code: %d (%s)", res.StatusCode, res.StatusMessage) + return nil, fmt.Errorf("SETUP: bad status code: %d (%s)", res.StatusCode, res.StatusMessage) + } + + return res, nil +} + +// SetupUdp writes a SETUP request, that indicates that we want to read +// a track with given media and given id with the UDP transport, +// and reads a response. +func (c *ConnClient) SetupUdp(u *url.URL, media *sdp.MediaDescription, + rtpPort int, rtcpPort int) (int, int, *Response, error) { + + res, err := c.setup(u, media, []string{ + "RTP/AVP/UDP", + "unicast", + fmt.Sprintf("client_port=%d-%d", rtpPort, rtcpPort), + }) + if err != nil { + return 0, 0, nil, err + } + + tsRaw, ok := res.Header["Transport"] + if !ok || len(tsRaw) != 1 { + return 0, 0, nil, fmt.Errorf("transport header not provided") + } + + th := ReadHeaderTransport(tsRaw[0]) + rtpServerPort, rtcpServerPort := th.GetPorts("server_port") + if rtpServerPort == 0 { + return 0, 0, nil, fmt.Errorf("server ports not provided") + } + + return rtpServerPort, rtcpServerPort, res, nil +} + +// SetupTcp writes a SETUP request, that indicates that we want to read +// a track with given media and given id with the TCP transport, +// and reads a response. +func (c *ConnClient) SetupTcp(u *url.URL, media *sdp.MediaDescription, trackId int) (*Response, error) { + interleaved := fmt.Sprintf("interleaved=%d-%d", (trackId * 2), (trackId*2)+1) + + res, err := c.setup(u, media, []string{ + "RTP/AVP/TCP", + "unicast", + interleaved, + }) + if err != nil { + return nil, err + } + + tsRaw, ok := res.Header["Transport"] + if !ok || len(tsRaw) != 1 { + return nil, fmt.Errorf("SETUP: transport header not provided") + } + th := ReadHeaderTransport(tsRaw[0]) + + _, ok = th[interleaved] + if !ok { + return nil, fmt.Errorf("SETUP: transport header does not have %s (%s)", interleaved, tsRaw[0]) } return res, nil diff --git a/examples/client-tcp.go b/examples/client-tcp.go index 738004d6..75d7156e 100644 --- a/examples/client-tcp.go +++ b/examples/client-tcp.go @@ -39,11 +39,7 @@ func main() { } for i, media := range sdpd.MediaDescriptions { - _, err := rconn.Setup(u, media, []string{ - "RTP/AVP/TCP", - "unicast", - fmt.Sprintf("interleaved=%d-%d", (i * 2), (i*2)+1), - }) + _, err := rconn.SetupTcp(u, media, i) if err != nil { panic(err) } @@ -54,9 +50,7 @@ func main() { panic(err) } - frame := &gortsplib.InterleavedFrame{ - Content: make([]byte, 512*1024), - } + frame := &gortsplib.InterleavedFrame{Content: make([]byte, 512*1024)} for { err := rconn.ReadFrame(frame) @@ -64,6 +58,7 @@ func main() { panic(err) } - fmt.Println("incoming", frame.Channel, frame.Content) + trackId, streamType := gortsplib.ConvChannelToTrackIdAndStreamType(frame.Channel) + fmt.Printf("packet from track %d, type %v: %v\n", trackId, streamType, frame.Content) } } diff --git a/examples/client-udp.go b/examples/client-udp.go new file mode 100644 index 00000000..b00d2cca --- /dev/null +++ b/examples/client-udp.go @@ -0,0 +1,100 @@ +// +build ignore + +package main + +import ( + "fmt" + "net" + "net/url" + "strconv" + "time" + + "github.com/aler9/gortsplib" +) + +func main() { + u, err := url.Parse("rtsp://user:pass@example.com/mystream") + if err != nil { + panic(err) + } + + conn, err := net.DialTimeout("tcp", u.Host, 5*time.Second) + if err != nil { + panic(err) + } + defer conn.Close() + + rconn, err := gortsplib.NewConnClient(gortsplib.ConnClientConf{Conn: conn}) + if err != nil { + panic(err) + } + + _, err = rconn.Options(u) + if err != nil { + panic(err) + } + + sdpd, _, err := rconn.Describe(u) + if err != nil { + panic(err) + } + + var rtpListeners []net.PacketConn + var rtcpListeners []net.PacketConn + + for i, media := range sdpd.MediaDescriptions { + rtpPort := 9000 + i*2 + rtpl, err := net.ListenPacket("udp", ":"+strconv.FormatInt(int64(rtpPort), 10)) + if err != nil { + panic(err) + } + rtpListeners = append(rtpListeners, rtpl) + + rtcpPort := 9001 + i*2 + rtcpl, err := net.ListenPacket("udp", ":"+strconv.FormatInt(int64(rtcpPort), 10)) + if err != nil { + panic(err) + } + rtcpListeners = append(rtcpListeners, rtcpl) + + _, _, _, err = rconn.SetupUdp(u, media, rtpPort, rtcpPort) + if err != nil { + panic(err) + } + } + + _, err = rconn.Play(u) + if err != nil { + panic(err) + } + + for trackId, l := range rtpListeners { + go func(trackId int, l net.PacketConn) { + buf := make([]byte, 2048) + for { + n, _, err := l.ReadFrom(buf) + if err != nil { + break + } + + fmt.Printf("packet from track %d, type RTP: %v\n", trackId, buf[:n]) + } + }(trackId, l) + } + + for trackId, l := range rtcpListeners { + go func(trackId int, l net.PacketConn) { + buf := make([]byte, 2048) + for { + n, _, err := l.ReadFrom(buf) + if err != nil { + break + } + + fmt.Printf("packet from track %d, type RTCP: %v\n", trackId, buf[:n]) + } + }(trackId, l) + } + + select {} +} diff --git a/interleaved-frame.go b/interleaved-frame.go index 9993eb37..6a5db9d4 100644 --- a/interleaved-frame.go +++ b/interleaved-frame.go @@ -22,6 +22,7 @@ const ( StreamTypeRtcp ) +// ConvChannelToTrackIdAndStreamType converts a channel into a track id and a streamType. func ConvChannelToTrackIdAndStreamType(channel uint8) (int, StreamType) { if (channel % 2) == 0 { return int(channel / 2), StreamTypeRtp @@ -29,6 +30,7 @@ func ConvChannelToTrackIdAndStreamType(channel uint8) (int, StreamType) { return int((channel - 1) / 2), StreamTypeRtcp } +// ConvTrackIdAndStreamTypeToChannel converts a track id and a streamType into a channel. func ConvTrackIdAndStreamTypeToChannel(trackId int, StreamType StreamType) uint8 { if StreamType == StreamTypeRtp { return uint8(trackId * 2)