diff --git a/client_read_test.go b/client_read_test.go index 7fd4cdc8..654732b5 100644 --- a/client_read_test.go +++ b/client_read_test.go @@ -67,13 +67,15 @@ func TestClientReadTracks(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) + tracks := cloneAndClearTracks(Tracks{track1, track2, track3}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: Tracks{track1, track2, track3}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) @@ -228,13 +230,15 @@ func TestClientRead(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{scheme + "://" + listenIP + ":8554/teststream/"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) @@ -484,12 +488,14 @@ func TestClientReadNoContentBase(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) @@ -590,13 +596,15 @@ func TestClientReadAnyPort(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) @@ -716,13 +724,15 @@ func TestClientReadAutomaticProtocol(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) @@ -847,13 +857,15 @@ func TestClientReadAutomaticProtocol(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) @@ -1072,13 +1084,15 @@ func TestClientReadRedirect(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) @@ -1230,13 +1244,15 @@ func TestClientReadPause(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) @@ -1414,13 +1430,15 @@ func TestClientReadRTCPReport(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) @@ -1590,13 +1608,15 @@ func TestClientReadErrorTimeout(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) @@ -1745,13 +1765,15 @@ func TestClientReadIgnoreTCPInvalidTrack(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) @@ -1875,13 +1897,15 @@ func TestClientReadSeek(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) diff --git a/client_test.go b/client_test.go index fc403b51..b55a48a6 100644 --- a/client_test.go +++ b/client_test.go @@ -72,13 +72,15 @@ func TestClientSession(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, "Session": base.HeaderValue{"123456"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) }() @@ -150,12 +152,14 @@ func TestClientAuth(t *testing.T) { track, err := NewTrackH264(96, []byte("123456"), []byte("123456")) require.NoError(t, err) + tracks := cloneAndClearTracks(Tracks{track}) + err = base.Response{ StatusCode: base.StatusOK, Header: base.Header{ "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: Tracks{track}.Write(), + Body: tracks.Write(), }.Write(bconn.Writer) require.NoError(t, err) }() diff --git a/clientconn.go b/clientconn.go index 8ead131d..1a1240bc 100644 --- a/clientconn.go +++ b/clientconn.go @@ -1090,14 +1090,17 @@ func (cc *ClientConn) doAnnounce(u *base.URL, tracks Tracks) (*base.Response, er // (tested with ffmpeg and gstreamer) baseURL := u.Clone() - // set id, base url and control attribute on tracks + // set ID, base URL, control attribute of tracks for i, t := range tracks { t.ID = i t.BaseURL = baseURL - t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) + + if !t.hasControlAttribute() { + t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ + Key: "control", + Value: "trackID=" + strconv.FormatInt(int64(i), 10), + }) + } } res, err := cc.do(&base.Request{ diff --git a/serverstream.go b/serverstream.go index 44b66fb9..a6c01f6e 100644 --- a/serverstream.go +++ b/serverstream.go @@ -41,11 +41,12 @@ type ServerStream struct { // NewServerStream allocates a ServerStream. func NewServerStream(tracks Tracks) *ServerStream { st := &ServerStream{ - tracks: tracks, readersUnicast: make(map[*ServerSession]struct{}), readers: make(map[*ServerSession]struct{}), } + st.tracks = cloneAndClearTracks(tracks) + st.trackInfos = make([]*trackInfo, len(tracks)) for i := range st.trackInfos { st.trackInfos[i] = &trackInfo{} diff --git a/track.go b/track.go index 217651ab..7ab9f4f7 100644 --- a/track.go +++ b/track.go @@ -26,6 +26,112 @@ type Track struct { Media *psdp.MediaDescription } +func (t *Track) hasControlAttribute() bool { + for _, attr := range t.Media.Attributes { + if attr.Key == "control" { + return true + } + } + return false +} + +// URL returns the track url. +func (t *Track) URL() (*base.URL, error) { + if t.BaseURL == nil { + return nil, fmt.Errorf("empty base url") + } + + controlAttr := func() string { + for _, attr := range t.Media.Attributes { + if attr.Key == "control" { + return attr.Value + } + } + return "" + }() + + // no control attribute, use base URL + if controlAttr == "" { + return t.BaseURL, nil + } + + // control attribute contains an absolute path + if strings.HasPrefix(controlAttr, "rtsp://") { + ur, err := base.ParseURL(controlAttr) + if err != nil { + return nil, err + } + + // copy host and credentials + ur.Host = t.BaseURL.Host + ur.User = t.BaseURL.User + return ur, nil + } + + // control attribute contains a relative control attribute + // 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 +} + +// ClockRate returns the clock rate of the track. +func (t *Track) ClockRate() (int, error) { + if len(t.Media.MediaName.Formats) != 1 { + return 0, fmt.Errorf("invalid format (%v)", t.Media.MediaName.Formats) + } + + // get clock rate from payload type + switch t.Media.MediaName.Formats[0] { + case "0", "1", "2", "3", "4", "5", "7", "8", "9", "12", "13", "15", "18": + return 8000, nil + + case "6": + return 16000, nil + + case "10", "11": + return 44100, nil + + case "14", "25", "26", "28", "31", "32", "33", "34": + return 90000, nil + + case "16": + return 11025, nil + + case "17": + return 22050, nil + } + + // get clock rate from rtpmap + // https://tools.ietf.org/html/rfc4566 + // a=rtpmap: / [/] + for _, a := range t.Media.Attributes { + if a.Key == "rtpmap" { + tmp := strings.Split(a.Value, " ") + if len(tmp) < 2 { + return 0, fmt.Errorf("invalid rtpmap (%v)", a.Value) + } + + tmp = strings.Split(tmp[1], "/") + if len(tmp) != 2 && len(tmp) != 3 { + return 0, fmt.Errorf("invalid rtpmap (%v)", a.Value) + } + + v, err := strconv.ParseInt(tmp[1], 10, 64) + if err != nil { + return 0, err + } + return int(v), nil + } + } + return 0, fmt.Errorf("attribute 'rtpmap' not found") +} + // NewTrackH264 initializes an H264 track from a SPS and PPS. func NewTrackH264(payloadType uint8, sps []byte, pps []byte) (*Track, error) { spropParameterSets := base64.StdEncoding.EncodeToString(sps) + @@ -227,103 +333,6 @@ func (t *Track) ExtractDataAAC() ([]byte, error) { return config, nil } -// ClockRate returns the clock rate of the track. -func (t *Track) ClockRate() (int, error) { - if len(t.Media.MediaName.Formats) != 1 { - return 0, fmt.Errorf("invalid format (%v)", t.Media.MediaName.Formats) - } - - // get clock rate from payload type - switch t.Media.MediaName.Formats[0] { - case "0", "1", "2", "3", "4", "5", "7", "8", "9", "12", "13", "15", "18": - return 8000, nil - - case "6": - return 16000, nil - - case "10", "11": - return 44100, nil - - case "14", "25", "26", "28", "31", "32", "33", "34": - return 90000, nil - - case "16": - return 11025, nil - - case "17": - return 22050, nil - } - - // get clock rate from rtpmap - // https://tools.ietf.org/html/rfc4566 - // a=rtpmap: / [/] - for _, a := range t.Media.Attributes { - if a.Key == "rtpmap" { - tmp := strings.Split(a.Value, " ") - if len(tmp) < 2 { - return 0, fmt.Errorf("invalid rtpmap (%v)", a.Value) - } - - tmp = strings.Split(tmp[1], "/") - if len(tmp) != 2 && len(tmp) != 3 { - return 0, fmt.Errorf("invalid rtpmap (%v)", a.Value) - } - - v, err := strconv.ParseInt(tmp[1], 10, 64) - if err != nil { - return 0, err - } - return int(v), nil - } - } - return 0, fmt.Errorf("attribute 'rtpmap' not found") -} - -// URL returns the track url. -func (t *Track) URL() (*base.URL, error) { - if t.BaseURL == nil { - return nil, fmt.Errorf("empty base url") - } - - controlAttr := func() string { - for _, attr := range t.Media.Attributes { - if attr.Key == "control" { - return attr.Value - } - } - return "" - }() - - // no control attribute, use base URL - if controlAttr == "" { - return t.BaseURL, nil - } - - // control attribute contains an absolute path - if strings.HasPrefix(controlAttr, "rtsp://") { - ur, err := base.ParseURL(controlAttr) - if err != nil { - return nil, err - } - - // copy host and credentials - ur.Host = t.BaseURL.Host - ur.User = t.BaseURL.User - return ur, nil - } - - // control attribute contains a relative control attribute - // 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 -} - // Tracks is a list of tracks. type Tracks []*Track @@ -357,6 +366,43 @@ func ReadTracks(byts []byte, baseURL *base.URL) (Tracks, error) { return tracks, nil } +func cloneAndClearTracks(ts Tracks) Tracks { + ret := make(Tracks, len(ts)) + + for i, track := range ts { + md := &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: track.Media.MediaName.Media, + Protos: []string{"RTP", "AVP"}, // override protocol + Formats: track.Media.MediaName.Formats, + }, + Bandwidth: track.Media.Bandwidth, + Attributes: func() []psdp.Attribute { + var ret []psdp.Attribute + + for _, attr := range track.Media.Attributes { + if attr.Key == "rtpmap" || attr.Key == "fmtp" { + ret = append(ret, attr) + } + } + + ret = append(ret, psdp.Attribute{ + Key: "control", + Value: "trackID=" + strconv.FormatInt(int64(i), 10), + }) + + return ret + }(), + } + + ret[i] = &Track{ + Media: md, + } + } + + return ret +} + // Write encodes tracks into SDP. func (ts Tracks) Write() []byte { sout := &sdp.SessionDescription{ @@ -378,7 +424,7 @@ func (ts Tracks) Write() []byte { }, } - for i, track := range ts { + for _, track := range ts { mout := &psdp.MediaDescription{ MediaName: psdp.MediaName{ Media: track.Media.MediaName.Media, @@ -390,18 +436,11 @@ func (ts Tracks) Write() []byte { var ret []psdp.Attribute for _, attr := range track.Media.Attributes { - if attr.Key == "rtpmap" || attr.Key == "fmtp" { + if attr.Key == "rtpmap" || attr.Key == "fmtp" || attr.Key == "control" { ret = append(ret, attr) } } - // control attribute is the path that is appended - // to the stream path in SETUP - ret = append(ret, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) - return ret }(), } diff --git a/track_test.go b/track_test.go index 9edde8df..3354129d 100644 --- a/track_test.go +++ b/track_test.go @@ -9,83 +9,6 @@ import ( "github.com/aler9/gortsplib/pkg/base" ) -func TestTrackClockRate(t *testing.T) { - for _, ca := range []struct { - name string - sdp []byte - clockRate int - }{ - { - "empty encoding parameters", - []byte("v=0\r\n" + - "o=- 38990265062388 38990265062388 IN IP4 192.168.1.142\r\n" + - "s=RTSP Session\r\n" + - "c=IN IP4 192.168.1.142\r\n" + - "t=0 0\r\n" + - "a=control:*\r\n" + - "a=range:npt=0-\r\n" + - "m=video 0 RTP/AVP 96\r\n" + - "a=rtpmap:96 H264/90000 \r\n" + - "a=range:npt=0-\r\n" + - "a=framerate:0S\r\n" + - "a=fmtp:96 profile-level-id=64000c; packetization-mode=1; sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==\r\n" + - "a=framerate:25\r\n" + - "a=control:trackID=3\r\n"), - 90000, - }, - { - "static payload type 1", - []byte("v=0\r\n" + - "o=- 38990265062388 38990265062388 IN IP4 192.168.1.142\r\n" + - "s=RTSP Session\r\n" + - "c=IN IP4 192.168.1.142\r\n" + - "t=0 0\r\n" + - "a=control:*\r\n" + - "a=range:npt=0-\r\n" + - "m=audio 0 RTP/AVP 8\r\n" + - "a=control:trackID=4"), - 8000, - }, - { - "static payload type 2", - []byte("v=0\r\n" + - "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + - "s=SDP Seminar\r\n" + - "i=A Seminar on the session description protocol\r\n" + - "u=http://www.example.com/seminars/sdp.pdf\r\n" + - "e=j.doe@example.com (Jane Doe)\r\n" + - "p=+1 617 555-6011\r\n" + - "c=IN IP4 224.2.17.12/127\r\n" + - "b=X-YZ:128\r\n" + - "b=AS:12345\r\n" + - "t=2873397496 2873404696\r\n" + - "t=3034423619 3042462419\r\n" + - "r=604800 3600 0 90000\r\n" + - "z=2882844526 -3600 2898848070 0\r\n" + - "k=prompt\r\n" + - "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" + - "a=recvonly\r\n" + - "m=audio 49170 RTP/AVP 0\r\n" + - "i=Vivamus a posuere nisl\r\n" + - "c=IN IP4 203.0.113.1\r\n" + - "b=X-YZ:128\r\n" + - "k=prompt\r\n" + - "a=sendrecv\r\n"), - 8000, - }, - } { - t.Run(ca.name, func(t *testing.T) { - tracks, err := ReadTracks(ca.sdp, nil) - require.NoError(t, err) - - clockRate, err := tracks[0].ClockRate() - require.NoError(t, err) - - require.Equal(t, clockRate, ca.clockRate) - }) - } -} - func TestTrackURL(t *testing.T) { for _, ca := range []struct { name string @@ -194,6 +117,83 @@ func TestTrackURL(t *testing.T) { } } +func TestTrackClockRate(t *testing.T) { + for _, ca := range []struct { + name string + sdp []byte + clockRate int + }{ + { + "empty encoding parameters", + []byte("v=0\r\n" + + "o=- 38990265062388 38990265062388 IN IP4 192.168.1.142\r\n" + + "s=RTSP Session\r\n" + + "c=IN IP4 192.168.1.142\r\n" + + "t=0 0\r\n" + + "a=control:*\r\n" + + "a=range:npt=0-\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000 \r\n" + + "a=range:npt=0-\r\n" + + "a=framerate:0S\r\n" + + "a=fmtp:96 profile-level-id=64000c; packetization-mode=1; sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==\r\n" + + "a=framerate:25\r\n" + + "a=control:trackID=3\r\n"), + 90000, + }, + { + "static payload type 1", + []byte("v=0\r\n" + + "o=- 38990265062388 38990265062388 IN IP4 192.168.1.142\r\n" + + "s=RTSP Session\r\n" + + "c=IN IP4 192.168.1.142\r\n" + + "t=0 0\r\n" + + "a=control:*\r\n" + + "a=range:npt=0-\r\n" + + "m=audio 0 RTP/AVP 8\r\n" + + "a=control:trackID=4"), + 8000, + }, + { + "static payload type 2", + []byte("v=0\r\n" + + "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + + "s=SDP Seminar\r\n" + + "i=A Seminar on the session description protocol\r\n" + + "u=http://www.example.com/seminars/sdp.pdf\r\n" + + "e=j.doe@example.com (Jane Doe)\r\n" + + "p=+1 617 555-6011\r\n" + + "c=IN IP4 224.2.17.12/127\r\n" + + "b=X-YZ:128\r\n" + + "b=AS:12345\r\n" + + "t=2873397496 2873404696\r\n" + + "t=3034423619 3042462419\r\n" + + "r=604800 3600 0 90000\r\n" + + "z=2882844526 -3600 2898848070 0\r\n" + + "k=prompt\r\n" + + "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" + + "a=recvonly\r\n" + + "m=audio 49170 RTP/AVP 0\r\n" + + "i=Vivamus a posuere nisl\r\n" + + "c=IN IP4 203.0.113.1\r\n" + + "b=X-YZ:128\r\n" + + "k=prompt\r\n" + + "a=sendrecv\r\n"), + 8000, + }, + } { + t.Run(ca.name, func(t *testing.T) { + tracks, err := ReadTracks(ca.sdp, nil) + require.NoError(t, err) + + clockRate, err := tracks[0].ClockRate() + require.NoError(t, err) + + require.Equal(t, clockRate, ca.clockRate) + }) + } +} + func TestTrackH264New(t *testing.T) { sps := []byte{ 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,