diff --git a/clientconn.go b/clientconn.go index 07d4b17a..3745e257 100644 --- a/clientconn.go +++ b/clientconn.go @@ -71,24 +71,24 @@ func (s clientConnState) String() string { // ClientConn is a client-side RTSP connection. type ClientConn struct { - conf ClientConf - nconn net.Conn - isTLS bool - br *bufio.Reader - bw *bufio.Writer - session string - cseq int - sender *auth.Sender - state clientConnState - streamURL *base.URL - streamProtocol *StreamProtocol - tracks map[int]clientConnTrack - getParameterSupported bool - writeMutex sync.Mutex - writeFrameAllowed bool - writeError error - backgroundRunning bool - readCB func(int, StreamType, []byte) + conf ClientConf + nconn net.Conn + isTLS bool + br *bufio.Reader + bw *bufio.Writer + session string + cseq int + sender *auth.Sender + state clientConnState + streamBaseURL *base.URL + streamProtocol *StreamProtocol + tracks map[int]clientConnTrack + useGetParameter bool + writeMutex sync.Mutex + writeFrameAllowed bool + writeError error + backgroundRunning bool + readCB func(int, StreamType, []byte) // TCP stream protocol tcpFrameBuffer *multibuffer.MultiBuffer @@ -161,7 +161,7 @@ func (cc *ClientConn) Close() error { if cc.state == clientConnStatePlay || cc.state == clientConnStateRecord { cc.Do(&base.Request{ Method: base.Teardown, - URL: cc.streamURL, + URL: cc.streamBaseURL, SkipResponse: true, }) } @@ -185,10 +185,10 @@ func (cc *ClientConn) reset() { cc.state = clientConnStateInitial cc.nconn = nil - cc.streamURL = nil + cc.streamBaseURL = nil cc.streamProtocol = nil cc.tracks = make(map[int]clientConnTrack) - cc.getParameterSupported = false + cc.useGetParameter = false cc.backgroundRunning = false // read @@ -375,7 +375,7 @@ func (cc *ClientConn) Options(u *base.URL) (*base.Response, error) { return res, liberrors.ErrClientWrongStatusCode{Code: res.StatusCode, Message: res.StatusMessage} } - cc.getParameterSupported = func() bool { + cc.useGetParameter = func() bool { pub, ok := res.Header["Public"] if !ok || len(pub) != 1 { return false @@ -453,7 +453,29 @@ func (cc *ClientConn) Describe(u *base.URL) (Tracks, *base.Response, error) { return nil, nil, liberrors.ErrClientContentTypeUnsupported{CT: ct} } - tracks, err := ReadTracks(res.Body, u) + baseURL, err := func() (*base.URL, error) { + // prefer Content-Base (optional) + if cb, ok := res.Header["Content-Base"]; ok { + if len(cb) != 1 { + return nil, fmt.Errorf("invalid Content-Base: '%v'", cb) + } + + ret, err := base.ParseURL(cb[0]) + if err != nil { + return nil, fmt.Errorf("invalid Content-Base: '%v'", cb) + } + + return ret, nil + } + + // if not provided, use DESCRIBE URL + return u, nil + }() + if err != nil { + return nil, nil, err + } + + tracks, err := ReadTracks(res.Body, baseURL) if err != nil { return nil, nil, err } @@ -481,7 +503,7 @@ func (cc *ClientConn) Setup(mode headers.TransportMode, track *Track, return nil, liberrors.ErrClientCannotReadPublishAtSameTime{} } - if cc.streamURL != nil && *track.BaseURL != *cc.streamURL { + if cc.streamBaseURL != nil && *track.BaseURL != *cc.streamBaseURL { return nil, liberrors.ErrClientCannotSetupTracksDifferentURLs{} } @@ -670,7 +692,7 @@ func (cc *ClientConn) Setup(mode headers.TransportMode, track *Track, cct.rtcpSender = rtcpsender.New(clockRate) } - cc.streamURL = track.BaseURL + cc.streamBaseURL = track.BaseURL cc.streamProtocol = &proto if proto == StreamProtocolUDP { @@ -726,7 +748,7 @@ func (cc *ClientConn) Pause() (*base.Response, error) { res, err := cc.Do(&base.Request{ Method: base.Pause, - URL: cc.streamURL, + URL: cc.streamBaseURL, }) if err != nil { return nil, err @@ -837,23 +859,19 @@ func (cc *ClientConn) ReadFrames(onFrame func(int, StreamType, []byte)) chan err safeState == clientConnStatePlay { if _, ok := err.(liberrors.ErrClientNoUDPPacketsRecently); ok { if cc.conf.StreamProtocol == nil { - prevURL := cc.streamURL + prevBaseURL := cc.streamBaseURL + oldUseGetParameter := cc.useGetParameter prevTracks := cc.tracks cc.reset() v := StreamProtocolTCP cc.streamProtocol = &v + cc.useGetParameter = oldUseGetParameter - err := cc.connOpen(prevURL.Scheme, prevURL.Host) + err := cc.connOpen(prevBaseURL.Scheme, prevBaseURL.Host) if err != nil { return err } - _, err = cc.Options(prevURL) - if err != nil { - cc.Close() - return err - } - for _, track := range prevTracks { _, err := cc.Setup(headers.TransportModePlay, track.track, 0, 0) if err != nil { diff --git a/clientconnpublish.go b/clientconnpublish.go index 85ad71ea..4ac1828d 100644 --- a/clientconnpublish.go +++ b/clientconnpublish.go @@ -20,11 +20,14 @@ func (cc *ClientConn) Announce(u *base.URL, tracks Tracks) (*base.Response, erro return nil, err } + // in case of ANNOUNCE, the base URL doesn't have a trailing slash. + // (tested with ffmpeg and gstreamer) + baseURL := u.Clone() + // set id, base url and control attribute on tracks for i, t := range tracks { t.ID = i - t.BaseURL = u - + t.BaseURL = baseURL t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ Key: "control", Value: "trackID=" + strconv.FormatInt(int64(i), 10), @@ -48,7 +51,7 @@ func (cc *ClientConn) Announce(u *base.URL, tracks Tracks) (*base.Response, erro Code: res.StatusCode, Message: res.StatusMessage} } - cc.streamURL = u + cc.streamBaseURL = baseURL cc.state = clientConnStatePreRecord return res, nil @@ -66,7 +69,7 @@ func (cc *ClientConn) Record() (*base.Response, error) { res, err := cc.Do(&base.Request{ Method: base.Record, - URL: cc.streamURL, + URL: cc.streamBaseURL, }) if err != nil { return nil, err diff --git a/clientconnpublish_test.go b/clientconnpublish_test.go index 244d69cb..c52df77c 100644 --- a/clientconnpublish_test.go +++ b/clientconnpublish_test.go @@ -40,6 +40,7 @@ func TestClientPublishSerial(t *testing.T) { err = req.Read(bconn.Reader) require.NoError(t, err) require.Equal(t, base.Options, req.Method) + require.Equal(t, base.MustParseURL("rtsp://localhost:8554/teststream"), req.URL) err = base.Response{ StatusCode: base.StatusOK, @@ -56,6 +57,7 @@ func TestClientPublishSerial(t *testing.T) { err = req.Read(bconn.Reader) require.NoError(t, err) require.Equal(t, base.Announce, req.Method) + require.Equal(t, base.MustParseURL("rtsp://localhost:8554/teststream"), req.URL) err = base.Response{ StatusCode: base.StatusOK, @@ -65,6 +67,7 @@ func TestClientPublishSerial(t *testing.T) { err = req.Read(bconn.Reader) require.NoError(t, err) require.Equal(t, base.Setup, req.Method) + require.Equal(t, base.MustParseURL("rtsp://localhost:8554/teststream/trackID=0"), req.URL) var inTH headers.Transport err = inTH.Read(req.Header["Transport"]) @@ -110,6 +113,7 @@ func TestClientPublishSerial(t *testing.T) { err = req.Read(bconn.Reader) require.NoError(t, err) require.Equal(t, base.Record, req.Method) + require.Equal(t, base.MustParseURL("rtsp://localhost:8554/teststream"), req.URL) err = base.Response{ StatusCode: base.StatusOK, @@ -151,6 +155,7 @@ func TestClientPublishSerial(t *testing.T) { err = req.Read(bconn.Reader) require.NoError(t, err) require.Equal(t, base.Teardown, req.Method) + require.Equal(t, base.MustParseURL("rtsp://localhost:8554/teststream"), req.URL) err = base.Response{ StatusCode: base.StatusOK, diff --git a/clientconnread.go b/clientconnread.go index 014839fa..b84be9d7 100644 --- a/clientconnread.go +++ b/clientconnread.go @@ -22,7 +22,7 @@ func (cc *ClientConn) Play() (*base.Response, error) { res, err := cc.Do(&base.Request{ Method: base.Play, - URL: cc.streamURL, + URL: cc.streamBaseURL, }) if err != nil { return nil, err @@ -119,13 +119,13 @@ func (cc *ClientConn) backgroundPlayUDP() error { _, err := cc.Do(&base.Request{ Method: func() base.Method { // the vlc integrated rtsp server requires GET_PARAMETER - if cc.getParameterSupported { + if cc.useGetParameter { return base.GetParameter } return base.Options }(), - // use the stream path, otherwise some cameras do not reply - URL: cc.streamURL, + // use the stream base URL, otherwise some cameras do not reply + URL: cc.streamBaseURL, SkipResponse: true, }) if err != nil { diff --git a/clientconnread_test.go b/clientconnread_test.go index 0be8d710..a6b753a4 100644 --- a/clientconnread_test.go +++ b/clientconnread_test.go @@ -74,6 +74,7 @@ func TestClientRead(t *testing.T) { err = req.Read(bconn.Reader) require.NoError(t, err) require.Equal(t, base.Options, req.Method) + require.Equal(t, base.MustParseURL(scheme+"://localhost:8554/teststream"), req.URL) err = base.Response{ StatusCode: base.StatusOK, @@ -90,6 +91,7 @@ func TestClientRead(t *testing.T) { err = req.Read(bconn.Reader) require.NoError(t, err) require.Equal(t, base.Describe, req.Method) + require.Equal(t, base.MustParseURL(scheme+"://localhost:8554/teststream"), req.URL) track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) @@ -98,6 +100,7 @@ func TestClientRead(t *testing.T) { StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, + "Content-Base": base.HeaderValue{scheme + "://localhost:8554/teststream/"}, }, Body: Tracks{track}.Write(), }.Write(bconn.Writer) @@ -106,6 +109,7 @@ func TestClientRead(t *testing.T) { err = req.Read(bconn.Reader) require.NoError(t, err) require.Equal(t, base.Setup, req.Method) + require.Equal(t, base.MustParseURL(scheme+"://localhost:8554/teststream/trackID=0"), req.URL) var inTH headers.Transport err = inTH.Read(req.Header["Transport"]) @@ -150,6 +154,7 @@ func TestClientRead(t *testing.T) { err = req.Read(bconn.Reader) require.NoError(t, err) require.Equal(t, base.Play, req.Method) + require.Equal(t, base.MustParseURL(scheme+"://localhost:8554/teststream/"), req.URL) err = base.Response{ StatusCode: base.StatusOK, @@ -194,6 +199,16 @@ func TestClientRead(t *testing.T) { } close(frameRecv) + + err = req.Read(bconn.Reader) + require.NoError(t, err) + require.Equal(t, base.Teardown, req.Method) + require.Equal(t, base.MustParseURL(scheme+"://localhost:8554/teststream/"), req.URL) + + err = base.Response{ + StatusCode: base.StatusOK, + }.Write(bconn.Writer) + require.NoError(t, err) }() conf := ClientConf{ @@ -226,6 +241,108 @@ func TestClientRead(t *testing.T) { } } +func TestClientReadNoContentBase(t *testing.T) { + l, err := net.Listen("tcp", "localhost:8554") + require.NoError(t, err) + defer l.Close() + + serverDone := make(chan struct{}) + defer func() { <-serverDone }() + go func() { + defer close(serverDone) + + conn, err := l.Accept() + require.NoError(t, err) + defer conn.Close() + bconn := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) + + var req base.Request + err = req.Read(bconn.Reader) + require.NoError(t, err) + require.Equal(t, base.Options, req.Method) + + err = base.Response{ + StatusCode: base.StatusOK, + Header: base.Header{ + "Public": base.HeaderValue{strings.Join([]string{ + string(base.Describe), + string(base.Setup), + string(base.Play), + }, ", ")}, + }, + }.Write(bconn.Writer) + require.NoError(t, err) + + err = req.Read(bconn.Reader) + require.NoError(t, err) + require.Equal(t, base.Describe, req.Method) + require.Equal(t, base.MustParseURL("rtsp://localhost:8554/teststream"), req.URL) + + track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) + require.NoError(t, err) + + err = base.Response{ + StatusCode: base.StatusOK, + Header: base.Header{ + "Content-Type": base.HeaderValue{"application/sdp"}, + }, + Body: Tracks{track}.Write(), + }.Write(bconn.Writer) + require.NoError(t, err) + + err = req.Read(bconn.Reader) + require.NoError(t, err) + require.Equal(t, base.Setup, req.Method) + require.Equal(t, base.MustParseURL("rtsp://localhost:8554/teststream/trackID=0"), req.URL) + + var inTH headers.Transport + err = inTH.Read(req.Header["Transport"]) + require.NoError(t, err) + + th := headers.Transport{ + Delivery: func() *base.StreamDelivery { + v := base.StreamDeliveryUnicast + return &v + }(), + Protocol: StreamProtocolUDP, + ClientPorts: inTH.ClientPorts, + ServerPorts: &[2]int{34556, 34557}, + } + + err = base.Response{ + StatusCode: base.StatusOK, + Header: base.Header{ + "Transport": th.Write(), + }, + }.Write(bconn.Writer) + require.NoError(t, err) + + err = req.Read(bconn.Reader) + require.NoError(t, err) + require.Equal(t, base.Play, req.Method) + require.Equal(t, base.MustParseURL("rtsp://localhost:8554/teststream"), req.URL) + + err = base.Response{ + StatusCode: base.StatusOK, + }.Write(bconn.Writer) + require.NoError(t, err) + + err = req.Read(bconn.Reader) + require.NoError(t, err) + require.Equal(t, base.Teardown, req.Method) + require.Equal(t, base.MustParseURL("rtsp://localhost:8554/teststream"), req.URL) + + err = base.Response{ + StatusCode: base.StatusOK, + }.Write(bconn.Writer) + require.NoError(t, err) + }() + + conn, err := DialRead("rtsp://localhost:8554/teststream") + require.NoError(t, err) + conn.Close() +} + func TestClientReadAnyPort(t *testing.T) { for _, ca := range []string{ "zero", @@ -274,6 +391,7 @@ func TestClientReadAnyPort(t *testing.T) { StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, + "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, Body: Tracks{track}.Write(), }.Write(bconn.Writer) @@ -392,6 +510,7 @@ func TestClientReadAutomaticProtocol(t *testing.T) { StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, + "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, Body: Tracks{track}.Write(), }.Write(bconn.Writer) @@ -497,6 +616,7 @@ func TestClientReadAutomaticProtocol(t *testing.T) { StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, + "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, Body: Tracks{track}.Write(), }.Write(bconn.Writer) @@ -505,6 +625,7 @@ func TestClientReadAutomaticProtocol(t *testing.T) { err = req.Read(bconn.Reader) require.NoError(t, err) require.Equal(t, base.Setup, req.Method) + require.Equal(t, base.MustParseURL("rtsp://localhost:8554/teststream/trackID=0"), req.URL) var inTH headers.Transport err = inTH.Read(req.Header["Transport"]) @@ -552,25 +673,10 @@ func TestClientReadAutomaticProtocol(t *testing.T) { require.NoError(t, err) bconn = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) - err = req.Read(bconn.Reader) - require.NoError(t, err) - require.Equal(t, base.Options, req.Method) - - err = base.Response{ - StatusCode: base.StatusOK, - Header: base.Header{ - "Public": base.HeaderValue{strings.Join([]string{ - string(base.Describe), - string(base.Setup), - string(base.Play), - }, ", ")}, - }, - }.Write(bconn.Writer) - require.NoError(t, err) - err = req.Read(bconn.Reader) require.NoError(t, err) require.Equal(t, base.Setup, req.Method) + require.Equal(t, base.MustParseURL("rtsp://localhost:8554/teststream/trackID=0"), req.URL) inTH = headers.Transport{} err = inTH.Read(req.Header["Transport"]) @@ -715,6 +821,7 @@ func TestClientReadRedirect(t *testing.T) { StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, + "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, Body: Tracks{track}.Write(), }.Write(bconn.Writer) @@ -869,6 +976,7 @@ func TestClientReadPause(t *testing.T) { StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, + "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, Body: Tracks{track}.Write(), }.Write(bconn.Writer) @@ -1042,6 +1150,7 @@ func TestClientReadRTCPReport(t *testing.T) { StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, + "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, Body: Tracks{track}.Write(), }.Write(bconn.Writer) @@ -1214,6 +1323,7 @@ func TestClientReadErrorTimeout(t *testing.T) { StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, + "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, Body: Tracks{track}.Write(), }.Write(bconn.Writer) diff --git a/pkg/base/url.go b/pkg/base/url.go index 73c19c24..5843674b 100644 --- a/pkg/base/url.go +++ b/pkg/base/url.go @@ -113,16 +113,3 @@ func (u *URL) RTSPPathAndQuery() (string, bool) { return pathAndQuery, true } - -// AddControlAttribute adds a control attribute to a RTSP url. -func (u *URL) AddControlAttribute(controlPath string) { - if controlPath[0] != '?' { - controlPath = "/" + controlPath - } - - // insert the control attribute at the end of the url - // if there's a query, insert it after the query - // otherwise insert it after the path - nu, _ := ParseURL(u.String() + controlPath) - *u = *nu -} diff --git a/pkg/base/url_test.go b/pkg/base/url_test.go index 6d336fb3..7e28a6b9 100644 --- a/pkg/base/url_test.go +++ b/pkg/base/url_test.go @@ -94,50 +94,3 @@ func TestURLRTSPPathAndQuery(t *testing.T) { require.Equal(t, ca.b, b) } } - -func TestURLAddControlAttribute(t *testing.T) { - for _, ca := range []struct { - control string - u *URL - ou *URL - }{ - { - "trackID=1", - MustParseURL("rtsp://localhost:8554/teststream"), - MustParseURL("rtsp://localhost:8554/teststream/trackID=1"), - }, - { - "trackID=1", - MustParseURL("rtsp://localhost:8554/test/stream"), - MustParseURL("rtsp://localhost:8554/test/stream/trackID=1"), - }, - { - "trackID=1", - MustParseURL("rtsp://192.168.1.99:554/test?user=tmp&password=BagRep1&channel=1&stream=0.sdp"), - MustParseURL("rtsp://192.168.1.99:554/test?user=tmp&password=BagRep1&channel=1&stream=0.sdp/trackID=1"), - }, - { - "trackID=1", - MustParseURL("rtsp://192.168.1.99:554/te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp"), - MustParseURL("rtsp://192.168.1.99:554/te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1"), - }, - { - "trackID=1", - MustParseURL("rtsp://192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp"), - MustParseURL("rtsp://192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=1"), - }, - { - "?ctype=video", - MustParseURL("rtsp://192.168.1.99:554/"), - MustParseURL("rtsp://192.168.1.99:554/?ctype=video"), - }, - { - "?ctype=video", - MustParseURL("rtsp://192.168.1.99:554/test"), - MustParseURL("rtsp://192.168.1.99:554/test?ctype=video"), - }, - } { - ca.u.AddControlAttribute(ca.control) - require.Equal(t, ca.ou, ca.u) - } -} diff --git a/track.go b/track.go index 43893a2d..fe264c26 100644 --- a/track.go +++ b/track.go @@ -306,8 +306,14 @@ func (t *Track) URL() (*base.URL, error) { } // control attribute contains a relative control attribute - ur := t.BaseURL.Clone() - ur.AddControlAttribute(controlAttr) + // insert the control attribute at the end of the url + // if there's a query, insert it after the query + // otherwise insert it after the path + strURL := t.BaseURL.String() + if controlAttr[0] != '?' && !strings.HasSuffix(strURL, "/") { + strURL += "/" + } + ur, _ := base.ParseURL(strURL + controlAttr) return ur, nil } diff --git a/track_test.go b/track_test.go index 1be9bc9c..94709923 100644 --- a/track_test.go +++ b/track_test.go @@ -5,6 +5,8 @@ import ( psdp "github.com/pion/sdp/v3" "github.com/stretchr/testify/require" + + "github.com/aler9/gortsplib/pkg/base" ) func TestTrackClockRate(t *testing.T) { @@ -84,6 +86,114 @@ func TestTrackClockRate(t *testing.T) { } } +func TestTrackURL(t *testing.T) { + for _, ca := range []struct { + name string + sdp []byte + baseURL *base.URL + ur *base.URL + }{ + { + "missing control", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"), + }, + { + "absolute control", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:rtsp://localhost/path/trackID=7"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/trackID=7"), + }, + { + "relative control", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:trackID=5"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/path/trackID=5"), + }, + { + "relative control, subpath", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:trackID=5"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/trackID=5"), + }, + { + "relative control, url without slash", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:trackID=5"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/sub/path/trackID=5"), + }, + { + "relative control, url with query", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:trackID=5"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/test?user=tmp&password=BagRep1&channel=1&stream=0.sdp"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/test?user=tmp&password=BagRep1&channel=1&stream=0.sdp/trackID=5"), + }, + { + "relative control, url with special chars and query", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:trackID=5"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/te!st?user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=5"), + }, + { + "relative control, url with query without question mark", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:trackID=5"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp"), + base.MustParseURL("rtsp://myuser:mypass@192.168.1.99:554/user=tmp&password=BagRep1!&channel=1&stream=0.sdp/trackID=5"), + }, + { + "relative control, control is query", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:?ctype=video"), + base.MustParseURL("rtsp://192.168.1.99:554/test"), + base.MustParseURL("rtsp://192.168.1.99:554/test?ctype=video"), + }, + { + "relative control, control is query and no path", + []byte("v=0\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=control:?ctype=video"), + base.MustParseURL("rtsp://192.168.1.99:554/"), + base.MustParseURL("rtsp://192.168.1.99:554/?ctype=video"), + }, + } { + t.Run(ca.name, func(t *testing.T) { + tracks, err := ReadTracks(ca.sdp, nil) + require.NoError(t, err) + tracks[0].BaseURL = ca.baseURL + ur, err := tracks[0].URL() + require.NoError(t, err) + require.Equal(t, ca.ur, ur) + }) + } +} + var testH264SPS = []byte("\x67\x64\x00\x0c\xac\x3b\x50\xb0\x4b\x42\x00\x00\x03\x00\x02\x00\x00\x03\x00\x3d\x08") var testH264PPS = []byte("\x68\xee\x3c\x80")