mirror of
https://github.com/aler9/gortsplib
synced 2025-10-05 15:16:51 +08:00
client: allow to publish tracks with pre-existing control attribute (#48)
This commit is contained in:
@@ -67,13 +67,15 @@ func TestClientReadTracks(t *testing.T) {
|
|||||||
require.Equal(t, base.Describe, req.Method)
|
require.Equal(t, base.Describe, req.Method)
|
||||||
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
|
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track1, track2, track3})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track1, track2, track3}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -228,13 +230,15 @@ func TestClientRead(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Content-Base": base.HeaderValue{scheme + "://" + listenIP + ":8554/teststream/"},
|
"Content-Base": base.HeaderValue{scheme + "://" + listenIP + ":8554/teststream/"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -484,12 +488,14 @@ func TestClientReadNoContentBase(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -590,13 +596,15 @@ func TestClientReadAnyPort(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -716,13 +724,15 @@ func TestClientReadAutomaticProtocol(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -847,13 +857,15 @@ func TestClientReadAutomaticProtocol(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1072,13 +1084,15 @@ func TestClientReadRedirect(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1230,13 +1244,15 @@ func TestClientReadPause(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1414,13 +1430,15 @@ func TestClientReadRTCPReport(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1590,13 +1608,15 @@ func TestClientReadErrorTimeout(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1745,13 +1765,15 @@ func TestClientReadIgnoreTCPInvalidTrack(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1875,13 +1897,15 @@ func TestClientReadSeek(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@@ -72,13 +72,15 @@ func TestClientSession(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
"Session": base.HeaderValue{"123456"},
|
"Session": base.HeaderValue{"123456"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}()
|
}()
|
||||||
@@ -150,12 +152,14 @@ func TestClientAuth(t *testing.T) {
|
|||||||
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
track, err := NewTrackH264(96, []byte("123456"), []byte("123456"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tracks := cloneAndClearTracks(Tracks{track})
|
||||||
|
|
||||||
err = base.Response{
|
err = base.Response{
|
||||||
StatusCode: base.StatusOK,
|
StatusCode: base.StatusOK,
|
||||||
Header: base.Header{
|
Header: base.Header{
|
||||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||||
},
|
},
|
||||||
Body: Tracks{track}.Write(),
|
Body: tracks.Write(),
|
||||||
}.Write(bconn.Writer)
|
}.Write(bconn.Writer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}()
|
}()
|
||||||
|
@@ -1090,15 +1090,18 @@ func (cc *ClientConn) doAnnounce(u *base.URL, tracks Tracks) (*base.Response, er
|
|||||||
// (tested with ffmpeg and gstreamer)
|
// (tested with ffmpeg and gstreamer)
|
||||||
baseURL := u.Clone()
|
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 {
|
for i, t := range tracks {
|
||||||
t.ID = i
|
t.ID = i
|
||||||
t.BaseURL = baseURL
|
t.BaseURL = baseURL
|
||||||
|
|
||||||
|
if !t.hasControlAttribute() {
|
||||||
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
||||||
Key: "control",
|
Key: "control",
|
||||||
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res, err := cc.do(&base.Request{
|
res, err := cc.do(&base.Request{
|
||||||
Method: base.Announce,
|
Method: base.Announce,
|
||||||
|
@@ -41,11 +41,12 @@ type ServerStream struct {
|
|||||||
// NewServerStream allocates a ServerStream.
|
// NewServerStream allocates a ServerStream.
|
||||||
func NewServerStream(tracks Tracks) *ServerStream {
|
func NewServerStream(tracks Tracks) *ServerStream {
|
||||||
st := &ServerStream{
|
st := &ServerStream{
|
||||||
tracks: tracks,
|
|
||||||
readersUnicast: make(map[*ServerSession]struct{}),
|
readersUnicast: make(map[*ServerSession]struct{}),
|
||||||
readers: make(map[*ServerSession]struct{}),
|
readers: make(map[*ServerSession]struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
st.tracks = cloneAndClearTracks(tracks)
|
||||||
|
|
||||||
st.trackInfos = make([]*trackInfo, len(tracks))
|
st.trackInfos = make([]*trackInfo, len(tracks))
|
||||||
for i := range st.trackInfos {
|
for i := range st.trackInfos {
|
||||||
st.trackInfos[i] = &trackInfo{}
|
st.trackInfos[i] = &trackInfo{}
|
||||||
|
251
track.go
251
track.go
@@ -26,6 +26,112 @@ type Track struct {
|
|||||||
Media *psdp.MediaDescription
|
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:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
|
||||||
|
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.
|
// NewTrackH264 initializes an H264 track from a SPS and PPS.
|
||||||
func NewTrackH264(payloadType uint8, sps []byte, pps []byte) (*Track, error) {
|
func NewTrackH264(payloadType uint8, sps []byte, pps []byte) (*Track, error) {
|
||||||
spropParameterSets := base64.StdEncoding.EncodeToString(sps) +
|
spropParameterSets := base64.StdEncoding.EncodeToString(sps) +
|
||||||
@@ -227,103 +333,6 @@ func (t *Track) ExtractDataAAC() ([]byte, error) {
|
|||||||
return config, nil
|
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:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
|
|
||||||
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.
|
// Tracks is a list of tracks.
|
||||||
type Tracks []*Track
|
type Tracks []*Track
|
||||||
|
|
||||||
@@ -357,6 +366,43 @@ func ReadTracks(byts []byte, baseURL *base.URL) (Tracks, error) {
|
|||||||
return tracks, nil
|
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.
|
// Write encodes tracks into SDP.
|
||||||
func (ts Tracks) Write() []byte {
|
func (ts Tracks) Write() []byte {
|
||||||
sout := &sdp.SessionDescription{
|
sout := &sdp.SessionDescription{
|
||||||
@@ -378,7 +424,7 @@ func (ts Tracks) Write() []byte {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, track := range ts {
|
for _, track := range ts {
|
||||||
mout := &psdp.MediaDescription{
|
mout := &psdp.MediaDescription{
|
||||||
MediaName: psdp.MediaName{
|
MediaName: psdp.MediaName{
|
||||||
Media: track.Media.MediaName.Media,
|
Media: track.Media.MediaName.Media,
|
||||||
@@ -390,18 +436,11 @@ func (ts Tracks) Write() []byte {
|
|||||||
var ret []psdp.Attribute
|
var ret []psdp.Attribute
|
||||||
|
|
||||||
for _, attr := range track.Media.Attributes {
|
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)
|
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
|
return ret
|
||||||
}(),
|
}(),
|
||||||
}
|
}
|
||||||
|
154
track_test.go
154
track_test.go
@@ -9,83 +9,6 @@ import (
|
|||||||
"github.com/aler9/gortsplib/pkg/base"
|
"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) {
|
func TestTrackURL(t *testing.T) {
|
||||||
for _, ca := range []struct {
|
for _, ca := range []struct {
|
||||||
name string
|
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) {
|
func TestTrackH264New(t *testing.T) {
|
||||||
sps := []byte{
|
sps := []byte{
|
||||||
0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,
|
0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,
|
||||||
|
Reference in New Issue
Block a user