diff --git a/client.go b/client.go index ff0fc90e..d7d39cde 100644 --- a/client.go +++ b/client.go @@ -21,8 +21,6 @@ import ( "sync/atomic" "time" - psdp "github.com/pion/sdp/v3" - "github.com/aler9/gortsplib/pkg/auth" "github.com/aler9/gortsplib/pkg/base" "github.com/aler9/gortsplib/pkg/headers" @@ -53,7 +51,7 @@ const ( ) type clientTrack struct { - track *Track + track Track udpRTPListener *clientUDPListener udpRTCPListener *clientUDPListener tcpChannel int @@ -97,7 +95,7 @@ type announceReq struct { type setupReq struct { forPlay bool - track *Track + track Track baseURL *base.URL rtpPort int rtcpPort int @@ -1177,14 +1175,7 @@ func (c *Client) doAnnounce(u *base.URL, tracks Tracks) (*base.Response, error) // (tested with ffmpeg and gstreamer) baseURL := u.Clone() - for i, t := range tracks { - if !t.hasControlAttribute() { - t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) - } - } + tracks.setControls() res, err := c.do(&base.Request{ Method: base.Announce, @@ -1225,7 +1216,7 @@ func (c *Client) Announce(u *base.URL, tracks Tracks) (*base.Response, error) { func (c *Client) doSetup( forPlay bool, - track *Track, + track Track, baseURL *base.URL, rtpPort int, rtcpPort int) (*base.Response, error) { @@ -1330,7 +1321,7 @@ func (c *Client) doSetup( th.InterleavedIDs = &[2]int{(trackID * 2), (trackID * 2) + 1} } - trackURL, err := track.URL(baseURL) + trackURL, err := track.url(baseURL) if err != nil { if proto == TransportUDP { rtpListener.close() @@ -1444,11 +1435,11 @@ func (c *Client) doSetup( } } - clockRate, _ := track.ClockRate() cct := clientTrack{ track: track, } + clockRate := track.ClockRate() if mode == headers.TransportModePlay { c.state = clientStatePrePlay cct.rtcpReceiver = rtcpreceiver.New(nil, clockRate) @@ -1555,7 +1546,7 @@ func (c *Client) doSetup( // if rtpPort and rtcpPort are zero, they are chosen automatically. func (c *Client) Setup( forPlay bool, - track *Track, + track Track, baseURL *base.URL, rtpPort int, rtcpPort int) (*base.Response, error) { diff --git a/client_publish_test.go b/client_publish_test.go index fb7dbb0d..7e9a6b36 100644 --- a/client_publish_test.go +++ b/client_publish_test.go @@ -201,9 +201,7 @@ func TestClientPublishSerial(t *testing.T) { }, } - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) err = c.StartPublishing(scheme+"://localhost:8554/teststream", @@ -360,9 +358,7 @@ func TestClientPublishParallel(t *testing.T) { }(), } - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) writerDone := make(chan struct{}) @@ -527,9 +523,7 @@ func TestClientPublishPauseSerial(t *testing.T) { }(), } - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) err = c.StartPublishing("rtsp://localhost:8554/teststream", @@ -668,9 +662,7 @@ func TestClientPublishPauseParallel(t *testing.T) { }(), } - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) err = c.StartPublishing("rtsp://localhost:8554/teststream", @@ -815,9 +807,7 @@ func TestClientPublishAutomaticProtocol(t *testing.T) { require.NoError(t, err) }() - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) c := Client{} @@ -960,9 +950,7 @@ func TestClientPublishRTCPReport(t *testing.T) { udpSenderReportPeriod: 1 * time.Second, } - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) err = c.StartPublishing("rtsp://localhost:8554/teststream", @@ -1106,9 +1094,7 @@ func TestClientPublishIgnoreTCPRTPPackets(t *testing.T) { }, } - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) err = c.StartPublishing("rtsp://localhost:8554/teststream", diff --git a/client_read_test.go b/client_read_test.go index 473d3013..fc9a1de0 100644 --- a/client_read_test.go +++ b/client_read_test.go @@ -13,7 +13,6 @@ import ( "github.com/pion/rtcp" "github.com/pion/rtp" - psdp "github.com/pion/sdp/v3" "github.com/stretchr/testify/require" "golang.org/x/net/ipv4" @@ -24,15 +23,13 @@ import ( ) func TestClientReadTracks(t *testing.T) { - track1, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - track2, err := NewTrackAAC(96, &TrackConfigAAC{Type: 2, SampleRate: 44100, ChannelCount: 2}) + track2, err := NewTrackAAC(96, 2, 44100, 2, nil) require.NoError(t, err) - track3, err := NewTrackAAC(96, &TrackConfigAAC{Type: 2, SampleRate: 96000, ChannelCount: 2}) + track3, err := NewTrackAAC(96, 2, 96000, 2, nil) require.NoError(t, err) l, err := net.Listen("tcp", "localhost:8554") @@ -72,7 +69,8 @@ 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}) + tracks := Tracks{track1, track2, track3} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -144,32 +142,7 @@ func TestClientReadTracks(t *testing.T) { require.NoError(t, err) defer c.Close() - track1.Media.Attributes = append(track1.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=0", - }) - - track2.Media.Attributes = append(track2.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=1", - }) - - track3.Media.Attributes = append(track3.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=2", - }) - - require.Equal(t, Tracks{ - { - Media: track1.Media, - }, - { - Media: track2.Media, - }, - { - Media: track3.Media, - }, - }, c.Tracks()) + require.Equal(t, Tracks{track1, track2, track3}, c.Tracks()) } func TestClientRead(t *testing.T) { @@ -233,15 +206,11 @@ func TestClientRead(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL(scheme+"://"+listenIP+":8554/test/stream?param=value"), req.URL) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=0", - }) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -249,7 +218,7 @@ func TestClientRead(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{scheme + "://" + listenIP + ":8554/test/stream?param=value/"}, }, - Body: Tracks{track}.Write(false), + Body: tracks.Write(false), }.Write(&bb) _, err = conn.Write(bb.Bytes()) require.NoError(t, err) @@ -498,15 +467,11 @@ func TestClientReadNonStandardFrameSize(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=0", - }) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -514,7 +479,7 @@ func TestClientReadNonStandardFrameSize(t *testing.T) { "Content-Type": base.HeaderValue{"application/sdp"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, }, - Body: Tracks{track}.Write(false), + Body: tracks.Write(false), }.Write(&bb) _, err = conn.Write(bb.Bytes()) require.NoError(t, err) @@ -606,17 +571,14 @@ func TestClientReadPartial(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL("rtsp://"+listenIP+":8554/teststream"), req.URL) - track1, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - track2, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track2, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track1, track2}) + tracks := Tracks{track1, track2} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -758,12 +720,11 @@ func TestClientReadNoContentBase(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -879,12 +840,11 @@ func TestClientReadAnyPort(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -1037,12 +997,11 @@ func TestClientReadAutomaticProtocol(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -1177,12 +1136,11 @@ func TestClientReadAutomaticProtocol(t *testing.T) { err = v.ValidateRequest(req) require.NoError(t, err) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -1394,12 +1352,11 @@ func TestClientReadDifferentInterleavedIDs(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) - track1, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track1}) + tracks := Tracks{track1} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -1562,12 +1519,11 @@ func TestClientReadRedirect(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -1728,12 +1684,11 @@ func TestClientReadPause(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -1908,12 +1863,11 @@ func TestClientReadRTCPReport(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -2087,12 +2041,11 @@ func TestClientReadErrorTimeout(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -2243,12 +2196,11 @@ func TestClientReadIgnoreTCPInvalidTrack(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -2378,12 +2330,11 @@ func TestClientReadSeek(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -2559,12 +2510,11 @@ func TestClientReadKeepaliveFromSession(t *testing.T) { require.NoError(t, err) require.Equal(t, base.Describe, req.Method) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, diff --git a/client_test.go b/client_test.go index 05e25267..277bc14b 100644 --- a/client_test.go +++ b/client_test.go @@ -120,12 +120,11 @@ func TestClientSession(t *testing.T) { require.Equal(t, base.HeaderValue{"123456"}, req.Header["Session"]) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -208,12 +207,11 @@ func TestClientAuth(t *testing.T) { err = v.ValidateRequest(req) require.NoError(t, err) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - tracks := cloneAndClearTracks(Tracks{track}) + tracks := Tracks{track} + tracks.setControls() base.Response{ StatusCode: base.StatusOK, @@ -278,9 +276,7 @@ func TestClientDescribeCharset(t *testing.T) { require.Equal(t, base.Describe, req.Method) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) - track1, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) base.Response{ diff --git a/examples/client-publish-aac/main.go b/examples/client-publish-aac/main.go index d16ef0ae..7f0fbc86 100644 --- a/examples/client-publish-aac/main.go +++ b/examples/client-publish-aac/main.go @@ -34,7 +34,7 @@ func main() { fmt.Println("stream connected") // create an AAC track - track, err := gortsplib.NewTrackAAC(96, &gortsplib.TrackConfigAAC{Type: 2, SampleRate: 48000, ChannelCount: 2}) + track, err := gortsplib.NewTrackAAC(96, 2, 48000, 2, nil) if err != nil { panic(err) } diff --git a/examples/client-publish-h264/main.go b/examples/client-publish-h264/main.go index 77c4cbd6..0a405533 100644 --- a/examples/client-publish-h264/main.go +++ b/examples/client-publish-h264/main.go @@ -35,7 +35,7 @@ func main() { fmt.Println("stream connected") // create an H264 track - track, err := gortsplib.NewTrackH264(96, &gortsplib.TrackConfigH264{sps, pps}) + track, err := gortsplib.NewTrackH264(96, sps, pps) if err != nil { panic(err) } diff --git a/examples/client-publish-options/main.go b/examples/client-publish-options/main.go index 774b9613..b02ed834 100644 --- a/examples/client-publish-options/main.go +++ b/examples/client-publish-options/main.go @@ -36,7 +36,7 @@ func main() { fmt.Println("stream connected") // create an H264 track - track, err := gortsplib.NewTrackH264(96, &gortsplib.TrackConfigH264{sps, pps}) + track, err := gortsplib.NewTrackH264(96, sps, pps) if err != nil { panic(err) } diff --git a/examples/client-publish-opus/main.go b/examples/client-publish-opus/main.go index f213212a..2429755b 100644 --- a/examples/client-publish-opus/main.go +++ b/examples/client-publish-opus/main.go @@ -34,7 +34,7 @@ func main() { fmt.Println("stream connected") // create an Opus track - track, err := gortsplib.NewTrackOpus(96, &gortsplib.TrackConfigOpus{SampleRate: 48000, ChannelCount: 2}) + track, err := gortsplib.NewTrackOpus(96, 48000, 2) if err != nil { panic(err) } diff --git a/examples/client-publish-pause/main.go b/examples/client-publish-pause/main.go index 1d04a29d..97af8575 100644 --- a/examples/client-publish-pause/main.go +++ b/examples/client-publish-pause/main.go @@ -37,7 +37,7 @@ func main() { fmt.Println("stream connected") // create an H264 track - track, err := gortsplib.NewTrackH264(96, &gortsplib.TrackConfigH264{sps, pps}) + track, err := gortsplib.NewTrackH264(96, sps, pps) if err != nil { panic(err) } diff --git a/examples/client-read-aac/main.go b/examples/client-read-aac/main.go index 9c708bfc..105632c5 100644 --- a/examples/client-read-aac/main.go +++ b/examples/client-read-aac/main.go @@ -43,9 +43,11 @@ func main() { } // find the AAC track + var clockRate int aacTrack := func() int { for i, track := range tracks { - if track.IsAAC() { + if _, ok := track.(*gortsplib.TrackAAC); ok { + clockRate = track.ClockRate() return i } } @@ -55,14 +57,8 @@ func main() { panic("AAC track not found") } - // get track config - aacConf, err := tracks[aacTrack].ExtractConfigAAC() - if err != nil { - panic(err) - } - // setup decoder - dec := rtpaac.NewDecoder(aacConf.SampleRate) + dec := rtpaac.NewDecoder(clockRate) // called when a RTP packet arrives c.OnPacketRTP = func(trackID int, payload []byte) { diff --git a/examples/client-read-h264-save-to-disk/main.go b/examples/client-read-h264-save-to-disk/main.go index ac17fc57..b26f8be4 100644 --- a/examples/client-read-h264-save-to-disk/main.go +++ b/examples/client-read-h264-save-to-disk/main.go @@ -41,9 +41,13 @@ func main() { } // find the H264 track + var sps []byte + var pps []byte h264Track := func() int { for i, track := range tracks { - if track.IsH264() { + if h264t, ok := track.(*gortsplib.TrackH264); ok { + sps = h264t.SPS() + pps = h264t.PPS() return i } } @@ -53,17 +57,11 @@ func main() { panic("H264 track not found") } - // get track config - h264Conf, err := tracks[h264Track].ExtractConfigH264() - if err != nil { - panic(err) - } - // setup decoder dec := rtph264.NewDecoder() // setup encoder - enc, err := newMPEGTSEncoder(h264Conf) + enc, err := newMPEGTSEncoder(sps, pps) if err != nil { panic(err) } diff --git a/examples/client-read-h264-save-to-disk/mpegtsencoder.go b/examples/client-read-h264-save-to-disk/mpegtsencoder.go index 6d888a98..5bd7791c 100644 --- a/examples/client-read-h264-save-to-disk/mpegtsencoder.go +++ b/examples/client-read-h264-save-to-disk/mpegtsencoder.go @@ -7,14 +7,14 @@ import ( "os" "time" - "github.com/aler9/gortsplib" "github.com/aler9/gortsplib/pkg/h264" "github.com/asticode/go-astits" ) // mpegtsEncoder allows to encode H264 NALUs into MPEG-TS. type mpegtsEncoder struct { - h264Conf *gortsplib.TrackConfigH264 + sps []byte + pps []byte f *os.File b *bufio.Writer @@ -25,7 +25,11 @@ type mpegtsEncoder struct { } // newMPEGTSEncoder allocates a mpegtsEncoder. -func newMPEGTSEncoder(h264Conf *gortsplib.TrackConfigH264) (*mpegtsEncoder, error) { +func newMPEGTSEncoder(sps []byte, pps []byte) (*mpegtsEncoder, error) { + if sps == nil || pps == nil { + return nil, fmt.Errorf("SPS or PPS not provided") + } + f, err := os.Create("mystream.ts") if err != nil { return nil, err @@ -40,11 +44,12 @@ func newMPEGTSEncoder(h264Conf *gortsplib.TrackConfigH264) (*mpegtsEncoder, erro mux.SetPCRPID(256) return &mpegtsEncoder{ - h264Conf: h264Conf, - f: f, - b: b, - mux: mux, - dtsEst: h264.NewDTSEstimator(), + sps: sps, + pps: pps, + f: f, + b: b, + mux: mux, + dtsEst: h264.NewDTSEstimator(), }, nil } @@ -87,7 +92,7 @@ func (e *mpegtsEncoder) encode(nalus [][]byte, pts time.Duration) error { case h264.NALUTypeIDR: // add SPS and PPS before every IDR - filteredNALUs = append(filteredNALUs, e.h264Conf.SPS, e.h264Conf.PPS) + filteredNALUs = append(filteredNALUs, e.sps, e.pps) } filteredNALUs = append(filteredNALUs, nalu) diff --git a/examples/client-read-h264/main.go b/examples/client-read-h264/main.go index 1b731f6b..cc0a92e6 100644 --- a/examples/client-read-h264/main.go +++ b/examples/client-read-h264/main.go @@ -45,7 +45,7 @@ func main() { // find the H264 track h264Track := func() int { for i, track := range tracks { - if track.IsH264() { + if _, ok := track.(*gortsplib.TrackH264); ok { return i } } diff --git a/examples/client-read-partial/main.go b/examples/client-read-partial/main.go index 402ca374..c53aad09 100644 --- a/examples/client-read-partial/main.go +++ b/examples/client-read-partial/main.go @@ -45,9 +45,9 @@ func main() { panic(err) } - // setup only video tracks, skipping audio or application tracks + // setup only H264 tracks, skipping audio or application tracks for _, t := range tracks { - if t.Media.MediaName.Media == "video" { + if _, ok := t.(*gortsplib.TrackH264); ok { _, err := c.Setup(true, t, baseURL, 0, 0) if err != nil { panic(err) diff --git a/server_publish_test.go b/server_publish_test.go index a53b4e22..e44a8db7 100644 --- a/server_publish_test.go +++ b/server_publish_test.go @@ -5,7 +5,6 @@ import ( "bytes" "crypto/tls" "net" - "strconv" "testing" "time" @@ -27,14 +26,9 @@ func invalidURLAnnounceReq(t *testing.T, control string) base.Request { "Content-Type": base.HeaderValue{"application/sdp"}, }, Body: func() []byte { - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: control, - }) + track.setControl(control) sout := &psdp.SessionDescription{ SessionName: psdp.SessionName("Stream"), @@ -48,7 +42,7 @@ func invalidURLAnnounceReq(t *testing.T, control string) base.Request { {Timing: psdp.Timing{0, 0}}, //nolint:govet }, MediaDescriptions: []*psdp.MediaDescription{ - track.Media, + track.mediaDescription(), }, } @@ -236,6 +230,9 @@ func TestServerPublishSetupPath(t *testing.T) { s := &Server{ Handler: &testServerHandler{ onAnnounce: func(ctx *ServerHandlerOnAnnounceCtx) (*base.Response, error) { + // make sure that track URLs are not overridden by NewServerStream() + NewServerStream(ctx.Tracks) + return &base.Response{ StatusCode: base.StatusOK, }, nil @@ -260,14 +257,10 @@ func TestServerPublishSetupPath(t *testing.T) { defer conn.Close() br := bufio.NewReader(conn) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: ca.control, - }) + + track.setControl(ca.control) sout := &psdp.SessionDescription{ SessionName: psdp.SessionName("Stream"), @@ -281,7 +274,7 @@ func TestServerPublishSetupPath(t *testing.T) { {Timing: psdp.Timing{0, 0}}, //nolint:govet }, MediaDescriptions: []*psdp.MediaDescription{ - track.Media, + track.mediaDescription(), }, } @@ -362,18 +355,11 @@ func TestServerPublishErrorSetupDifferentPaths(t *testing.T) { defer conn.Close() br := bufio.NewReader(conn) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) tracks := Tracks{track} - for i, t := range tracks { - t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) - } + tracks.setControls() res, err := writeReqReadRes(conn, br, base.Request{ Method: base.Announce, @@ -451,18 +437,11 @@ func TestServerPublishErrorSetupTrackTwice(t *testing.T) { defer conn.Close() br := bufio.NewReader(conn) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) tracks := Tracks{track} - for i, t := range tracks { - t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) - } + tracks.setControls() res, err := writeReqReadRes(conn, br, base.Request{ Method: base.Announce, @@ -557,23 +536,14 @@ func TestServerPublishErrorRecordPartialTracks(t *testing.T) { defer conn.Close() br := bufio.NewReader(conn) - track1, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - track2, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track2, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) tracks := Tracks{track1, track2} - for i, t := range tracks { - t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) - } + tracks.setControls() res, err := writeReqReadRes(conn, br, base.Request{ Method: base.Announce, @@ -715,18 +685,11 @@ func TestServerPublish(t *testing.T) { <-connOpened - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) tracks := Tracks{track} - for i, t := range tracks { - t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) - } + tracks.setControls() res, err := writeReqReadRes(conn, br, base.Request{ Method: base.Announce, @@ -914,15 +877,11 @@ func TestServerPublishNonStandardFrameSize(t *testing.T) { br := bufio.NewReader(conn) var bb bytes.Buffer - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=0", - }) + tracks := Tracks{track} + tracks.setControls() res, err := writeReqReadRes(conn, br, base.Request{ Method: base.Announce, @@ -931,7 +890,7 @@ func TestServerPublishNonStandardFrameSize(t *testing.T) { "CSeq": base.HeaderValue{"1"}, "Content-Type": base.HeaderValue{"application/sdp"}, }, - Body: Tracks{track}.Write(false), + Body: tracks.Write(false), }) require.NoError(t, err) require.Equal(t, base.StatusOK, res.StatusCode) @@ -1023,18 +982,11 @@ func TestServerPublishErrorInvalidProtocol(t *testing.T) { br := bufio.NewReader(conn) var bb bytes.Buffer - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) tracks := Tracks{track} - for i, t := range tracks { - t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) - } + tracks.setControls() res, err := writeReqReadRes(conn, br, base.Request{ Method: base.Announce, @@ -1134,18 +1086,11 @@ func TestServerPublishRTCPReport(t *testing.T) { defer conn.Close() br := bufio.NewReader(conn) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) tracks := Tracks{track} - for i, t := range tracks { - t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) - } + tracks.setControls() res, err := writeReqReadRes(conn, br, base.Request{ Method: base.Announce, @@ -1316,18 +1261,11 @@ func TestServerPublishTimeout(t *testing.T) { defer conn.Close() br := bufio.NewReader(conn) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) tracks := Tracks{track} - for i, t := range tracks { - t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) - } + tracks.setControls() res, err := writeReqReadRes(conn, br, base.Request{ Method: base.Announce, @@ -1450,18 +1388,11 @@ func TestServerPublishWithoutTeardown(t *testing.T) { require.NoError(t, err) br := bufio.NewReader(conn) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) tracks := Tracks{track} - for i, t := range tracks { - t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) - } + tracks.setControls() res, err := writeReqReadRes(conn, br, base.Request{ Method: base.Announce, @@ -1576,18 +1507,11 @@ func TestServerPublishUDPChangeConn(t *testing.T) { defer conn.Close() br := bufio.NewReader(conn) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) tracks := Tracks{track} - for i, t := range tracks { - t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) - } + tracks.setControls() res, err := writeReqReadRes(conn, br, base.Request{ Method: base.Announce, diff --git a/server_read_test.go b/server_read_test.go index acad7c0d..276ad47f 100644 --- a/server_read_test.go +++ b/server_read_test.go @@ -91,9 +91,7 @@ func TestServerReadSetupPath(t *testing.T) { }, } { t.Run(ca.name, func(t *testing.T) { - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track, track, track, track, track}) @@ -152,9 +150,7 @@ func TestServerReadSetupErrors(t *testing.T) { t.Run(ca, func(t *testing.T) { connClosed := make(chan struct{}) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -262,9 +258,7 @@ func TestServerRead(t *testing.T) { sessionClosed := make(chan struct{}) framesReceived := make(chan struct{}) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -572,9 +566,7 @@ func TestServerRead(t *testing.T) { } func TestServerReadVLCMulticast(t *testing.T) { - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -622,9 +614,7 @@ func TestServerReadVLCMulticast(t *testing.T) { } func TestServerReadNonStandardFrameSize(t *testing.T) { - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -711,9 +701,7 @@ func TestServerReadTCPResponseBeforeFrames(t *testing.T) { writerDone := make(chan struct{}) writerTerminate := make(chan struct{}) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -811,9 +799,7 @@ func TestServerReadTCPResponseBeforeFrames(t *testing.T) { } func TestServerReadPlayPlay(t *testing.T) { - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -899,9 +885,7 @@ func TestServerReadPlayPausePlay(t *testing.T) { writerDone := make(chan struct{}) writerTerminate := make(chan struct{}) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -1023,9 +1007,7 @@ func TestServerReadPlayPausePause(t *testing.T) { writerDone := make(chan struct{}) writerTerminate := make(chan struct{}) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -1157,9 +1139,7 @@ func TestServerReadTimeout(t *testing.T) { t.Run(transport, func(t *testing.T) { sessionClosed := make(chan struct{}) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -1255,9 +1235,7 @@ func TestServerReadWithoutTeardown(t *testing.T) { connClosed := make(chan struct{}) sessionClosed := make(chan struct{}) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -1359,9 +1337,7 @@ func TestServerReadWithoutTeardown(t *testing.T) { } func TestServerReadUDPChangeConn(t *testing.T) { - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -1463,14 +1439,10 @@ func TestServerReadUDPChangeConn(t *testing.T) { } func TestServerReadPartialTracks(t *testing.T) { - track1, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) - track2, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track2, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track1, track2}) @@ -1643,9 +1615,7 @@ func TestServerReadAdditionalInfos(t *testing.T) { return &ri, ssrcs } - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track, track}) @@ -1765,9 +1735,7 @@ func TestServerReadAdditionalInfos(t *testing.T) { } func TestServerReadErrorUDPSamePorts(t *testing.T) { - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) diff --git a/server_test.go b/server_test.go index 49c31f99..e80abd9b 100644 --- a/server_test.go +++ b/server_test.go @@ -13,7 +13,6 @@ import ( "testing" "time" - psdp "github.com/pion/sdp/v3" "github.com/stretchr/testify/require" "github.com/aler9/gortsplib/pkg/base" @@ -690,9 +689,7 @@ func TestServerErrorInvalidMethod(t *testing.T) { } func TestServerErrorTCPTwoConnOneSession(t *testing.T) { - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -794,9 +791,7 @@ func TestServerErrorTCPTwoConnOneSession(t *testing.T) { } func TestServerErrorTCPOneConnTwoSessions(t *testing.T) { - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -1064,9 +1059,7 @@ func TestServerSessionClose(t *testing.T) { func TestServerSessionAutoClose(t *testing.T) { sessionClosed := make(chan struct{}) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -1133,9 +1126,7 @@ func TestServerErrorInvalidPath(t *testing.T) { t.Run(string(method), func(t *testing.T) { connClosed := make(chan struct{}) - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) stream := NewServerStream(Tracks{track}) @@ -1177,18 +1168,11 @@ func TestServerErrorInvalidPath(t *testing.T) { sxID := "" if method == base.Record { - track, err := NewTrackH264(96, &TrackConfigH264{ - []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}, - }) + track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04}) require.NoError(t, err) tracks := Tracks{track} - for i, t := range tracks { - t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{ - Key: "control", - Value: "trackID=" + strconv.FormatInt(int64(i), 10), - }) - } + tracks.setControls() res, err := writeReqReadRes(conn, br, base.Request{ Method: base.Announce, diff --git a/serversession.go b/serversession.go index 45ab8dee..efd99a50 100644 --- a/serversession.go +++ b/serversession.go @@ -76,7 +76,7 @@ func setupGetTrackIDPathQuery( } for trackID, track := range announcedTracks { - u, _ := track.track.URL(setuppedBaseURL) + u, _ := track.track.url(setuppedBaseURL) if u.String() == url.String() { return trackID, *setuppedPath, *setuppedQuery, nil } @@ -150,7 +150,7 @@ type ServerSessionSetuppedTrack struct { // ServerSessionAnnouncedTrack is an announced track of a ServerSession. type ServerSessionAnnouncedTrack struct { - track *Track + track Track rtcpReceiver *rtcpreceiver.RTCPReceiver } @@ -508,7 +508,7 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base } for _, track := range tracks { - trackURL, err := track.URL(req.URL) + trackURL, err := track.url(req.URL) if err != nil { return &base.Response{ StatusCode: base.StatusBadRequest, @@ -795,8 +795,8 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base ss.setuppedTracks[trackID] = sst if ss.state == ServerSessionStatePrePublish && *ss.setuppedTransport != TransportTCP { - clockRate, _ := ss.announcedTracks[trackID].track.ClockRate() - ss.announcedTracks[trackID].rtcpReceiver = rtcpreceiver.New(nil, clockRate) + ss.announcedTracks[trackID].rtcpReceiver = rtcpreceiver.New(nil, + ss.announcedTracks[trackID].track.ClockRate()) } res.Header["Transport"] = th.Write() diff --git a/serverstream.go b/serverstream.go index e80502cc..4994e6a3 100644 --- a/serverstream.go +++ b/serverstream.go @@ -34,13 +34,15 @@ type ServerStream struct { // NewServerStream allocates a ServerStream. func NewServerStream(tracks Tracks) *ServerStream { + tracks = tracks.clone() + tracks.setControls() + 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{} @@ -90,14 +92,13 @@ func (st *ServerStream) ssrc(trackID int) uint32 { func (st *ServerStream) timestamp(trackID int) uint32 { lastTimeRTP := atomic.LoadUint32(&st.trackInfos[trackID].lastTimeRTP) lastTimeNTP := atomic.LoadInt64(&st.trackInfos[trackID].lastTimeNTP) - clockRate, _ := st.tracks[trackID].ClockRate() if lastTimeRTP == 0 || lastTimeNTP == 0 { return 0 } return uint32(uint64(lastTimeRTP) + - uint64(time.Since(time.Unix(lastTimeNTP, 0)).Seconds()*float64(clockRate))) + uint64(time.Since(time.Unix(lastTimeNTP, 0)).Seconds()*float64(st.tracks[trackID].ClockRate()))) } func (st *ServerStream) lastSequenceNumber(trackID int) uint16 { diff --git a/track.go b/track.go index ed18ee5a..d2754058 100644 --- a/track.go +++ b/track.go @@ -8,47 +8,83 @@ import ( psdp "github.com/pion/sdp/v3" "github.com/aler9/gortsplib/pkg/base" - "github.com/aler9/gortsplib/pkg/sdp" ) // Track is a RTSP track. -type Track struct { - // attributes in SDP format - Media *psdp.MediaDescription +type Track interface { + // ClockRate returns the track clock rate. + ClockRate() int + clone() Track + getControl() string + setControl(string) + url(*base.URL) (*base.URL, error) + mediaDescription() *psdp.MediaDescription } -func (t *Track) hasControlAttribute() bool { - for _, attr := range t.Media.Attributes { - if attr.Key == "control" { - return true +func newTrackFromMediaDescription(md *psdp.MediaDescription) (Track, error) { + if md.MediaName.Media == "video" { + if rtpmap, ok := md.Attribute("rtpmap"); ok { + rtpmap = strings.TrimSpace(rtpmap) + + if vals := strings.Split(rtpmap, " "); len(vals) == 2 && vals[1] == "H264/90000" { + tmp, err := strconv.ParseInt(vals[0], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid payload type '%s'", vals[0]) + } + payloadType := uint8(tmp) + + return newTrackH264FromMediaDescription(payloadType, md) + } } } - return false + + if md.MediaName.Media == "audio" { + if rtpmap, ok := md.Attribute("rtpmap"); ok { + if vals := strings.Split(rtpmap, " "); len(vals) == 2 { + tmp, err := strconv.ParseInt(vals[0], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid payload type '%s'", vals[0]) + } + payloadType := uint8(tmp) + + if strings.HasPrefix(strings.ToLower(vals[1]), "mpeg4-generic/") { + return newTrackAACFromMediaDescription(payloadType, md) + } + + if strings.HasPrefix(vals[1], "opus/") { + return newTrackOpusFromMediaDescription(payloadType, md) + } + } + } + } + + return newTrackGenericFromMediaDescription(md) } -// URL returns the track URL. -func (t *Track) URL(contentBase *base.URL) (*base.URL, error) { +func trackFindControl(md *psdp.MediaDescription) string { + for _, attr := range md.Attributes { + if attr.Key == "control" { + return attr.Value + } + } + return "" +} + +func trackURL(t Track, contentBase *base.URL) (*base.URL, error) { if contentBase == nil { return nil, fmt.Errorf("no Content-Base header provided") } - controlAttr := func() string { - for _, attr := range t.Media.Attributes { - if attr.Key == "control" { - return attr.Value - } - } - return "" - }() + control := t.getControl() // no control attribute, use base URL - if controlAttr == "" { + if control == "" { return contentBase, nil } // control attribute contains an absolute path - if strings.HasPrefix(controlAttr, "rtsp://") { - ur, err := base.ParseURL(controlAttr) + if strings.HasPrefix(control, "rtsp://") { + ur, err := base.ParseURL(control) if err != nil { return nil, err } @@ -64,163 +100,10 @@ func (t *Track) URL(contentBase *base.URL) (*base.URL, error) { // if there's a query, insert it after the query // otherwise insert it after the path strURL := contentBase.String() - if controlAttr[0] != '?' && !strings.HasSuffix(strURL, "/") { + if control[0] != '?' && !strings.HasSuffix(strURL, "/") { strURL += "/" } - ur, _ := base.ParseURL(strURL + controlAttr) + + ur, _ := base.ParseURL(strURL + control) 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("no formats provided") - } - - // 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") -} - -// Tracks is a list of tracks. -type Tracks []*Track - -// ReadTracks decodes tracks from SDP. -func ReadTracks(byts []byte) (Tracks, error) { - var desc sdp.SessionDescription - err := desc.Unmarshal(byts) - if err != nil { - return nil, err - } - - tracks := make(Tracks, len(desc.MediaDescriptions)) - - for i, media := range desc.MediaDescriptions { - tracks[i] = &Track{ - Media: media, - } - } - - // since ReadTracks is used to handle ANNOUNCE and SETUP requests, - // all tracks must have a valid clock rate. - for i, track := range tracks { - _, err := track.ClockRate() - if err != nil { - return nil, fmt.Errorf("unable to get clock rate of track %d: %s", i, err) - } - } - - 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(multicast bool) []byte { - address := "0.0.0.0" - if multicast { - address = "224.1.0.0" - } - - sout := &sdp.SessionDescription{ - SessionName: psdp.SessionName("Stream"), - Origin: psdp.Origin{ - Username: "-", - NetworkType: "IN", - AddressType: "IP4", - UnicastAddress: "127.0.0.1", - }, - // required by Darwin Streaming Server - ConnectionInformation: &psdp.ConnectionInformation{ - NetworkType: "IN", - AddressType: "IP4", - Address: &psdp.Address{Address: address}, - }, - TimeDescriptions: []psdp.TimeDescription{ - {Timing: psdp.Timing{0, 0}}, //nolint:govet - }, - } - - for _, track := range ts { - sout.MediaDescriptions = append(sout.MediaDescriptions, track.Media) - } - - byts, _ := sout.Marshal() - return byts -} diff --git a/track_aac.go b/track_aac.go index e7f46aa4..fefd8a93 100644 --- a/track_aac.go +++ b/track_aac.go @@ -9,79 +9,43 @@ import ( psdp "github.com/pion/sdp/v3" "github.com/aler9/gortsplib/pkg/aac" + "github.com/aler9/gortsplib/pkg/base" ) -// TrackConfigAAC is the configuration of an AAC track. -type TrackConfigAAC struct { - Type int - SampleRate int - ChannelCount int - AOTSpecificConfig []byte +// TrackAAC is an AAC track. +type TrackAAC struct { + control string + payloadType uint8 + sampleRate int + channelCount int + mpegConf []byte } -// NewTrackAAC initializes an AAC track. -func NewTrackAAC(payloadType uint8, conf *TrackConfigAAC) (*Track, error) { +// NewTrackAAC allocates a TrackAAC. +func NewTrackAAC(payloadType uint8, typ int, sampleRate int, + channelCount int, aotSpecificConfig []byte) (*TrackAAC, error) { mpegConf, err := aac.MPEG4AudioConfig{ - Type: aac.MPEG4AudioType(conf.Type), - SampleRate: conf.SampleRate, - ChannelCount: conf.ChannelCount, - AOTSpecificConfig: conf.AOTSpecificConfig, + Type: aac.MPEG4AudioType(typ), + SampleRate: sampleRate, + ChannelCount: channelCount, + AOTSpecificConfig: aotSpecificConfig, }.Encode() if err != nil { - return nil, err + return nil, fmt.Errorf("invalid configuration: %s", err) } - typ := strconv.FormatInt(int64(payloadType), 10) - - return &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " mpeg4-generic/" + strconv.FormatInt(int64(conf.SampleRate), 10) + - "/" + strconv.FormatInt(int64(conf.ChannelCount), 10), - }, - { - Key: "fmtp", - Value: typ + " profile-level-id=1; " + - "mode=AAC-hbr; " + - "sizelength=13; " + - "indexlength=3; " + - "indexdeltalength=3; " + - "config=" + hex.EncodeToString(mpegConf), - }, - }, - }, + return &TrackAAC{ + payloadType: payloadType, + sampleRate: sampleRate, + channelCount: channelCount, + mpegConf: mpegConf, }, nil } -// IsAAC checks whether the track is an AAC track. -func (t *Track) IsAAC() bool { - if t.Media.MediaName.Media != "audio" { - return false - } +func newTrackAACFromMediaDescription(payloadType uint8, md *psdp.MediaDescription) (*TrackAAC, error) { + control := trackFindControl(md) - v, ok := t.Media.Attribute("rtpmap") - if !ok { - return false - } - - vals := strings.Split(v, " ") - if len(vals) != 2 { - return false - } - - return strings.HasPrefix(strings.ToLower(vals[1]), "mpeg4-generic/") -} - -// ExtractConfigAAC extracts the configuration of an AAC track. -func (t *Track) ExtractConfigAAC() (*TrackConfigAAC, error) { - v, ok := t.Media.Attribute("fmtp") + v, ok := md.Attribute("fmtp") if !ok { return nil, fmt.Errorf("fmtp attribute is missing") } @@ -115,16 +79,80 @@ func (t *Track) ExtractConfigAAC() (*TrackConfigAAC, error) { return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1]) } - conf := &TrackConfigAAC{ - Type: int(mpegConf.Type), - SampleRate: mpegConf.SampleRate, - ChannelCount: mpegConf.ChannelCount, - AOTSpecificConfig: mpegConf.AOTSpecificConfig, + // re-encode the conf to normalize it + enc, err = mpegConf.Encode() + if err != nil { + return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1]) } - return conf, nil + return &TrackAAC{ + control: control, + payloadType: payloadType, + sampleRate: mpegConf.SampleRate, + channelCount: mpegConf.ChannelCount, + mpegConf: enc, + }, nil } } return nil, fmt.Errorf("config is missing (%v)", v) } + +// ClockRate returns the track clock rate. +func (t *TrackAAC) ClockRate() int { + return t.sampleRate +} + +func (t *TrackAAC) clone() Track { + return &TrackAAC{ + control: t.control, + payloadType: t.payloadType, + sampleRate: t.sampleRate, + channelCount: t.channelCount, + mpegConf: t.mpegConf, + } +} + +func (t *TrackAAC) getControl() string { + return t.control +} + +func (t *TrackAAC) setControl(c string) { + t.control = c +} + +func (t *TrackAAC) url(contentBase *base.URL) (*base.URL, error) { + return trackURL(t, contentBase) +} + +func (t *TrackAAC) mediaDescription() *psdp.MediaDescription { + typ := strconv.FormatInt(int64(t.payloadType), 10) + + return &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{typ}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: typ + " mpeg4-generic/" + strconv.FormatInt(int64(t.sampleRate), 10) + + "/" + strconv.FormatInt(int64(t.channelCount), 10), + }, + { + Key: "fmtp", + Value: typ + " profile-level-id=1; " + + "mode=AAC-hbr; " + + "sizelength=13; " + + "indexlength=3; " + + "indexdeltalength=3; " + + "config=" + hex.EncodeToString(t.mpegConf), + }, + { + Key: "control", + Value: t.control, + }, + }, + } +} diff --git a/track_aac_test.go b/track_aac_test.go index 7ab5f08d..2cd0a8a3 100644 --- a/track_aac_test.go +++ b/track_aac_test.go @@ -8,178 +8,95 @@ import ( ) func TestTrackAACNew(t *testing.T) { - track, err := NewTrackAAC(96, &TrackConfigAAC{ - Type: 2, - SampleRate: 48000, - ChannelCount: 2, - }) + _, err := NewTrackAAC(96, 2, 48000, 2, nil) require.NoError(t, err) - require.Equal(t, &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", - }, - }, - }, - }, track) } -func TestTrackIsAAC(t *testing.T) { +func TestTrackAACNewFromMediaDescription(t *testing.T) { for _, ca := range []struct { name string - track *Track - }{ - { - "standard", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", - }, - }, - }, - }, - }, - { - "uppercase", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 MPEG4-GENERIC/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", - }, - }, - }, - }, - }, - } { - t.Run(ca.name, func(t *testing.T) { - require.Equal(t, true, ca.track.IsAAC()) - }) - } -} - -func TestTrackExtractConfigAAC(t *testing.T) { - for _, ca := range []struct { - name string - track *Track - conf *TrackConfigAAC + md *psdp.MediaDescription + track *TrackAAC }{ { "generic", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", - }, + { + Key: "fmtp", + Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", }, }, }, - &TrackConfigAAC{ - Type: 2, - SampleRate: 48000, - ChannelCount: 2, + &TrackAAC{ + payloadType: 96, + sampleRate: 48000, + channelCount: 2, + mpegConf: []byte{0x11, 0x90}, }, }, { "vlc rtsp server", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190;", - }, + { + Key: "fmtp", + Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190;", }, }, }, - &TrackConfigAAC{ - Type: 2, - SampleRate: 48000, - ChannelCount: 2, + &TrackAAC{ + payloadType: 96, + sampleRate: 48000, + channelCount: 2, + mpegConf: []byte{0x11, 0x90}, }, }, } { t.Run(ca.name, func(t *testing.T) { - conf, err := ca.track.ExtractConfigAAC() + track, err := newTrackAACFromMediaDescription(96, ca.md) require.NoError(t, err) - require.Equal(t, ca.conf, conf) + require.Equal(t, ca.track, track) }) } } -func TestTrackConfigAACErrors(t *testing.T) { +func TestTrackAACNewFromMediaDescriptionErrors(t *testing.T) { for _, ca := range []struct { - name string - track *Track - err string + name string + md *psdp.MediaDescription + err string }{ { "missing fmtp", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", }, }, }, @@ -187,22 +104,20 @@ func TestTrackConfigAACErrors(t *testing.T) { }, { "invalid fmtp", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96", - }, + { + Key: "fmtp", + Value: "96", }, }, }, @@ -210,22 +125,20 @@ func TestTrackConfigAACErrors(t *testing.T) { }, { "fmtp without key", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id", - }, + { + Key: "fmtp", + Value: "96 profile-level-id", }, }, }, @@ -233,22 +146,20 @@ func TestTrackConfigAACErrors(t *testing.T) { }, { "missing config", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1", - }, + { + Key: "fmtp", + Value: "96 profile-level-id=1", }, }, }, @@ -256,22 +167,20 @@ func TestTrackConfigAACErrors(t *testing.T) { }, { "invalid config", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 mpeg4-generic/48000/2", - }, - { - Key: "fmtp", - Value: "96 profile-level-id=1; config=zz", - }, + { + Key: "fmtp", + Value: "96 profile-level-id=1; config=zz", }, }, }, @@ -279,8 +188,35 @@ func TestTrackConfigAACErrors(t *testing.T) { }, } { t.Run(ca.name, func(t *testing.T) { - _, err := ca.track.ExtractConfigAAC() + _, err := newTrackAACFromMediaDescription(96, ca.md) require.EqualError(t, err, ca.err) }) } } + +func TestTrackAACMediaDescription(t *testing.T) { + track, err := NewTrackAAC(96, 2, 48000, 2, nil) + require.NoError(t, err) + + require.Equal(t, &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", + }, + { + Key: "fmtp", + Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", + }, + { + Key: "control", + Value: "", + }, + }, + }, track.mediaDescription()) +} diff --git a/track_generic.go b/track_generic.go new file mode 100644 index 00000000..97c3d560 --- /dev/null +++ b/track_generic.go @@ -0,0 +1,171 @@ +package gortsplib + +import ( + "fmt" + "strconv" + "strings" + + psdp "github.com/pion/sdp/v3" + + "github.com/aler9/gortsplib/pkg/base" +) + +func trackGenericGetClockRate(md *psdp.MediaDescription) (int, error) { + if len(md.MediaName.Formats) < 1 { + return 0, fmt.Errorf("no formats provided") + } + + // get clock rate from payload type + switch md.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 md.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") +} + +// TrackGeneric is a generic track. +type TrackGeneric struct { + control string + clockRate int + media string + formats []string + rtpmap string + fmtp string +} + +func newTrackGenericFromMediaDescription(md *psdp.MediaDescription) (*TrackGeneric, error) { + control := trackFindControl(md) + + clockRate, err := trackGenericGetClockRate(md) + if err != nil { + return nil, fmt.Errorf("unable to get clock rate: %s", err) + } + + rtpmap := func() string { + for _, attr := range md.Attributes { + if attr.Key == "rtpmap" { + return attr.Value + } + } + return "" + }() + + fmtp := func() string { + for _, attr := range md.Attributes { + if attr.Key == "fmtp" { + return attr.Value + } + } + return "" + }() + + return &TrackGeneric{ + control: control, + clockRate: clockRate, + media: md.MediaName.Media, + formats: md.MediaName.Formats, + rtpmap: rtpmap, + fmtp: fmtp, + }, nil +} + +// ClockRate returns the track clock rate. +func (t *TrackGeneric) ClockRate() int { + return t.clockRate +} + +func (t *TrackGeneric) clone() Track { + return &TrackGeneric{ + control: t.control, + clockRate: t.clockRate, + media: t.media, + formats: t.formats, + rtpmap: t.rtpmap, + fmtp: t.fmtp, + } +} + +func (t *TrackGeneric) getControl() string { + return t.control +} + +func (t *TrackGeneric) setControl(c string) { + t.control = c +} + +func (t *TrackGeneric) url(contentBase *base.URL) (*base.URL, error) { + return trackURL(t, contentBase) +} + +func (t *TrackGeneric) mediaDescription() *psdp.MediaDescription { + return &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: t.media, + Protos: []string{"RTP", "AVP"}, + Formats: t.formats, + }, + Attributes: func() []psdp.Attribute { + var ret []psdp.Attribute + + if t.rtpmap != "" { + ret = append(ret, psdp.Attribute{ + Key: "rtpmap", + Value: t.rtpmap, + }) + } + + if t.fmtp != "" { + ret = append(ret, psdp.Attribute{ + Key: "fmtp", + Value: t.fmtp, + }) + } + + ret = append(ret, psdp.Attribute{ + Key: "control", + Value: t.control, + }) + + return ret + }(), + } +} diff --git a/track_generic_test.go b/track_generic_test.go new file mode 100644 index 00000000..eb33aaab --- /dev/null +++ b/track_generic_test.go @@ -0,0 +1,84 @@ +package gortsplib + +import ( + "testing" + + psdp "github.com/pion/sdp/v3" + "github.com/stretchr/testify/require" +) + +func TestTrackGenericNewFromMediaDescription(t *testing.T) { + for _, ca := range []struct { + name string + md *psdp.MediaDescription + track *TrackGeneric + }{ + { + "pcma", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"8"}, + }, + }, + &TrackGeneric{ + clockRate: 8000, + media: "audio", + formats: []string{"8"}, + }, + }, + { + "pcmu", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Port: psdp.RangedPort{Value: 49170}, + Protos: []string{"RTP", "AVP"}, + Formats: []string{"0"}, + }, + }, + &TrackGeneric{ + clockRate: 8000, + media: "audio", + formats: []string{"0"}, + }, + }, + { + "multiple formats", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Port: psdp.RangedPort{Value: 0}, + Protos: []string{"RTP", "AVP"}, + Formats: []string{"98", "96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "98 H265/90000", + }, + { + Key: "fmtp", + Value: "98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + + "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + }, + }, + }, + &TrackGeneric{ + clockRate: 90000, + media: "video", + formats: []string{"98", "96"}, + rtpmap: "98 H265/90000", + fmtp: "98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + + "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=", + }, + }, + } { + t.Run(ca.name, func(t *testing.T) { + track, err := newTrackGenericFromMediaDescription(ca.md) + require.NoError(t, err) + require.Equal(t, ca.track, track) + }) + } +} diff --git a/track_h264.go b/track_h264.go index a58451a2..f7f0607b 100644 --- a/track_h264.go +++ b/track_h264.go @@ -8,79 +8,19 @@ import ( "strings" psdp "github.com/pion/sdp/v3" + + "github.com/aler9/gortsplib/pkg/base" ) -// TrackConfigH264 is the configuration of an H264 track. -type TrackConfigH264 struct { - SPS []byte - PPS []byte -} - -// NewTrackH264 initializes an H264 track. -func NewTrackH264(payloadType uint8, conf *TrackConfigH264) (*Track, error) { - if len(conf.SPS) < 4 { - return nil, fmt.Errorf("invalid SPS") - } - - spropParameterSets := base64.StdEncoding.EncodeToString(conf.SPS) + - "," + base64.StdEncoding.EncodeToString(conf.PPS) - profileLevelID := strings.ToUpper(hex.EncodeToString(conf.SPS[1:4])) - - typ := strconv.FormatInt(int64(payloadType), 10) - - return &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " H264/90000", - }, - { - Key: "fmtp", - Value: typ + " packetization-mode=1; " + - "sprop-parameter-sets=" + spropParameterSets + "; " + - "profile-level-id=" + profileLevelID, - }, - }, - }, - }, nil -} - -// IsH264 checks whether the track is an H264 track. -func (t *Track) IsH264() bool { - if t.Media.MediaName.Media != "video" { - return false - } - - v, ok := t.Media.Attribute("rtpmap") +func trackH264GetSPSPPS(md *psdp.MediaDescription) ([]byte, []byte, error) { + v, ok := md.Attribute("fmtp") if !ok { - return false - } - - v = strings.TrimSpace(v) - vals := strings.Split(v, " ") - if len(vals) != 2 { - return false - } - - return vals[1] == "H264/90000" -} - -// ExtractConfigH264 extracts the configuration of an H264 track. -func (t *Track) ExtractConfigH264() (*TrackConfigH264, error) { - v, ok := t.Media.Attribute("fmtp") - if !ok { - return nil, fmt.Errorf("fmtp attribute is missing") + return nil, nil, fmt.Errorf("fmtp attribute is missing") } tmp := strings.SplitN(v, " ", 2) if len(tmp) != 2 { - return nil, fmt.Errorf("invalid fmtp attribute (%v)", v) + return nil, nil, fmt.Errorf("invalid fmtp attribute (%v)", v) } for _, kv := range strings.Split(tmp[1], ";") { @@ -92,33 +32,133 @@ func (t *Track) ExtractConfigH264() (*TrackConfigH264, error) { tmp := strings.SplitN(kv, "=", 2) if len(tmp) != 2 { - return nil, fmt.Errorf("invalid fmtp attribute (%v)", v) + return nil, nil, fmt.Errorf("invalid fmtp attribute (%v)", v) } if tmp[0] == "sprop-parameter-sets" { - tmp := strings.SplitN(tmp[1], ",", 3) + tmp := strings.Split(tmp[1], ",") if len(tmp) < 2 { return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v) } sps, err := base64.StdEncoding.DecodeString(tmp[0]) if err != nil { - return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v) + return nil, nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v) } pps, err := base64.StdEncoding.DecodeString(tmp[1]) if err != nil { - return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v) + return nil, nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v) } - conf := &TrackConfigH264{ - SPS: sps, - PPS: pps, - } - - return conf, nil + return sps, pps, nil } } - return nil, fmt.Errorf("sprop-parameter-sets is missing (%v)", v) + return nil, nil, fmt.Errorf("sprop-parameter-sets is missing (%v)", v) +} + +// TrackH264 is a H264 track. +type TrackH264 struct { + control string + payloadType uint8 + sps []byte + pps []byte +} + +// NewTrackH264 allocates a TrackH264. +func NewTrackH264(payloadType uint8, sps []byte, pps []byte) (*TrackH264, error) { + return &TrackH264{ + payloadType: payloadType, + sps: sps, + pps: pps, + }, nil +} + +func newTrackH264FromMediaDescription(payloadType uint8, + md *psdp.MediaDescription) (*TrackH264, error) { + control := trackFindControl(md) + + t := &TrackH264{ + control: control, + payloadType: payloadType, + } + + sps, pps, err := trackH264GetSPSPPS(md) + if err == nil { + t.sps = sps + t.pps = pps + } + + return t, nil +} + +// ClockRate returns the track clock rate. +func (t *TrackH264) ClockRate() int { + return 90000 +} + +func (t *TrackH264) clone() Track { + return &TrackH264{ + control: t.control, + payloadType: t.payloadType, + sps: t.sps, + pps: t.pps, + } +} + +func (t *TrackH264) getControl() string { + return t.control +} + +func (t *TrackH264) setControl(c string) { + t.control = c +} + +func (t *TrackH264) url(contentBase *base.URL) (*base.URL, error) { + return trackURL(t, contentBase) +} + +// SPS returns the track SPS. +func (t *TrackH264) SPS() []byte { + return t.sps +} + +// PPS returns the track PPS. +func (t *TrackH264) PPS() []byte { + return t.pps +} + +func (t *TrackH264) mediaDescription() *psdp.MediaDescription { + typ := strconv.FormatInt(int64(t.payloadType), 10) + + fmtp := typ + " packetization-mode=1" + if len(t.sps) >= 4 { + spropParameterSets := base64.StdEncoding.EncodeToString(t.sps) + + "," + base64.StdEncoding.EncodeToString(t.pps) + profileLevelID := strings.ToUpper(hex.EncodeToString(t.sps[1:4])) + fmtp += "; sprop-parameter-sets=" + spropParameterSets + "; profile-level-id=" + profileLevelID + } + + return &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{typ}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: typ + " H264/90000", + }, + { + Key: "fmtp", + Value: fmtp, + }, + { + Key: "control", + Value: t.control, + }, + }, + } } diff --git a/track_h264_test.go b/track_h264_test.go index d0d82967..a559f2c2 100644 --- a/track_h264_test.go +++ b/track_h264_test.go @@ -7,195 +7,24 @@ import ( "github.com/stretchr/testify/require" ) -func TestTrackH264New(t *testing.T) { - tr, err := NewTrackH264(96, &TrackConfigH264{ - SPS: []byte{ - 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, - 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, - 0x00, 0x03, 0x00, 0x3d, 0x08, - }, - PPS: []byte{ - 0x68, 0xee, 0x3c, 0x80, - }, - }) - require.NoError(t, err) - require.Equal(t, &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1; " + - "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", - }, - }, - }, - }, tr) -} - -func TestTrackIsH264(t *testing.T) { +func TestTrackH264GetSPSPPSErrors(t *testing.T) { for _, ca := range []struct { - name string - track *Track - }{ - { - "standard", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1; " + - "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", - }, - }, - }, - }, - }, - { - "space at the end rtpmap", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000 ", - }, - }, - }, - }, - }, - } { - t.Run(ca.name, func(t *testing.T) { - require.Equal(t, true, ca.track.IsH264()) - }) - } -} - -func TestTrackExtractConfigH264(t *testing.T) { - for _, ca := range []struct { - name string - track *Track - conf *TrackConfigH264 - }{ - { - "generic", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1; " + - "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", - }, - }, - }, - }, - &TrackConfigH264{ - SPS: []byte{ - 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, - 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, - 0x00, 0x03, 0x00, 0x3d, 0x08, - }, - PPS: []byte{ - 0x68, 0xee, 0x3c, 0x80, - }, - }, - }, - { - "vlc rtsp server", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1;profile-level-id=64001f;" + - "sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,aOvjyyLA;", - }, - }, - }, - }, - &TrackConfigH264{ - SPS: []byte{ - 0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40, 0x50, - 0x05, 0xbb, 0x01, 0x6c, 0x80, 0x00, 0x00, 0x03, - 0x00, 0x80, 0x00, 0x00, 0x1e, 0x07, 0x8c, 0x18, - 0xcb, - }, - PPS: []byte{ - 0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0, - }, - }, - }, - } { - t.Run(ca.name, func(t *testing.T) { - conf, err := ca.track.ExtractConfigH264() - require.NoError(t, err) - require.Equal(t, ca.conf, conf) - }) - } -} - -func TestTrackConfigH264Errors(t *testing.T) { - for _, ca := range []struct { - name string - track *Track - err string + name string + md *psdp.MediaDescription + err string }{ { "missing fmtp", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", }, }, }, @@ -203,22 +32,20 @@ func TestTrackConfigH264Errors(t *testing.T) { }, { "invalid fmtp", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96", - }, + { + Key: "fmtp", + Value: "96", }, }, }, @@ -226,22 +53,20 @@ func TestTrackConfigH264Errors(t *testing.T) { }, { "fmtp without key", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode", - }, + { + Key: "fmtp", + Value: "96 packetization-mode", }, }, }, @@ -249,22 +74,20 @@ func TestTrackConfigH264Errors(t *testing.T) { }, { "missing sprop-parameter-set", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 packetization-mode=1", - }, + { + Key: "fmtp", + Value: "96 packetization-mode=1", }, }, }, @@ -272,22 +95,20 @@ func TestTrackConfigH264Errors(t *testing.T) { }, { "invalid sprop-parameter-set 1", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 sprop-parameter-sets=aaaaaa", - }, + { + Key: "fmtp", + Value: "96 sprop-parameter-sets=aaaaaa", }, }, }, @@ -295,22 +116,20 @@ func TestTrackConfigH264Errors(t *testing.T) { }, { "invalid sprop-parameter-set 2", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 sprop-parameter-sets=aaaaaa,bbb", - }, + { + Key: "fmtp", + Value: "96 sprop-parameter-sets=aaaaaa,bbb", }, }, }, @@ -318,22 +137,20 @@ func TestTrackConfigH264Errors(t *testing.T) { }, { "invalid sprop-parameter-set 3", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "video", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 H264/90000", - }, - { - Key: "fmtp", - Value: "96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb", - }, + { + Key: "fmtp", + Value: "96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb", }, }, }, @@ -341,8 +158,137 @@ func TestTrackConfigH264Errors(t *testing.T) { }, } { t.Run(ca.name, func(t *testing.T) { - _, err := ca.track.ExtractConfigH264() + _, _, err := trackH264GetSPSPPS(ca.md) require.EqualError(t, err, ca.err) }) } } + +func TestTrackH264New(t *testing.T) { + _, err := NewTrackH264(96, + []byte{ + 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, + 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x00, 0x03, 0x00, 0x3d, 0x08, + }, + []byte{ + 0x68, 0xee, 0x3c, 0x80, + }) + require.NoError(t, err) +} + +func TestTrackH264NewFromMediaDescription(t *testing.T) { + for _, ca := range []struct { + name string + md *psdp.MediaDescription + track *TrackH264 + }{ + { + "generic", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 packetization-mode=1; " + + "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", + }, + }, + }, + &TrackH264{ + payloadType: 96, + sps: []byte{ + 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, + 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x00, 0x03, 0x00, 0x3d, 0x08, + }, + pps: []byte{ + 0x68, 0xee, 0x3c, 0x80, + }, + }, + }, + { + "vlc rtsp server", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 packetization-mode=1;profile-level-id=64001f;" + + "sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,aOvjyyLA;", + }, + }, + }, + &TrackH264{ + payloadType: 96, + sps: []byte{ + 0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9, 0x40, 0x50, + 0x05, 0xbb, 0x01, 0x6c, 0x80, 0x00, 0x00, 0x03, + 0x00, 0x80, 0x00, 0x00, 0x1e, 0x07, 0x8c, 0x18, + 0xcb, + }, + pps: []byte{ + 0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0, + }, + }, + }, + } { + t.Run(ca.name, func(t *testing.T) { + track, err := newTrackH264FromMediaDescription(96, ca.md) + require.NoError(t, err) + require.Equal(t, ca.track, track) + }) + } +} + +func TestTrackH264MediaDescription(t *testing.T) { + track, err := NewTrackH264(96, + []byte{ + 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, + 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x00, 0x03, 0x00, 0x3d, 0x08, + }, + []byte{ + 0x68, 0xee, 0x3c, 0x80, + }) + require.NoError(t, err) + + require.Equal(t, &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 packetization-mode=1; " + + "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", + }, + { + Key: "control", + Value: "", + }, + }, + }, track.mediaDescription()) +} diff --git a/track_opus.go b/track_opus.go index 67114930..b40af878 100644 --- a/track_opus.go +++ b/track_opus.go @@ -6,67 +6,32 @@ import ( "strings" psdp "github.com/pion/sdp/v3" + + "github.com/aler9/gortsplib/pkg/base" ) -// TrackConfigOpus is the configuration of an Opus track. -type TrackConfigOpus struct { - SampleRate int - ChannelCount int +// TrackOpus is a Opus track. +type TrackOpus struct { + control string + payloadType uint8 + sampleRate int + channelCount int } -// NewTrackOpus initializes an Opus track. -func NewTrackOpus(payloadType uint8, conf *TrackConfigOpus) (*Track, error) { - typ := strconv.FormatInt(int64(payloadType), 10) - - return &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{typ}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: typ + " opus/" + strconv.FormatInt(int64(conf.SampleRate), 10) + - "/" + strconv.FormatInt(int64(conf.ChannelCount), 10), - }, - { - Key: "fmtp", - Value: typ + " sprop-stereo=" + func() string { - if conf.ChannelCount == 2 { - return "1" - } - return "0" - }(), - }, - }, - }, +// NewTrackOpus allocates a TrackOpus. +func NewTrackOpus(payloadType uint8, sampleRate int, channelCount int) (*TrackOpus, error) { + return &TrackOpus{ + payloadType: payloadType, + sampleRate: sampleRate, + channelCount: channelCount, }, nil } -// IsOpus checks whether the track is an Opus track. -func (t *Track) IsOpus() bool { - if t.Media.MediaName.Media != "audio" { - return false - } +func newTrackOpusFromMediaDescription(payloadType uint8, + md *psdp.MediaDescription) (*TrackOpus, error) { + control := trackFindControl(md) - v, ok := t.Media.Attribute("rtpmap") - if !ok { - return false - } - - vals := strings.Split(v, " ") - if len(vals) != 2 { - return false - } - - return strings.HasPrefix(vals[1], "opus/") -} - -// ExtractConfigOpus extracts the configuration of an Opus track. -func (t *Track) ExtractConfigOpus() (*TrackConfigOpus, error) { - v, ok := t.Media.Attribute("rtpmap") + v, ok := md.Attribute("rtpmap") if !ok { return nil, fmt.Errorf("rtpmap attribute is missing") } @@ -86,8 +51,68 @@ func (t *Track) ExtractConfigOpus() (*TrackConfigOpus, error) { return nil, err } - return &TrackConfigOpus{ - SampleRate: int(sampleRate), - ChannelCount: int(channelCount), + return &TrackOpus{ + control: control, + payloadType: payloadType, + sampleRate: int(sampleRate), + channelCount: int(channelCount), }, nil } + +// ClockRate returns the track clock rate. +func (t *TrackOpus) ClockRate() int { + return t.sampleRate +} + +func (t *TrackOpus) clone() Track { + return &TrackOpus{ + control: t.control, + payloadType: t.payloadType, + sampleRate: t.sampleRate, + channelCount: t.channelCount, + } +} + +func (t *TrackOpus) getControl() string { + return t.control +} + +func (t *TrackOpus) setControl(c string) { + t.control = c +} + +func (t *TrackOpus) url(contentBase *base.URL) (*base.URL, error) { + return trackURL(t, contentBase) +} + +func (t *TrackOpus) mediaDescription() *psdp.MediaDescription { + typ := strconv.FormatInt(int64(t.payloadType), 10) + + return &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{typ}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: typ + " opus/" + strconv.FormatInt(int64(t.sampleRate), 10) + + "/" + strconv.FormatInt(int64(t.channelCount), 10), + }, + { + Key: "fmtp", + Value: typ + " sprop-stereo=" + func() string { + if t.channelCount == 2 { + return "1" + } + return "0" + }(), + }, + { + Key: "control", + Value: t.control, + }, + }, + } +} diff --git a/track_opus_test.go b/track_opus_test.go index fb77fe15..653cc4f2 100644 --- a/track_opus_test.go +++ b/track_opus_test.go @@ -8,141 +8,80 @@ import ( ) func TestTrackOpusNew(t *testing.T) { - track, err := NewTrackOpus(96, &TrackConfigOpus{ - SampleRate: 48000, - ChannelCount: 2, - }) + _, err := NewTrackOpus(96, 48000, 2) require.NoError(t, err) - require.Equal(t, &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 opus/48000/2", - }, - { - Key: "fmtp", - Value: "96 sprop-stereo=1", - }, - }, - }, - }, track) } -func TestTrackIsOpus(t *testing.T) { +func TestTrackOpusNewFromMediaDescription(t *testing.T) { for _, ca := range []struct { name string - track *Track - }{ - { - "standard", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 opus/48000/2", - }, - { - Key: "fmtp", - Value: "96 sprop-stereo=1", - }, - }, - }, - }, - }, - } { - t.Run(ca.name, func(t *testing.T) { - require.Equal(t, true, ca.track.IsOpus()) - }) - } -} - -func TestTrackExtractConfigOpus(t *testing.T) { - for _, ca := range []struct { - name string - track *Track - conf *TrackConfigOpus + md *psdp.MediaDescription + track *TrackOpus }{ { "generic", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 opus/48000/2", }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96 opus/48000/2", - }, - { - Key: "fmtp", - Value: "96 sprop-stereo=1", - }, + { + Key: "fmtp", + Value: "96 sprop-stereo=1", }, }, }, - &TrackConfigOpus{ - SampleRate: 48000, - ChannelCount: 2, + &TrackOpus{ + payloadType: 96, + sampleRate: 48000, + channelCount: 2, }, }, } { t.Run(ca.name, func(t *testing.T) { - conf, err := ca.track.ExtractConfigOpus() + track, err := newTrackOpusFromMediaDescription(96, ca.md) require.NoError(t, err) - require.Equal(t, ca.conf, conf) + require.Equal(t, ca.track, track) }) } } -func TestTrackConfigOpusErrors(t *testing.T) { +func TestTrackOpusNewFromMediaDescriptionErrors(t *testing.T) { for _, ca := range []struct { - name string - track *Track - err string + name string + md *psdp.MediaDescription + err string }{ { "missing rtpmap", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{}, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, }, + Attributes: []psdp.Attribute{}, }, "rtpmap attribute is missing", }, { "invalid rtpmap", - &Track{ - Media: &psdp.MediaDescription{ - MediaName: psdp.MediaName{ - Media: "audio", - Protos: []string{"RTP", "AVP"}, - Formats: []string{"96"}, - }, - Attributes: []psdp.Attribute{ - { - Key: "rtpmap", - Value: "96", - }, + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96", }, }, }, @@ -150,8 +89,35 @@ func TestTrackConfigOpusErrors(t *testing.T) { }, } { t.Run(ca.name, func(t *testing.T) { - _, err := ca.track.ExtractConfigOpus() + _, err := newTrackOpusFromMediaDescription(96, ca.md) require.EqualError(t, err, ca.err) }) } } + +func TestTrackOpusMediaDescription(t *testing.T) { + track, err := NewTrackOpus(96, 48000, 2) + require.NoError(t, err) + + require.Equal(t, &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 opus/48000/2", + }, + { + Key: "fmtp", + Value: "96 sprop-stereo=1", + }, + { + Key: "control", + Value: "", + }, + }, + }, track.mediaDescription()) +} diff --git a/track_test.go b/track_test.go index b852d841..aa34e42c 100644 --- a/track_test.go +++ b/track_test.go @@ -3,11 +3,199 @@ package gortsplib import ( "testing" + psdp "github.com/pion/sdp/v3" "github.com/stretchr/testify/require" "github.com/aler9/gortsplib/pkg/base" ) +func TestTrackNewFromMediaDescription(t *testing.T) { + for _, ca := range []struct { + name string + md *psdp.MediaDescription + track Track + }{ + { + "pcma", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"8"}, + }, + }, + &TrackGeneric{ + clockRate: 8000, + media: "audio", + formats: []string{"8"}, + }, + }, + { + "aac", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 mpeg4-generic/48000/2", + }, + { + Key: "fmtp", + Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", + }, + }, + }, + &TrackAAC{ + payloadType: 96, + sampleRate: 48000, + channelCount: 2, + mpegConf: []byte{0x11, 0x90}, + }, + }, + { + "aac uppercase", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 MPEG4-GENERIC/48000/2", + }, + { + Key: "fmtp", + Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190", + }, + }, + }, + &TrackAAC{ + payloadType: 96, + sampleRate: 48000, + channelCount: 2, + mpegConf: []byte{0x11, 0x90}, + }, + }, + { + "opus", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "audio", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 opus/48000/2", + }, + { + Key: "fmtp", + Value: "96 sprop-stereo=1", + }, + }, + }, + &TrackOpus{ + payloadType: 96, + sampleRate: 48000, + channelCount: 2, + }, + }, + { + "h264", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000", + }, + { + Key: "fmtp", + Value: "96 packetization-mode=1; " + + "sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C", + }, + }, + }, + &TrackH264{ + payloadType: 96, + sps: []byte{ + 0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0, + 0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x00, 0x03, 0x00, 0x3d, 0x08, + }, + pps: []byte{ + 0x68, 0xee, 0x3c, 0x80, + }, + }, + }, + { + "h264 with a space at the end of rtpmap", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H264/90000 ", + }, + }, + }, + &TrackH264{ + payloadType: 96, + }, + }, + { + "h265", + &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: "video", + Protos: []string{"RTP", "AVP"}, + Formats: []string{"96"}, + }, + Attributes: []psdp.Attribute{ + { + Key: "rtpmap", + Value: "96 H265/90000", + }, + { + Key: "fmtp", + Value: "96 96 sprop-vps=QAEMAf//AWAAAAMAkAAAAwAAAwB4mZgJ; " + + "sprop-sps=QgEBAWAAAAMAkAAAAwAAAwB4oAPAgBDllmZpJMrgEAAAAwAQAAADAeCA; sprop-pps=RAHBcrRiQA==", + }, + }, + }, + &TrackGeneric{ + clockRate: 90000, + media: "video", + formats: []string{"96"}, + rtpmap: "96 H265/90000", + fmtp: "96 96 sprop-vps=QAEMAf//AWAAAAMAkAAAAwAAAwB4mZgJ; " + + "sprop-sps=QgEBAWAAAAMAkAAAAwAAAwB4oAPAgBDllmZpJMrgEAAAAwAQAAADAeCA; sprop-pps=RAHBcrRiQA==", + }, + }, + } { + t.Run(ca.name, func(t *testing.T) { + track, err := newTrackFromMediaDescription(ca.md) + require.NoError(t, err) + require.Equal(t, ca.track, track) + }) + } +} + func TestTrackURL(t *testing.T) { for _, ca := range []struct { name string @@ -112,109 +300,9 @@ func TestTrackURL(t *testing.T) { t.Run(ca.name, func(t *testing.T) { tracks, err := ReadTracks(ca.sdp) require.NoError(t, err) - ur, err := tracks[0].URL(ca.baseURL) + ur, err := tracks[0].url(ca.baseURL) require.NoError(t, err) require.Equal(t, ca.ur, ur) }) } } - -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, - }, - { - "multiple formats", - []byte("v=0\r\n" + - "o=RTSP 1853326073 627916868 IN IP4 0.0.0.0\r\n" + - "s=RTSP server\r\n" + - "c=IN IP4 0.0.0.0\r\n" + - "t=0 0\r\n" + - "a=control:*\r\n" + - "a=etag:1234567890\r\n" + - "a=range:npt=0-\r\n" + - "a=control:*\r\n" + - "m=video 0 RTP/AVP 98 96\r\n" + - "a=control:trackID=1\r\n" + - "b=AS:0\r\n" + - "a=rtpmap:98 H265/90000\r\n" + - "a=fmtp:98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " + - "sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=\r\n" + - "m=application 0 RTP/AVP 107\r\n" + - "a=control:trackID=3\r\n" + - "a=rtpmap:107 vnd.onvif.metadata/90000"), - 90000, - }, - } { - t.Run(ca.name, func(t *testing.T) { - tracks, err := ReadTracks(ca.sdp) - require.NoError(t, err) - - clockRate, err := tracks[0].ClockRate() - require.NoError(t, err) - - require.Equal(t, clockRate, ca.clockRate) - }) - } -} diff --git a/tracks.go b/tracks.go new file mode 100644 index 00000000..b535961a --- /dev/null +++ b/tracks.go @@ -0,0 +1,83 @@ +package gortsplib + +import ( + "fmt" + "strconv" + + psdp "github.com/pion/sdp/v3" + + "github.com/aler9/gortsplib/pkg/sdp" +) + +// Tracks is a list of tracks. +type Tracks []Track + +// ReadTracks decodes tracks from the SDP format. +func ReadTracks(byts []byte) (Tracks, error) { + var sd sdp.SessionDescription + err := sd.Unmarshal(byts) + if err != nil { + return nil, err + } + + tracks := make(Tracks, len(sd.MediaDescriptions)) + + for i, md := range sd.MediaDescriptions { + t, err := newTrackFromMediaDescription(md) + if err != nil { + return nil, fmt.Errorf("unable to parse track %d: %s", i, err) + } + + tracks[i] = t + } + + return tracks, nil +} + +func (ts Tracks) clone() Tracks { + ret := make(Tracks, len(ts)) + for i, track := range ts { + ret[i] = track.clone() + } + return ret +} + +func (ts Tracks) setControls() { + for i, t := range ts { + t.setControl("trackID=" + strconv.FormatInt(int64(i), 10)) + } +} + +// Write encodes tracks in the SDP format. +func (ts Tracks) Write(multicast bool) []byte { + address := "0.0.0.0" + if multicast { + address = "224.1.0.0" + } + + sout := &sdp.SessionDescription{ + SessionName: psdp.SessionName("Stream"), + Origin: psdp.Origin{ + Username: "-", + NetworkType: "IN", + AddressType: "IP4", + UnicastAddress: "127.0.0.1", + }, + // required by Darwin Streaming Server + ConnectionInformation: &psdp.ConnectionInformation{ + NetworkType: "IN", + AddressType: "IP4", + Address: &psdp.Address{Address: address}, + }, + TimeDescriptions: []psdp.TimeDescription{ + {Timing: psdp.Timing{0, 0}}, //nolint:govet + }, + } + + for _, track := range ts { + sout.MediaDescriptions = append(sout.MediaDescriptions, track.mediaDescription()) + } + + byts, _ := sout.Marshal() + return byts +} diff --git a/tracks_test.go b/tracks_test.go new file mode 100644 index 00000000..78931656 --- /dev/null +++ b/tracks_test.go @@ -0,0 +1,42 @@ +package gortsplib + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTracksReadErrors(t *testing.T) { + for _, ca := range []struct { + name string + sdp []byte + err string + }{ + { + "invalid SDP", + []byte{0x00, 0x01}, + "invalid line: (\x00\x01)", + }, + { + "invalid track", + []byte("v=0\r\n" + + "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + + "s=SDP Seminar\r\n" + + "m=video 0 RTP/AVP/TCP 96\r\n" + + "a=rtpmap:96 H265/90000\r\n" + + "a=fmtp:96 sprop-vps=QAEMAf//AWAAAAMAsAAAAwAAAwB4FwJA; " + + "sprop-sps=QgEBAWAAAAMAsAAAAwAAAwB4oAKggC8c1YgXuRZFL/y5/E/qbgQEBAE=; sprop-pps=RAHAcvBTJA==;\r\n" + + "a=control:streamid=0\r\n" + + "m=audio 0 RTP/AVP/TCP 97\r\n" + + "a=rtpmap:97 mpeg4-generic/44100/2\r\n" + + "a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=zzz1210\r\n" + + "a=control:streamid=1\r\n"), + "unable to parse track 1: invalid AAC config (zzz1210)", + }, + } { + t.Run(ca.name, func(t *testing.T) { + _, err := ReadTracks(ca.sdp) + require.EqualError(t, err, ca.err) + }) + } +}