client: build track URLs by using Content-Base, when provided by server

This commit is contained in:
aler9
2021-04-03 15:45:14 +02:00
parent c946e4b6cf
commit 1f8d7a9ae7
9 changed files with 312 additions and 120 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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")