new track system

This commit is contained in:
aler9
2022-01-28 17:42:20 +01:00
committed by Alessandro Ros
parent cd0db96a5d
commit 6d5bf0c1bb
31 changed files with 1523 additions and 1432 deletions

View File

@@ -21,8 +21,6 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
psdp "github.com/pion/sdp/v3"
"github.com/aler9/gortsplib/pkg/auth" "github.com/aler9/gortsplib/pkg/auth"
"github.com/aler9/gortsplib/pkg/base" "github.com/aler9/gortsplib/pkg/base"
"github.com/aler9/gortsplib/pkg/headers" "github.com/aler9/gortsplib/pkg/headers"
@@ -53,7 +51,7 @@ const (
) )
type clientTrack struct { type clientTrack struct {
track *Track track Track
udpRTPListener *clientUDPListener udpRTPListener *clientUDPListener
udpRTCPListener *clientUDPListener udpRTCPListener *clientUDPListener
tcpChannel int tcpChannel int
@@ -97,7 +95,7 @@ type announceReq struct {
type setupReq struct { type setupReq struct {
forPlay bool forPlay bool
track *Track track Track
baseURL *base.URL baseURL *base.URL
rtpPort int rtpPort int
rtcpPort int rtcpPort int
@@ -1177,14 +1175,7 @@ func (c *Client) doAnnounce(u *base.URL, tracks Tracks) (*base.Response, error)
// (tested with ffmpeg and gstreamer) // (tested with ffmpeg and gstreamer)
baseURL := u.Clone() baseURL := u.Clone()
for i, t := range tracks { tracks.setControls()
if !t.hasControlAttribute() {
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
Key: "control",
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
})
}
}
res, err := c.do(&base.Request{ res, err := c.do(&base.Request{
Method: base.Announce, Method: base.Announce,
@@ -1225,7 +1216,7 @@ func (c *Client) Announce(u *base.URL, tracks Tracks) (*base.Response, error) {
func (c *Client) doSetup( func (c *Client) doSetup(
forPlay bool, forPlay bool,
track *Track, track Track,
baseURL *base.URL, baseURL *base.URL,
rtpPort int, rtpPort int,
rtcpPort int) (*base.Response, error) { rtcpPort int) (*base.Response, error) {
@@ -1330,7 +1321,7 @@ func (c *Client) doSetup(
th.InterleavedIDs = &[2]int{(trackID * 2), (trackID * 2) + 1} th.InterleavedIDs = &[2]int{(trackID * 2), (trackID * 2) + 1}
} }
trackURL, err := track.URL(baseURL) trackURL, err := track.url(baseURL)
if err != nil { if err != nil {
if proto == TransportUDP { if proto == TransportUDP {
rtpListener.close() rtpListener.close()
@@ -1444,11 +1435,11 @@ func (c *Client) doSetup(
} }
} }
clockRate, _ := track.ClockRate()
cct := clientTrack{ cct := clientTrack{
track: track, track: track,
} }
clockRate := track.ClockRate()
if mode == headers.TransportModePlay { if mode == headers.TransportModePlay {
c.state = clientStatePrePlay c.state = clientStatePrePlay
cct.rtcpReceiver = rtcpreceiver.New(nil, clockRate) cct.rtcpReceiver = rtcpreceiver.New(nil, clockRate)
@@ -1555,7 +1546,7 @@ func (c *Client) doSetup(
// if rtpPort and rtcpPort are zero, they are chosen automatically. // if rtpPort and rtcpPort are zero, they are chosen automatically.
func (c *Client) Setup( func (c *Client) Setup(
forPlay bool, forPlay bool,
track *Track, track Track,
baseURL *base.URL, baseURL *base.URL,
rtpPort int, rtpPort int,
rtcpPort int) (*base.Response, error) { rtcpPort int) (*base.Response, error) {

View File

@@ -201,9 +201,7 @@ func TestClientPublishSerial(t *testing.T) {
}, },
} }
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
err = c.StartPublishing(scheme+"://localhost:8554/teststream", err = c.StartPublishing(scheme+"://localhost:8554/teststream",
@@ -360,9 +358,7 @@ func TestClientPublishParallel(t *testing.T) {
}(), }(),
} }
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
writerDone := make(chan struct{}) writerDone := make(chan struct{})
@@ -527,9 +523,7 @@ func TestClientPublishPauseSerial(t *testing.T) {
}(), }(),
} }
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
err = c.StartPublishing("rtsp://localhost:8554/teststream", err = c.StartPublishing("rtsp://localhost:8554/teststream",
@@ -668,9 +662,7 @@ func TestClientPublishPauseParallel(t *testing.T) {
}(), }(),
} }
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
err = c.StartPublishing("rtsp://localhost:8554/teststream", err = c.StartPublishing("rtsp://localhost:8554/teststream",
@@ -815,9 +807,7 @@ func TestClientPublishAutomaticProtocol(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
}() }()
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
c := Client{} c := Client{}
@@ -960,9 +950,7 @@ func TestClientPublishRTCPReport(t *testing.T) {
udpSenderReportPeriod: 1 * time.Second, udpSenderReportPeriod: 1 * time.Second,
} }
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
err = c.StartPublishing("rtsp://localhost:8554/teststream", err = c.StartPublishing("rtsp://localhost:8554/teststream",
@@ -1106,9 +1094,7 @@ func TestClientPublishIgnoreTCPRTPPackets(t *testing.T) {
}, },
} }
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
err = c.StartPublishing("rtsp://localhost:8554/teststream", err = c.StartPublishing("rtsp://localhost:8554/teststream",

View File

@@ -13,7 +13,6 @@ import (
"github.com/pion/rtcp" "github.com/pion/rtcp"
"github.com/pion/rtp" "github.com/pion/rtp"
psdp "github.com/pion/sdp/v3"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/net/ipv4" "golang.org/x/net/ipv4"
@@ -24,15 +23,13 @@ import (
) )
func TestClientReadTracks(t *testing.T) { func TestClientReadTracks(t *testing.T) {
track1, err := NewTrackH264(96, &TrackConfigH264{ track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) 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) 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) require.NoError(t, err)
l, err := net.Listen("tcp", "localhost:8554") 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, base.Describe, req.Method)
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
tracks := cloneAndClearTracks(Tracks{track1, track2, track3}) tracks := Tracks{track1, track2, track3}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -144,32 +142,7 @@ func TestClientReadTracks(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer c.Close() defer c.Close()
track1.Media.Attributes = append(track1.Media.Attributes, psdp.Attribute{ require.Equal(t, Tracks{track1, track2, track3}, c.Tracks())
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())
} }
func TestClientRead(t *testing.T) { func TestClientRead(t *testing.T) {
@@ -233,15 +206,11 @@ func TestClientRead(t *testing.T) {
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
require.Equal(t, mustParseURL(scheme+"://"+listenIP+":8554/test/stream?param=value"), req.URL) require.Equal(t, mustParseURL(scheme+"://"+listenIP+":8554/test/stream?param=value"), req.URL)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{ tracks := Tracks{track}
Key: "control", tracks.setControls()
Value: "trackID=0",
})
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -249,7 +218,7 @@ func TestClientRead(t *testing.T) {
"Content-Type": base.HeaderValue{"application/sdp"}, "Content-Type": base.HeaderValue{"application/sdp"},
"Content-Base": base.HeaderValue{scheme + "://" + listenIP + ":8554/test/stream?param=value/"}, "Content-Base": base.HeaderValue{scheme + "://" + listenIP + ":8554/test/stream?param=value/"},
}, },
Body: Tracks{track}.Write(false), Body: tracks.Write(false),
}.Write(&bb) }.Write(&bb)
_, err = conn.Write(bb.Bytes()) _, err = conn.Write(bb.Bytes())
require.NoError(t, err) require.NoError(t, err)
@@ -498,15 +467,11 @@ func TestClientReadNonStandardFrameSize(t *testing.T) {
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{ tracks := Tracks{track}
Key: "control", tracks.setControls()
Value: "trackID=0",
})
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -514,7 +479,7 @@ func TestClientReadNonStandardFrameSize(t *testing.T) {
"Content-Type": base.HeaderValue{"application/sdp"}, "Content-Type": base.HeaderValue{"application/sdp"},
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"}, "Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
}, },
Body: Tracks{track}.Write(false), Body: tracks.Write(false),
}.Write(&bb) }.Write(&bb)
_, err = conn.Write(bb.Bytes()) _, err = conn.Write(bb.Bytes())
require.NoError(t, err) require.NoError(t, err)
@@ -606,17 +571,14 @@ func TestClientReadPartial(t *testing.T) {
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
require.Equal(t, mustParseURL("rtsp://"+listenIP+":8554/teststream"), req.URL) require.Equal(t, mustParseURL("rtsp://"+listenIP+":8554/teststream"), req.URL)
track1, err := NewTrackH264(96, &TrackConfigH264{ track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
track2, err := NewTrackH264(96, &TrackConfigH264{ track2, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track1, track2}) tracks := Tracks{track1, track2}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -758,12 +720,11 @@ func TestClientReadNoContentBase(t *testing.T) {
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -879,12 +840,11 @@ func TestClientReadAnyPort(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -1037,12 +997,11 @@ func TestClientReadAutomaticProtocol(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -1177,12 +1136,11 @@ func TestClientReadAutomaticProtocol(t *testing.T) {
err = v.ValidateRequest(req) err = v.ValidateRequest(req)
require.NoError(t, err) require.NoError(t, err)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -1394,12 +1352,11 @@ func TestClientReadDifferentInterleavedIDs(t *testing.T) {
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
track1, err := NewTrackH264(96, &TrackConfigH264{ track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track1}) tracks := Tracks{track1}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -1562,12 +1519,11 @@ func TestClientReadRedirect(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -1728,12 +1684,11 @@ func TestClientReadPause(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -1908,12 +1863,11 @@ func TestClientReadRTCPReport(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -2087,12 +2041,11 @@ func TestClientReadErrorTimeout(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -2243,12 +2196,11 @@ func TestClientReadIgnoreTCPInvalidTrack(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -2378,12 +2330,11 @@ func TestClientReadSeek(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -2559,12 +2510,11 @@ func TestClientReadKeepaliveFromSession(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,

View File

@@ -120,12 +120,11 @@ func TestClientSession(t *testing.T) {
require.Equal(t, base.HeaderValue{"123456"}, req.Header["Session"]) require.Equal(t, base.HeaderValue{"123456"}, req.Header["Session"])
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -208,12 +207,11 @@ func TestClientAuth(t *testing.T) {
err = v.ValidateRequest(req) err = v.ValidateRequest(req)
require.NoError(t, err) require.NoError(t, err)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := cloneAndClearTracks(Tracks{track}) tracks := Tracks{track}
tracks.setControls()
base.Response{ base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
@@ -278,9 +276,7 @@ func TestClientDescribeCharset(t *testing.T) {
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
track1, err := NewTrackH264(96, &TrackConfigH264{ track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
base.Response{ base.Response{

View File

@@ -34,7 +34,7 @@ func main() {
fmt.Println("stream connected") fmt.Println("stream connected")
// create an AAC track // 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 { if err != nil {
panic(err) panic(err)
} }

View File

@@ -35,7 +35,7 @@ func main() {
fmt.Println("stream connected") fmt.Println("stream connected")
// create an H264 track // create an H264 track
track, err := gortsplib.NewTrackH264(96, &gortsplib.TrackConfigH264{sps, pps}) track, err := gortsplib.NewTrackH264(96, sps, pps)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -36,7 +36,7 @@ func main() {
fmt.Println("stream connected") fmt.Println("stream connected")
// create an H264 track // create an H264 track
track, err := gortsplib.NewTrackH264(96, &gortsplib.TrackConfigH264{sps, pps}) track, err := gortsplib.NewTrackH264(96, sps, pps)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -34,7 +34,7 @@ func main() {
fmt.Println("stream connected") fmt.Println("stream connected")
// create an Opus track // 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 { if err != nil {
panic(err) panic(err)
} }

View File

@@ -37,7 +37,7 @@ func main() {
fmt.Println("stream connected") fmt.Println("stream connected")
// create an H264 track // create an H264 track
track, err := gortsplib.NewTrackH264(96, &gortsplib.TrackConfigH264{sps, pps}) track, err := gortsplib.NewTrackH264(96, sps, pps)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -43,9 +43,11 @@ func main() {
} }
// find the AAC track // find the AAC track
var clockRate int
aacTrack := func() int { aacTrack := func() int {
for i, track := range tracks { for i, track := range tracks {
if track.IsAAC() { if _, ok := track.(*gortsplib.TrackAAC); ok {
clockRate = track.ClockRate()
return i return i
} }
} }
@@ -55,14 +57,8 @@ func main() {
panic("AAC track not found") panic("AAC track not found")
} }
// get track config
aacConf, err := tracks[aacTrack].ExtractConfigAAC()
if err != nil {
panic(err)
}
// setup decoder // setup decoder
dec := rtpaac.NewDecoder(aacConf.SampleRate) dec := rtpaac.NewDecoder(clockRate)
// called when a RTP packet arrives // called when a RTP packet arrives
c.OnPacketRTP = func(trackID int, payload []byte) { c.OnPacketRTP = func(trackID int, payload []byte) {

View File

@@ -41,9 +41,13 @@ func main() {
} }
// find the H264 track // find the H264 track
var sps []byte
var pps []byte
h264Track := func() int { h264Track := func() int {
for i, track := range tracks { for i, track := range tracks {
if track.IsH264() { if h264t, ok := track.(*gortsplib.TrackH264); ok {
sps = h264t.SPS()
pps = h264t.PPS()
return i return i
} }
} }
@@ -53,17 +57,11 @@ func main() {
panic("H264 track not found") panic("H264 track not found")
} }
// get track config
h264Conf, err := tracks[h264Track].ExtractConfigH264()
if err != nil {
panic(err)
}
// setup decoder // setup decoder
dec := rtph264.NewDecoder() dec := rtph264.NewDecoder()
// setup encoder // setup encoder
enc, err := newMPEGTSEncoder(h264Conf) enc, err := newMPEGTSEncoder(sps, pps)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -7,14 +7,14 @@ import (
"os" "os"
"time" "time"
"github.com/aler9/gortsplib"
"github.com/aler9/gortsplib/pkg/h264" "github.com/aler9/gortsplib/pkg/h264"
"github.com/asticode/go-astits" "github.com/asticode/go-astits"
) )
// mpegtsEncoder allows to encode H264 NALUs into MPEG-TS. // mpegtsEncoder allows to encode H264 NALUs into MPEG-TS.
type mpegtsEncoder struct { type mpegtsEncoder struct {
h264Conf *gortsplib.TrackConfigH264 sps []byte
pps []byte
f *os.File f *os.File
b *bufio.Writer b *bufio.Writer
@@ -25,7 +25,11 @@ type mpegtsEncoder struct {
} }
// newMPEGTSEncoder allocates a mpegtsEncoder. // 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") f, err := os.Create("mystream.ts")
if err != nil { if err != nil {
return nil, err return nil, err
@@ -40,11 +44,12 @@ func newMPEGTSEncoder(h264Conf *gortsplib.TrackConfigH264) (*mpegtsEncoder, erro
mux.SetPCRPID(256) mux.SetPCRPID(256)
return &mpegtsEncoder{ return &mpegtsEncoder{
h264Conf: h264Conf, sps: sps,
f: f, pps: pps,
b: b, f: f,
mux: mux, b: b,
dtsEst: h264.NewDTSEstimator(), mux: mux,
dtsEst: h264.NewDTSEstimator(),
}, nil }, nil
} }
@@ -87,7 +92,7 @@ func (e *mpegtsEncoder) encode(nalus [][]byte, pts time.Duration) error {
case h264.NALUTypeIDR: case h264.NALUTypeIDR:
// add SPS and PPS before every IDR // 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) filteredNALUs = append(filteredNALUs, nalu)

View File

@@ -45,7 +45,7 @@ func main() {
// find the H264 track // find the H264 track
h264Track := func() int { h264Track := func() int {
for i, track := range tracks { for i, track := range tracks {
if track.IsH264() { if _, ok := track.(*gortsplib.TrackH264); ok {
return i return i
} }
} }

View File

@@ -45,9 +45,9 @@ func main() {
panic(err) panic(err)
} }
// setup only video tracks, skipping audio or application tracks // setup only H264 tracks, skipping audio or application tracks
for _, t := range 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) _, err := c.Setup(true, t, baseURL, 0, 0)
if err != nil { if err != nil {
panic(err) panic(err)

View File

@@ -5,7 +5,6 @@ import (
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"net" "net"
"strconv"
"testing" "testing"
"time" "time"
@@ -27,14 +26,9 @@ func invalidURLAnnounceReq(t *testing.T, control string) base.Request {
"Content-Type": base.HeaderValue{"application/sdp"}, "Content-Type": base.HeaderValue{"application/sdp"},
}, },
Body: func() []byte { Body: func() []byte {
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{ track.setControl(control)
Key: "control",
Value: control,
})
sout := &psdp.SessionDescription{ sout := &psdp.SessionDescription{
SessionName: psdp.SessionName("Stream"), SessionName: psdp.SessionName("Stream"),
@@ -48,7 +42,7 @@ func invalidURLAnnounceReq(t *testing.T, control string) base.Request {
{Timing: psdp.Timing{0, 0}}, //nolint:govet {Timing: psdp.Timing{0, 0}}, //nolint:govet
}, },
MediaDescriptions: []*psdp.MediaDescription{ MediaDescriptions: []*psdp.MediaDescription{
track.Media, track.mediaDescription(),
}, },
} }
@@ -236,6 +230,9 @@ func TestServerPublishSetupPath(t *testing.T) {
s := &Server{ s := &Server{
Handler: &testServerHandler{ Handler: &testServerHandler{
onAnnounce: func(ctx *ServerHandlerOnAnnounceCtx) (*base.Response, error) { onAnnounce: func(ctx *ServerHandlerOnAnnounceCtx) (*base.Response, error) {
// make sure that track URLs are not overridden by NewServerStream()
NewServerStream(ctx.Tracks)
return &base.Response{ return &base.Response{
StatusCode: base.StatusOK, StatusCode: base.StatusOK,
}, nil }, nil
@@ -260,14 +257,10 @@ func TestServerPublishSetupPath(t *testing.T) {
defer conn.Close() defer conn.Close()
br := bufio.NewReader(conn) br := bufio.NewReader(conn)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{
Key: "control", track.setControl(ca.control)
Value: ca.control,
})
sout := &psdp.SessionDescription{ sout := &psdp.SessionDescription{
SessionName: psdp.SessionName("Stream"), SessionName: psdp.SessionName("Stream"),
@@ -281,7 +274,7 @@ func TestServerPublishSetupPath(t *testing.T) {
{Timing: psdp.Timing{0, 0}}, //nolint:govet {Timing: psdp.Timing{0, 0}}, //nolint:govet
}, },
MediaDescriptions: []*psdp.MediaDescription{ MediaDescriptions: []*psdp.MediaDescription{
track.Media, track.mediaDescription(),
}, },
} }
@@ -362,18 +355,11 @@ func TestServerPublishErrorSetupDifferentPaths(t *testing.T) {
defer conn.Close() defer conn.Close()
br := bufio.NewReader(conn) br := bufio.NewReader(conn)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := Tracks{track} tracks := Tracks{track}
for i, t := range tracks { tracks.setControls()
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
Key: "control",
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
})
}
res, err := writeReqReadRes(conn, br, base.Request{ res, err := writeReqReadRes(conn, br, base.Request{
Method: base.Announce, Method: base.Announce,
@@ -451,18 +437,11 @@ func TestServerPublishErrorSetupTrackTwice(t *testing.T) {
defer conn.Close() defer conn.Close()
br := bufio.NewReader(conn) br := bufio.NewReader(conn)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := Tracks{track} tracks := Tracks{track}
for i, t := range tracks { tracks.setControls()
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
Key: "control",
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
})
}
res, err := writeReqReadRes(conn, br, base.Request{ res, err := writeReqReadRes(conn, br, base.Request{
Method: base.Announce, Method: base.Announce,
@@ -557,23 +536,14 @@ func TestServerPublishErrorRecordPartialTracks(t *testing.T) {
defer conn.Close() defer conn.Close()
br := bufio.NewReader(conn) br := bufio.NewReader(conn)
track1, err := NewTrackH264(96, &TrackConfigH264{ track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
track2, err := NewTrackH264(96, &TrackConfigH264{ track2, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := Tracks{track1, track2} tracks := Tracks{track1, track2}
for i, t := range tracks { tracks.setControls()
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
Key: "control",
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
})
}
res, err := writeReqReadRes(conn, br, base.Request{ res, err := writeReqReadRes(conn, br, base.Request{
Method: base.Announce, Method: base.Announce,
@@ -715,18 +685,11 @@ func TestServerPublish(t *testing.T) {
<-connOpened <-connOpened
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := Tracks{track} tracks := Tracks{track}
for i, t := range tracks { tracks.setControls()
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
Key: "control",
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
})
}
res, err := writeReqReadRes(conn, br, base.Request{ res, err := writeReqReadRes(conn, br, base.Request{
Method: base.Announce, Method: base.Announce,
@@ -914,15 +877,11 @@ func TestServerPublishNonStandardFrameSize(t *testing.T) {
br := bufio.NewReader(conn) br := bufio.NewReader(conn)
var bb bytes.Buffer var bb bytes.Buffer
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{ tracks := Tracks{track}
Key: "control", tracks.setControls()
Value: "trackID=0",
})
res, err := writeReqReadRes(conn, br, base.Request{ res, err := writeReqReadRes(conn, br, base.Request{
Method: base.Announce, Method: base.Announce,
@@ -931,7 +890,7 @@ func TestServerPublishNonStandardFrameSize(t *testing.T) {
"CSeq": base.HeaderValue{"1"}, "CSeq": base.HeaderValue{"1"},
"Content-Type": base.HeaderValue{"application/sdp"}, "Content-Type": base.HeaderValue{"application/sdp"},
}, },
Body: Tracks{track}.Write(false), Body: tracks.Write(false),
}) })
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.StatusOK, res.StatusCode) require.Equal(t, base.StatusOK, res.StatusCode)
@@ -1023,18 +982,11 @@ func TestServerPublishErrorInvalidProtocol(t *testing.T) {
br := bufio.NewReader(conn) br := bufio.NewReader(conn)
var bb bytes.Buffer var bb bytes.Buffer
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := Tracks{track} tracks := Tracks{track}
for i, t := range tracks { tracks.setControls()
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
Key: "control",
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
})
}
res, err := writeReqReadRes(conn, br, base.Request{ res, err := writeReqReadRes(conn, br, base.Request{
Method: base.Announce, Method: base.Announce,
@@ -1134,18 +1086,11 @@ func TestServerPublishRTCPReport(t *testing.T) {
defer conn.Close() defer conn.Close()
br := bufio.NewReader(conn) br := bufio.NewReader(conn)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := Tracks{track} tracks := Tracks{track}
for i, t := range tracks { tracks.setControls()
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
Key: "control",
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
})
}
res, err := writeReqReadRes(conn, br, base.Request{ res, err := writeReqReadRes(conn, br, base.Request{
Method: base.Announce, Method: base.Announce,
@@ -1316,18 +1261,11 @@ func TestServerPublishTimeout(t *testing.T) {
defer conn.Close() defer conn.Close()
br := bufio.NewReader(conn) br := bufio.NewReader(conn)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := Tracks{track} tracks := Tracks{track}
for i, t := range tracks { tracks.setControls()
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
Key: "control",
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
})
}
res, err := writeReqReadRes(conn, br, base.Request{ res, err := writeReqReadRes(conn, br, base.Request{
Method: base.Announce, Method: base.Announce,
@@ -1450,18 +1388,11 @@ func TestServerPublishWithoutTeardown(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
br := bufio.NewReader(conn) br := bufio.NewReader(conn)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := Tracks{track} tracks := Tracks{track}
for i, t := range tracks { tracks.setControls()
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
Key: "control",
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
})
}
res, err := writeReqReadRes(conn, br, base.Request{ res, err := writeReqReadRes(conn, br, base.Request{
Method: base.Announce, Method: base.Announce,
@@ -1576,18 +1507,11 @@ func TestServerPublishUDPChangeConn(t *testing.T) {
defer conn.Close() defer conn.Close()
br := bufio.NewReader(conn) br := bufio.NewReader(conn)
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := Tracks{track} tracks := Tracks{track}
for i, t := range tracks { tracks.setControls()
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
Key: "control",
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
})
}
res, err := writeReqReadRes(conn, br, base.Request{ res, err := writeReqReadRes(conn, br, base.Request{
Method: base.Announce, Method: base.Announce,

View File

@@ -91,9 +91,7 @@ func TestServerReadSetupPath(t *testing.T) {
}, },
} { } {
t.Run(ca.name, func(t *testing.T) { t.Run(ca.name, func(t *testing.T) {
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track, track, track, track, track}) stream := NewServerStream(Tracks{track, track, track, track, track})
@@ -152,9 +150,7 @@ func TestServerReadSetupErrors(t *testing.T) {
t.Run(ca, func(t *testing.T) { t.Run(ca, func(t *testing.T) {
connClosed := make(chan struct{}) connClosed := make(chan struct{})
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -262,9 +258,7 @@ func TestServerRead(t *testing.T) {
sessionClosed := make(chan struct{}) sessionClosed := make(chan struct{})
framesReceived := make(chan struct{}) framesReceived := make(chan struct{})
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -572,9 +566,7 @@ func TestServerRead(t *testing.T) {
} }
func TestServerReadVLCMulticast(t *testing.T) { func TestServerReadVLCMulticast(t *testing.T) {
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -622,9 +614,7 @@ func TestServerReadVLCMulticast(t *testing.T) {
} }
func TestServerReadNonStandardFrameSize(t *testing.T) { func TestServerReadNonStandardFrameSize(t *testing.T) {
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -711,9 +701,7 @@ func TestServerReadTCPResponseBeforeFrames(t *testing.T) {
writerDone := make(chan struct{}) writerDone := make(chan struct{})
writerTerminate := make(chan struct{}) writerTerminate := make(chan struct{})
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -811,9 +799,7 @@ func TestServerReadTCPResponseBeforeFrames(t *testing.T) {
} }
func TestServerReadPlayPlay(t *testing.T) { func TestServerReadPlayPlay(t *testing.T) {
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -899,9 +885,7 @@ func TestServerReadPlayPausePlay(t *testing.T) {
writerDone := make(chan struct{}) writerDone := make(chan struct{})
writerTerminate := make(chan struct{}) writerTerminate := make(chan struct{})
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -1023,9 +1007,7 @@ func TestServerReadPlayPausePause(t *testing.T) {
writerDone := make(chan struct{}) writerDone := make(chan struct{})
writerTerminate := make(chan struct{}) writerTerminate := make(chan struct{})
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -1157,9 +1139,7 @@ func TestServerReadTimeout(t *testing.T) {
t.Run(transport, func(t *testing.T) { t.Run(transport, func(t *testing.T) {
sessionClosed := make(chan struct{}) sessionClosed := make(chan struct{})
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -1255,9 +1235,7 @@ func TestServerReadWithoutTeardown(t *testing.T) {
connClosed := make(chan struct{}) connClosed := make(chan struct{})
sessionClosed := make(chan struct{}) sessionClosed := make(chan struct{})
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -1359,9 +1337,7 @@ func TestServerReadWithoutTeardown(t *testing.T) {
} }
func TestServerReadUDPChangeConn(t *testing.T) { func TestServerReadUDPChangeConn(t *testing.T) {
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -1463,14 +1439,10 @@ func TestServerReadUDPChangeConn(t *testing.T) {
} }
func TestServerReadPartialTracks(t *testing.T) { func TestServerReadPartialTracks(t *testing.T) {
track1, err := NewTrackH264(96, &TrackConfigH264{ track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
track2, err := NewTrackH264(96, &TrackConfigH264{ track2, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track1, track2}) stream := NewServerStream(Tracks{track1, track2})
@@ -1643,9 +1615,7 @@ func TestServerReadAdditionalInfos(t *testing.T) {
return &ri, ssrcs return &ri, ssrcs
} }
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track, track}) stream := NewServerStream(Tracks{track, track})
@@ -1765,9 +1735,7 @@ func TestServerReadAdditionalInfos(t *testing.T) {
} }
func TestServerReadErrorUDPSamePorts(t *testing.T) { func TestServerReadErrorUDPSamePorts(t *testing.T) {
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})

View File

@@ -13,7 +13,6 @@ import (
"testing" "testing"
"time" "time"
psdp "github.com/pion/sdp/v3"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/aler9/gortsplib/pkg/base" "github.com/aler9/gortsplib/pkg/base"
@@ -690,9 +689,7 @@ func TestServerErrorInvalidMethod(t *testing.T) {
} }
func TestServerErrorTCPTwoConnOneSession(t *testing.T) { func TestServerErrorTCPTwoConnOneSession(t *testing.T) {
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -794,9 +791,7 @@ func TestServerErrorTCPTwoConnOneSession(t *testing.T) {
} }
func TestServerErrorTCPOneConnTwoSessions(t *testing.T) { func TestServerErrorTCPOneConnTwoSessions(t *testing.T) {
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -1064,9 +1059,7 @@ func TestServerSessionClose(t *testing.T) {
func TestServerSessionAutoClose(t *testing.T) { func TestServerSessionAutoClose(t *testing.T) {
sessionClosed := make(chan struct{}) sessionClosed := make(chan struct{})
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -1133,9 +1126,7 @@ func TestServerErrorInvalidPath(t *testing.T) {
t.Run(string(method), func(t *testing.T) { t.Run(string(method), func(t *testing.T) {
connClosed := make(chan struct{}) connClosed := make(chan struct{})
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
stream := NewServerStream(Tracks{track}) stream := NewServerStream(Tracks{track})
@@ -1177,18 +1168,11 @@ func TestServerErrorInvalidPath(t *testing.T) {
sxID := "" sxID := ""
if method == base.Record { if method == base.Record {
track, err := NewTrackH264(96, &TrackConfigH264{ track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
})
require.NoError(t, err) require.NoError(t, err)
tracks := Tracks{track} tracks := Tracks{track}
for i, t := range tracks { tracks.setControls()
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
Key: "control",
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
})
}
res, err := writeReqReadRes(conn, br, base.Request{ res, err := writeReqReadRes(conn, br, base.Request{
Method: base.Announce, Method: base.Announce,

View File

@@ -76,7 +76,7 @@ func setupGetTrackIDPathQuery(
} }
for trackID, track := range announcedTracks { for trackID, track := range announcedTracks {
u, _ := track.track.URL(setuppedBaseURL) u, _ := track.track.url(setuppedBaseURL)
if u.String() == url.String() { if u.String() == url.String() {
return trackID, *setuppedPath, *setuppedQuery, nil return trackID, *setuppedPath, *setuppedQuery, nil
} }
@@ -150,7 +150,7 @@ type ServerSessionSetuppedTrack struct {
// ServerSessionAnnouncedTrack is an announced track of a ServerSession. // ServerSessionAnnouncedTrack is an announced track of a ServerSession.
type ServerSessionAnnouncedTrack struct { type ServerSessionAnnouncedTrack struct {
track *Track track Track
rtcpReceiver *rtcpreceiver.RTCPReceiver rtcpReceiver *rtcpreceiver.RTCPReceiver
} }
@@ -508,7 +508,7 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base
} }
for _, track := range tracks { for _, track := range tracks {
trackURL, err := track.URL(req.URL) trackURL, err := track.url(req.URL)
if err != nil { if err != nil {
return &base.Response{ return &base.Response{
StatusCode: base.StatusBadRequest, StatusCode: base.StatusBadRequest,
@@ -795,8 +795,8 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base
ss.setuppedTracks[trackID] = sst ss.setuppedTracks[trackID] = sst
if ss.state == ServerSessionStatePrePublish && *ss.setuppedTransport != TransportTCP { if ss.state == ServerSessionStatePrePublish && *ss.setuppedTransport != TransportTCP {
clockRate, _ := ss.announcedTracks[trackID].track.ClockRate() ss.announcedTracks[trackID].rtcpReceiver = rtcpreceiver.New(nil,
ss.announcedTracks[trackID].rtcpReceiver = rtcpreceiver.New(nil, clockRate) ss.announcedTracks[trackID].track.ClockRate())
} }
res.Header["Transport"] = th.Write() res.Header["Transport"] = th.Write()

View File

@@ -34,13 +34,15 @@ type ServerStream struct {
// NewServerStream allocates a ServerStream. // NewServerStream allocates a ServerStream.
func NewServerStream(tracks Tracks) *ServerStream { func NewServerStream(tracks Tracks) *ServerStream {
tracks = tracks.clone()
tracks.setControls()
st := &ServerStream{ st := &ServerStream{
tracks: tracks,
readersUnicast: make(map[*ServerSession]struct{}), readersUnicast: make(map[*ServerSession]struct{}),
readers: make(map[*ServerSession]struct{}), readers: make(map[*ServerSession]struct{}),
} }
st.tracks = cloneAndClearTracks(tracks)
st.trackInfos = make([]*trackInfo, len(tracks)) st.trackInfos = make([]*trackInfo, len(tracks))
for i := range st.trackInfos { for i := range st.trackInfos {
st.trackInfos[i] = &trackInfo{} st.trackInfos[i] = &trackInfo{}
@@ -90,14 +92,13 @@ func (st *ServerStream) ssrc(trackID int) uint32 {
func (st *ServerStream) timestamp(trackID int) uint32 { func (st *ServerStream) timestamp(trackID int) uint32 {
lastTimeRTP := atomic.LoadUint32(&st.trackInfos[trackID].lastTimeRTP) lastTimeRTP := atomic.LoadUint32(&st.trackInfos[trackID].lastTimeRTP)
lastTimeNTP := atomic.LoadInt64(&st.trackInfos[trackID].lastTimeNTP) lastTimeNTP := atomic.LoadInt64(&st.trackInfos[trackID].lastTimeNTP)
clockRate, _ := st.tracks[trackID].ClockRate()
if lastTimeRTP == 0 || lastTimeNTP == 0 { if lastTimeRTP == 0 || lastTimeNTP == 0 {
return 0 return 0
} }
return uint32(uint64(lastTimeRTP) + 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 { func (st *ServerStream) lastSequenceNumber(trackID int) uint16 {

239
track.go
View File

@@ -8,47 +8,83 @@ import (
psdp "github.com/pion/sdp/v3" psdp "github.com/pion/sdp/v3"
"github.com/aler9/gortsplib/pkg/base" "github.com/aler9/gortsplib/pkg/base"
"github.com/aler9/gortsplib/pkg/sdp"
) )
// Track is a RTSP track. // Track is a RTSP track.
type Track struct { type Track interface {
// attributes in SDP format // ClockRate returns the track clock rate.
Media *psdp.MediaDescription ClockRate() int
clone() Track
getControl() string
setControl(string)
url(*base.URL) (*base.URL, error)
mediaDescription() *psdp.MediaDescription
} }
func (t *Track) hasControlAttribute() bool { func newTrackFromMediaDescription(md *psdp.MediaDescription) (Track, error) {
for _, attr := range t.Media.Attributes { if md.MediaName.Media == "video" {
if attr.Key == "control" { if rtpmap, ok := md.Attribute("rtpmap"); ok {
return true 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 trackFindControl(md *psdp.MediaDescription) string {
func (t *Track) URL(contentBase *base.URL) (*base.URL, error) { 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 { if contentBase == nil {
return nil, fmt.Errorf("no Content-Base header provided") return nil, fmt.Errorf("no Content-Base header provided")
} }
controlAttr := func() string { control := t.getControl()
for _, attr := range t.Media.Attributes {
if attr.Key == "control" {
return attr.Value
}
}
return ""
}()
// no control attribute, use base URL // no control attribute, use base URL
if controlAttr == "" { if control == "" {
return contentBase, nil return contentBase, nil
} }
// control attribute contains an absolute path // control attribute contains an absolute path
if strings.HasPrefix(controlAttr, "rtsp://") { if strings.HasPrefix(control, "rtsp://") {
ur, err := base.ParseURL(controlAttr) ur, err := base.ParseURL(control)
if err != nil { if err != nil {
return nil, err 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 // if there's a query, insert it after the query
// otherwise insert it after the path // otherwise insert it after the path
strURL := contentBase.String() strURL := contentBase.String()
if controlAttr[0] != '?' && !strings.HasSuffix(strURL, "/") { if control[0] != '?' && !strings.HasSuffix(strURL, "/") {
strURL += "/" strURL += "/"
} }
ur, _ := base.ParseURL(strURL + controlAttr)
ur, _ := base.ParseURL(strURL + control)
return ur, nil 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:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
for _, a := range t.Media.Attributes {
if a.Key == "rtpmap" {
tmp := strings.Split(a.Value, " ")
if len(tmp) < 2 {
return 0, fmt.Errorf("invalid rtpmap (%v)", a.Value)
}
tmp = strings.Split(tmp[1], "/")
if len(tmp) != 2 && len(tmp) != 3 {
return 0, fmt.Errorf("invalid rtpmap (%v)", a.Value)
}
v, err := strconv.ParseInt(tmp[1], 10, 64)
if err != nil {
return 0, err
}
return int(v), nil
}
}
return 0, fmt.Errorf("attribute 'rtpmap' not found")
}
// 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
}

View File

@@ -9,79 +9,43 @@ import (
psdp "github.com/pion/sdp/v3" psdp "github.com/pion/sdp/v3"
"github.com/aler9/gortsplib/pkg/aac" "github.com/aler9/gortsplib/pkg/aac"
"github.com/aler9/gortsplib/pkg/base"
) )
// TrackConfigAAC is the configuration of an AAC track. // TrackAAC is an AAC track.
type TrackConfigAAC struct { type TrackAAC struct {
Type int control string
SampleRate int payloadType uint8
ChannelCount int sampleRate int
AOTSpecificConfig []byte channelCount int
mpegConf []byte
} }
// NewTrackAAC initializes an AAC track. // NewTrackAAC allocates a TrackAAC.
func NewTrackAAC(payloadType uint8, conf *TrackConfigAAC) (*Track, error) { func NewTrackAAC(payloadType uint8, typ int, sampleRate int,
channelCount int, aotSpecificConfig []byte) (*TrackAAC, error) {
mpegConf, err := aac.MPEG4AudioConfig{ mpegConf, err := aac.MPEG4AudioConfig{
Type: aac.MPEG4AudioType(conf.Type), Type: aac.MPEG4AudioType(typ),
SampleRate: conf.SampleRate, SampleRate: sampleRate,
ChannelCount: conf.ChannelCount, ChannelCount: channelCount,
AOTSpecificConfig: conf.AOTSpecificConfig, AOTSpecificConfig: aotSpecificConfig,
}.Encode() }.Encode()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("invalid configuration: %s", err)
} }
typ := strconv.FormatInt(int64(payloadType), 10) return &TrackAAC{
payloadType: payloadType,
return &Track{ sampleRate: sampleRate,
Media: &psdp.MediaDescription{ channelCount: channelCount,
MediaName: psdp.MediaName{ mpegConf: mpegConf,
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),
},
},
},
}, nil }, nil
} }
// IsAAC checks whether the track is an AAC track. func newTrackAACFromMediaDescription(payloadType uint8, md *psdp.MediaDescription) (*TrackAAC, error) {
func (t *Track) IsAAC() bool { control := trackFindControl(md)
if t.Media.MediaName.Media != "audio" {
return false
}
v, ok := t.Media.Attribute("rtpmap") v, ok := md.Attribute("fmtp")
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")
if !ok { if !ok {
return nil, fmt.Errorf("fmtp attribute is missing") 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]) return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1])
} }
conf := &TrackConfigAAC{ // re-encode the conf to normalize it
Type: int(mpegConf.Type), enc, err = mpegConf.Encode()
SampleRate: mpegConf.SampleRate, if err != nil {
ChannelCount: mpegConf.ChannelCount, return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1])
AOTSpecificConfig: mpegConf.AOTSpecificConfig,
} }
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) 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,
},
},
}
}

View File

@@ -8,178 +8,95 @@ import (
) )
func TestTrackAACNew(t *testing.T) { func TestTrackAACNew(t *testing.T) {
track, err := NewTrackAAC(96, &TrackConfigAAC{ _, err := NewTrackAAC(96, 2, 48000, 2, nil)
Type: 2,
SampleRate: 48000,
ChannelCount: 2,
})
require.NoError(t, err) 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 { for _, ca := range []struct {
name string name string
track *Track md *psdp.MediaDescription
}{ track *TrackAAC
{
"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
}{ }{
{ {
"generic", "generic",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "audio",
Media: "audio", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 mpeg4-generic/48000/2",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
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",
},
}, },
}, },
}, },
&TrackConfigAAC{ &TrackAAC{
Type: 2, payloadType: 96,
SampleRate: 48000, sampleRate: 48000,
ChannelCount: 2, channelCount: 2,
mpegConf: []byte{0x11, 0x90},
}, },
}, },
{ {
"vlc rtsp server", "vlc rtsp server",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "audio",
Media: "audio", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 mpeg4-generic/48000/2",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190;",
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;",
},
}, },
}, },
}, },
&TrackConfigAAC{ &TrackAAC{
Type: 2, payloadType: 96,
SampleRate: 48000, sampleRate: 48000,
ChannelCount: 2, channelCount: 2,
mpegConf: []byte{0x11, 0x90},
}, },
}, },
} { } {
t.Run(ca.name, func(t *testing.T) { t.Run(ca.name, func(t *testing.T) {
conf, err := ca.track.ExtractConfigAAC() track, err := newTrackAACFromMediaDescription(96, ca.md)
require.NoError(t, err) 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 { for _, ca := range []struct {
name string name string
track *Track md *psdp.MediaDescription
err string err string
}{ }{
{ {
"missing fmtp", "missing fmtp",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "audio",
Media: "audio", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
}, Attributes: []psdp.Attribute{
Attributes: []psdp.Attribute{ {
{ Key: "rtpmap",
Key: "rtpmap", Value: "96 mpeg4-generic/48000/2",
Value: "96 mpeg4-generic/48000/2",
},
}, },
}, },
}, },
@@ -187,22 +104,20 @@ func TestTrackConfigAACErrors(t *testing.T) {
}, },
{ {
"invalid fmtp", "invalid fmtp",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "audio",
Media: "audio", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 mpeg4-generic/48000/2",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96",
Value: "96 mpeg4-generic/48000/2",
},
{
Key: "fmtp",
Value: "96",
},
}, },
}, },
}, },
@@ -210,22 +125,20 @@ func TestTrackConfigAACErrors(t *testing.T) {
}, },
{ {
"fmtp without key", "fmtp without key",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "audio",
Media: "audio", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 mpeg4-generic/48000/2",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96 profile-level-id",
Value: "96 mpeg4-generic/48000/2",
},
{
Key: "fmtp",
Value: "96 profile-level-id",
},
}, },
}, },
}, },
@@ -233,22 +146,20 @@ func TestTrackConfigAACErrors(t *testing.T) {
}, },
{ {
"missing config", "missing config",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "audio",
Media: "audio", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 mpeg4-generic/48000/2",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96 profile-level-id=1",
Value: "96 mpeg4-generic/48000/2",
},
{
Key: "fmtp",
Value: "96 profile-level-id=1",
},
}, },
}, },
}, },
@@ -256,22 +167,20 @@ func TestTrackConfigAACErrors(t *testing.T) {
}, },
{ {
"invalid config", "invalid config",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "audio",
Media: "audio", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 mpeg4-generic/48000/2",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96 profile-level-id=1; config=zz",
Value: "96 mpeg4-generic/48000/2",
},
{
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) { t.Run(ca.name, func(t *testing.T) {
_, err := ca.track.ExtractConfigAAC() _, err := newTrackAACFromMediaDescription(96, ca.md)
require.EqualError(t, err, ca.err) 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())
}

171
track_generic.go Normal file
View File

@@ -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:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
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
}(),
}
}

84
track_generic_test.go Normal file
View File

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

View File

@@ -8,79 +8,19 @@ import (
"strings" "strings"
psdp "github.com/pion/sdp/v3" psdp "github.com/pion/sdp/v3"
"github.com/aler9/gortsplib/pkg/base"
) )
// TrackConfigH264 is the configuration of an H264 track. func trackH264GetSPSPPS(md *psdp.MediaDescription) ([]byte, []byte, error) {
type TrackConfigH264 struct { v, ok := md.Attribute("fmtp")
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")
if !ok { if !ok {
return false return nil, nil, fmt.Errorf("fmtp attribute is missing")
}
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")
} }
tmp := strings.SplitN(v, " ", 2) tmp := strings.SplitN(v, " ", 2)
if len(tmp) != 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], ";") { for _, kv := range strings.Split(tmp[1], ";") {
@@ -92,33 +32,133 @@ func (t *Track) ExtractConfigH264() (*TrackConfigH264, error) {
tmp := strings.SplitN(kv, "=", 2) tmp := strings.SplitN(kv, "=", 2)
if len(tmp) != 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" { if tmp[0] == "sprop-parameter-sets" {
tmp := strings.SplitN(tmp[1], ",", 3) tmp := strings.Split(tmp[1], ",")
if len(tmp) < 2 { if len(tmp) < 2 {
return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v) return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
} }
sps, err := base64.StdEncoding.DecodeString(tmp[0]) sps, err := base64.StdEncoding.DecodeString(tmp[0])
if err != nil { 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]) pps, err := base64.StdEncoding.DecodeString(tmp[1])
if err != nil { 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{ return sps, pps, nil
SPS: sps,
PPS: pps,
}
return conf, 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,
},
},
}
} }

View File

@@ -7,195 +7,24 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestTrackH264New(t *testing.T) { func TestTrackH264GetSPSPPSErrors(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) {
for _, ca := range []struct { for _, ca := range []struct {
name string name string
track *Track md *psdp.MediaDescription
}{ err string
{
"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
}{ }{
{ {
"missing fmtp", "missing fmtp",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "video",
Media: "video", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
}, Attributes: []psdp.Attribute{
Attributes: []psdp.Attribute{ {
{ Key: "rtpmap",
Key: "rtpmap", Value: "96 H264/90000",
Value: "96 H264/90000",
},
}, },
}, },
}, },
@@ -203,22 +32,20 @@ func TestTrackConfigH264Errors(t *testing.T) {
}, },
{ {
"invalid fmtp", "invalid fmtp",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "video",
Media: "video", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 H264/90000",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96",
Value: "96 H264/90000",
},
{
Key: "fmtp",
Value: "96",
},
}, },
}, },
}, },
@@ -226,22 +53,20 @@ func TestTrackConfigH264Errors(t *testing.T) {
}, },
{ {
"fmtp without key", "fmtp without key",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "video",
Media: "video", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 H264/90000",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96 packetization-mode",
Value: "96 H264/90000",
},
{
Key: "fmtp",
Value: "96 packetization-mode",
},
}, },
}, },
}, },
@@ -249,22 +74,20 @@ func TestTrackConfigH264Errors(t *testing.T) {
}, },
{ {
"missing sprop-parameter-set", "missing sprop-parameter-set",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "video",
Media: "video", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 H264/90000",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96 packetization-mode=1",
Value: "96 H264/90000",
},
{
Key: "fmtp",
Value: "96 packetization-mode=1",
},
}, },
}, },
}, },
@@ -272,22 +95,20 @@ func TestTrackConfigH264Errors(t *testing.T) {
}, },
{ {
"invalid sprop-parameter-set 1", "invalid sprop-parameter-set 1",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "video",
Media: "video", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 H264/90000",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96 sprop-parameter-sets=aaaaaa",
Value: "96 H264/90000",
},
{
Key: "fmtp",
Value: "96 sprop-parameter-sets=aaaaaa",
},
}, },
}, },
}, },
@@ -295,22 +116,20 @@ func TestTrackConfigH264Errors(t *testing.T) {
}, },
{ {
"invalid sprop-parameter-set 2", "invalid sprop-parameter-set 2",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "video",
Media: "video", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 H264/90000",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96 sprop-parameter-sets=aaaaaa,bbb",
Value: "96 H264/90000",
},
{
Key: "fmtp",
Value: "96 sprop-parameter-sets=aaaaaa,bbb",
},
}, },
}, },
}, },
@@ -318,22 +137,20 @@ func TestTrackConfigH264Errors(t *testing.T) {
}, },
{ {
"invalid sprop-parameter-set 3", "invalid sprop-parameter-set 3",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "video",
Media: "video", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 H264/90000",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb",
Value: "96 H264/90000",
},
{
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) { t.Run(ca.name, func(t *testing.T) {
_, err := ca.track.ExtractConfigH264() _, _, err := trackH264GetSPSPPS(ca.md)
require.EqualError(t, err, ca.err) 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())
}

View File

@@ -6,67 +6,32 @@ import (
"strings" "strings"
psdp "github.com/pion/sdp/v3" psdp "github.com/pion/sdp/v3"
"github.com/aler9/gortsplib/pkg/base"
) )
// TrackConfigOpus is the configuration of an Opus track. // TrackOpus is a Opus track.
type TrackConfigOpus struct { type TrackOpus struct {
SampleRate int control string
ChannelCount int payloadType uint8
sampleRate int
channelCount int
} }
// NewTrackOpus initializes an Opus track. // NewTrackOpus allocates a TrackOpus.
func NewTrackOpus(payloadType uint8, conf *TrackConfigOpus) (*Track, error) { func NewTrackOpus(payloadType uint8, sampleRate int, channelCount int) (*TrackOpus, error) {
typ := strconv.FormatInt(int64(payloadType), 10) return &TrackOpus{
payloadType: payloadType,
return &Track{ sampleRate: sampleRate,
Media: &psdp.MediaDescription{ channelCount: channelCount,
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"
}(),
},
},
},
}, nil }, nil
} }
// IsOpus checks whether the track is an Opus track. func newTrackOpusFromMediaDescription(payloadType uint8,
func (t *Track) IsOpus() bool { md *psdp.MediaDescription) (*TrackOpus, error) {
if t.Media.MediaName.Media != "audio" { control := trackFindControl(md)
return false
}
v, ok := t.Media.Attribute("rtpmap") v, ok := md.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")
if !ok { if !ok {
return nil, fmt.Errorf("rtpmap attribute is missing") return nil, fmt.Errorf("rtpmap attribute is missing")
} }
@@ -86,8 +51,68 @@ func (t *Track) ExtractConfigOpus() (*TrackConfigOpus, error) {
return nil, err return nil, err
} }
return &TrackConfigOpus{ return &TrackOpus{
SampleRate: int(sampleRate), control: control,
ChannelCount: int(channelCount), payloadType: payloadType,
sampleRate: int(sampleRate),
channelCount: int(channelCount),
}, nil }, 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,
},
},
}
}

View File

@@ -8,141 +8,80 @@ import (
) )
func TestTrackOpusNew(t *testing.T) { func TestTrackOpusNew(t *testing.T) {
track, err := NewTrackOpus(96, &TrackConfigOpus{ _, err := NewTrackOpus(96, 48000, 2)
SampleRate: 48000,
ChannelCount: 2,
})
require.NoError(t, err) 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 { for _, ca := range []struct {
name string name string
track *Track md *psdp.MediaDescription
}{ track *TrackOpus
{
"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
}{ }{
{ {
"generic", "generic",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "audio",
Media: "audio", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
Attributes: []psdp.Attribute{
{
Key: "rtpmap",
Value: "96 opus/48000/2",
}, },
Attributes: []psdp.Attribute{ {
{ Key: "fmtp",
Key: "rtpmap", Value: "96 sprop-stereo=1",
Value: "96 opus/48000/2",
},
{
Key: "fmtp",
Value: "96 sprop-stereo=1",
},
}, },
}, },
}, },
&TrackConfigOpus{ &TrackOpus{
SampleRate: 48000, payloadType: 96,
ChannelCount: 2, sampleRate: 48000,
channelCount: 2,
}, },
}, },
} { } {
t.Run(ca.name, func(t *testing.T) { t.Run(ca.name, func(t *testing.T) {
conf, err := ca.track.ExtractConfigOpus() track, err := newTrackOpusFromMediaDescription(96, ca.md)
require.NoError(t, err) 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 { for _, ca := range []struct {
name string name string
track *Track md *psdp.MediaDescription
err string err string
}{ }{
{ {
"missing rtpmap", "missing rtpmap",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "audio",
Media: "audio", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"},
},
Attributes: []psdp.Attribute{},
}, },
Attributes: []psdp.Attribute{},
}, },
"rtpmap attribute is missing", "rtpmap attribute is missing",
}, },
{ {
"invalid rtpmap", "invalid rtpmap",
&Track{ &psdp.MediaDescription{
Media: &psdp.MediaDescription{ MediaName: psdp.MediaName{
MediaName: psdp.MediaName{ Media: "audio",
Media: "audio", Protos: []string{"RTP", "AVP"},
Protos: []string{"RTP", "AVP"}, Formats: []string{"96"},
Formats: []string{"96"}, },
}, Attributes: []psdp.Attribute{
Attributes: []psdp.Attribute{ {
{ Key: "rtpmap",
Key: "rtpmap", Value: "96",
Value: "96",
},
}, },
}, },
}, },
@@ -150,8 +89,35 @@ func TestTrackConfigOpusErrors(t *testing.T) {
}, },
} { } {
t.Run(ca.name, func(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) 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())
}

View File

@@ -3,11 +3,199 @@ package gortsplib
import ( import (
"testing" "testing"
psdp "github.com/pion/sdp/v3"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/aler9/gortsplib/pkg/base" "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) { func TestTrackURL(t *testing.T) {
for _, ca := range []struct { for _, ca := range []struct {
name string name string
@@ -112,109 +300,9 @@ func TestTrackURL(t *testing.T) {
t.Run(ca.name, func(t *testing.T) { t.Run(ca.name, func(t *testing.T) {
tracks, err := ReadTracks(ca.sdp) tracks, err := ReadTracks(ca.sdp)
require.NoError(t, err) require.NoError(t, err)
ur, err := tracks[0].URL(ca.baseURL) ur, err := tracks[0].url(ca.baseURL)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, ca.ur, ur) 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)
})
}
}

83
tracks.go Normal file
View File

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

42
tracks_test.go Normal file
View File

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