mirror of
https://github.com/aler9/gortsplib
synced 2025-10-04 23:02:45 +08:00
new track system
This commit is contained in:
23
client.go
23
client.go
@@ -21,8 +21,6 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
psdp "github.com/pion/sdp/v3"
|
||||
|
||||
"github.com/aler9/gortsplib/pkg/auth"
|
||||
"github.com/aler9/gortsplib/pkg/base"
|
||||
"github.com/aler9/gortsplib/pkg/headers"
|
||||
@@ -53,7 +51,7 @@ const (
|
||||
)
|
||||
|
||||
type clientTrack struct {
|
||||
track *Track
|
||||
track Track
|
||||
udpRTPListener *clientUDPListener
|
||||
udpRTCPListener *clientUDPListener
|
||||
tcpChannel int
|
||||
@@ -97,7 +95,7 @@ type announceReq struct {
|
||||
|
||||
type setupReq struct {
|
||||
forPlay bool
|
||||
track *Track
|
||||
track Track
|
||||
baseURL *base.URL
|
||||
rtpPort int
|
||||
rtcpPort int
|
||||
@@ -1177,14 +1175,7 @@ func (c *Client) doAnnounce(u *base.URL, tracks Tracks) (*base.Response, error)
|
||||
// (tested with ffmpeg and gstreamer)
|
||||
baseURL := u.Clone()
|
||||
|
||||
for i, t := range tracks {
|
||||
if !t.hasControlAttribute() {
|
||||
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
||||
})
|
||||
}
|
||||
}
|
||||
tracks.setControls()
|
||||
|
||||
res, err := c.do(&base.Request{
|
||||
Method: base.Announce,
|
||||
@@ -1225,7 +1216,7 @@ func (c *Client) Announce(u *base.URL, tracks Tracks) (*base.Response, error) {
|
||||
|
||||
func (c *Client) doSetup(
|
||||
forPlay bool,
|
||||
track *Track,
|
||||
track Track,
|
||||
baseURL *base.URL,
|
||||
rtpPort int,
|
||||
rtcpPort int) (*base.Response, error) {
|
||||
@@ -1330,7 +1321,7 @@ func (c *Client) doSetup(
|
||||
th.InterleavedIDs = &[2]int{(trackID * 2), (trackID * 2) + 1}
|
||||
}
|
||||
|
||||
trackURL, err := track.URL(baseURL)
|
||||
trackURL, err := track.url(baseURL)
|
||||
if err != nil {
|
||||
if proto == TransportUDP {
|
||||
rtpListener.close()
|
||||
@@ -1444,11 +1435,11 @@ func (c *Client) doSetup(
|
||||
}
|
||||
}
|
||||
|
||||
clockRate, _ := track.ClockRate()
|
||||
cct := clientTrack{
|
||||
track: track,
|
||||
}
|
||||
|
||||
clockRate := track.ClockRate()
|
||||
if mode == headers.TransportModePlay {
|
||||
c.state = clientStatePrePlay
|
||||
cct.rtcpReceiver = rtcpreceiver.New(nil, clockRate)
|
||||
@@ -1555,7 +1546,7 @@ func (c *Client) doSetup(
|
||||
// if rtpPort and rtcpPort are zero, they are chosen automatically.
|
||||
func (c *Client) Setup(
|
||||
forPlay bool,
|
||||
track *Track,
|
||||
track Track,
|
||||
baseURL *base.URL,
|
||||
rtpPort int,
|
||||
rtcpPort int) (*base.Response, error) {
|
||||
|
@@ -201,9 +201,7 @@ func TestClientPublishSerial(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.StartPublishing(scheme+"://localhost:8554/teststream",
|
||||
@@ -360,9 +358,7 @@ func TestClientPublishParallel(t *testing.T) {
|
||||
}(),
|
||||
}
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
writerDone := make(chan struct{})
|
||||
@@ -527,9 +523,7 @@ func TestClientPublishPauseSerial(t *testing.T) {
|
||||
}(),
|
||||
}
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.StartPublishing("rtsp://localhost:8554/teststream",
|
||||
@@ -668,9 +662,7 @@ func TestClientPublishPauseParallel(t *testing.T) {
|
||||
}(),
|
||||
}
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.StartPublishing("rtsp://localhost:8554/teststream",
|
||||
@@ -815,9 +807,7 @@ func TestClientPublishAutomaticProtocol(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
c := Client{}
|
||||
@@ -960,9 +950,7 @@ func TestClientPublishRTCPReport(t *testing.T) {
|
||||
udpSenderReportPeriod: 1 * time.Second,
|
||||
}
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.StartPublishing("rtsp://localhost:8554/teststream",
|
||||
@@ -1106,9 +1094,7 @@ func TestClientPublishIgnoreTCPRTPPackets(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.StartPublishing("rtsp://localhost:8554/teststream",
|
||||
|
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/rtp"
|
||||
psdp "github.com/pion/sdp/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/ipv4"
|
||||
|
||||
@@ -24,15 +23,13 @@ import (
|
||||
)
|
||||
|
||||
func TestClientReadTracks(t *testing.T) {
|
||||
track1, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
track2, err := NewTrackAAC(96, &TrackConfigAAC{Type: 2, SampleRate: 44100, ChannelCount: 2})
|
||||
track2, err := NewTrackAAC(96, 2, 44100, 2, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
track3, err := NewTrackAAC(96, &TrackConfigAAC{Type: 2, SampleRate: 96000, ChannelCount: 2})
|
||||
track3, err := NewTrackAAC(96, 2, 96000, 2, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
l, err := net.Listen("tcp", "localhost:8554")
|
||||
@@ -72,7 +69,8 @@ func TestClientReadTracks(t *testing.T) {
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track1, track2, track3})
|
||||
tracks := Tracks{track1, track2, track3}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -144,32 +142,7 @@ func TestClientReadTracks(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
track1.Media.Attributes = append(track1.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=0",
|
||||
})
|
||||
|
||||
track2.Media.Attributes = append(track2.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=1",
|
||||
})
|
||||
|
||||
track3.Media.Attributes = append(track3.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=2",
|
||||
})
|
||||
|
||||
require.Equal(t, Tracks{
|
||||
{
|
||||
Media: track1.Media,
|
||||
},
|
||||
{
|
||||
Media: track2.Media,
|
||||
},
|
||||
{
|
||||
Media: track3.Media,
|
||||
},
|
||||
}, c.Tracks())
|
||||
require.Equal(t, Tracks{track1, track2, track3}, c.Tracks())
|
||||
}
|
||||
|
||||
func TestClientRead(t *testing.T) {
|
||||
@@ -233,15 +206,11 @@ func TestClientRead(t *testing.T) {
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
require.Equal(t, mustParseURL(scheme+"://"+listenIP+":8554/test/stream?param=value"), req.URL)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=0",
|
||||
})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -249,7 +218,7 @@ func TestClientRead(t *testing.T) {
|
||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||
"Content-Base": base.HeaderValue{scheme + "://" + listenIP + ":8554/test/stream?param=value/"},
|
||||
},
|
||||
Body: Tracks{track}.Write(false),
|
||||
Body: tracks.Write(false),
|
||||
}.Write(&bb)
|
||||
_, err = conn.Write(bb.Bytes())
|
||||
require.NoError(t, err)
|
||||
@@ -498,15 +467,11 @@ func TestClientReadNonStandardFrameSize(t *testing.T) {
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=0",
|
||||
})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -514,7 +479,7 @@ func TestClientReadNonStandardFrameSize(t *testing.T) {
|
||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
|
||||
},
|
||||
Body: Tracks{track}.Write(false),
|
||||
Body: tracks.Write(false),
|
||||
}.Write(&bb)
|
||||
_, err = conn.Write(bb.Bytes())
|
||||
require.NoError(t, err)
|
||||
@@ -606,17 +571,14 @@ func TestClientReadPartial(t *testing.T) {
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
require.Equal(t, mustParseURL("rtsp://"+listenIP+":8554/teststream"), req.URL)
|
||||
|
||||
track1, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
track2, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track2, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track1, track2})
|
||||
tracks := Tracks{track1, track2}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -758,12 +720,11 @@ func TestClientReadNoContentBase(t *testing.T) {
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -879,12 +840,11 @@ func TestClientReadAnyPort(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -1037,12 +997,11 @@ func TestClientReadAutomaticProtocol(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -1177,12 +1136,11 @@ func TestClientReadAutomaticProtocol(t *testing.T) {
|
||||
err = v.ValidateRequest(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -1394,12 +1352,11 @@ func TestClientReadDifferentInterleavedIDs(t *testing.T) {
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
|
||||
|
||||
track1, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track1})
|
||||
tracks := Tracks{track1}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -1562,12 +1519,11 @@ func TestClientReadRedirect(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -1728,12 +1684,11 @@ func TestClientReadPause(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -1908,12 +1863,11 @@ func TestClientReadRTCPReport(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -2087,12 +2041,11 @@ func TestClientReadErrorTimeout(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -2243,12 +2196,11 @@ func TestClientReadIgnoreTCPInvalidTrack(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -2378,12 +2330,11 @@ func TestClientReadSeek(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -2559,12 +2510,11 @@ func TestClientReadKeepaliveFromSession(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
|
@@ -120,12 +120,11 @@ func TestClientSession(t *testing.T) {
|
||||
|
||||
require.Equal(t, base.HeaderValue{"123456"}, req.Header["Session"])
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -208,12 +207,11 @@ func TestClientAuth(t *testing.T) {
|
||||
err = v.ValidateRequest(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := cloneAndClearTracks(Tracks{track})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
@@ -278,9 +276,7 @@ func TestClientDescribeCharset(t *testing.T) {
|
||||
require.Equal(t, base.Describe, req.Method)
|
||||
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream"), req.URL)
|
||||
|
||||
track1, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
base.Response{
|
||||
|
@@ -34,7 +34,7 @@ func main() {
|
||||
fmt.Println("stream connected")
|
||||
|
||||
// create an AAC track
|
||||
track, err := gortsplib.NewTrackAAC(96, &gortsplib.TrackConfigAAC{Type: 2, SampleRate: 48000, ChannelCount: 2})
|
||||
track, err := gortsplib.NewTrackAAC(96, 2, 48000, 2, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ func main() {
|
||||
fmt.Println("stream connected")
|
||||
|
||||
// create an H264 track
|
||||
track, err := gortsplib.NewTrackH264(96, &gortsplib.TrackConfigH264{sps, pps})
|
||||
track, err := gortsplib.NewTrackH264(96, sps, pps)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ func main() {
|
||||
fmt.Println("stream connected")
|
||||
|
||||
// create an H264 track
|
||||
track, err := gortsplib.NewTrackH264(96, &gortsplib.TrackConfigH264{sps, pps})
|
||||
track, err := gortsplib.NewTrackH264(96, sps, pps)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ func main() {
|
||||
fmt.Println("stream connected")
|
||||
|
||||
// create an Opus track
|
||||
track, err := gortsplib.NewTrackOpus(96, &gortsplib.TrackConfigOpus{SampleRate: 48000, ChannelCount: 2})
|
||||
track, err := gortsplib.NewTrackOpus(96, 48000, 2)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@@ -37,7 +37,7 @@ func main() {
|
||||
fmt.Println("stream connected")
|
||||
|
||||
// create an H264 track
|
||||
track, err := gortsplib.NewTrackH264(96, &gortsplib.TrackConfigH264{sps, pps})
|
||||
track, err := gortsplib.NewTrackH264(96, sps, pps)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@@ -43,9 +43,11 @@ func main() {
|
||||
}
|
||||
|
||||
// find the AAC track
|
||||
var clockRate int
|
||||
aacTrack := func() int {
|
||||
for i, track := range tracks {
|
||||
if track.IsAAC() {
|
||||
if _, ok := track.(*gortsplib.TrackAAC); ok {
|
||||
clockRate = track.ClockRate()
|
||||
return i
|
||||
}
|
||||
}
|
||||
@@ -55,14 +57,8 @@ func main() {
|
||||
panic("AAC track not found")
|
||||
}
|
||||
|
||||
// get track config
|
||||
aacConf, err := tracks[aacTrack].ExtractConfigAAC()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// setup decoder
|
||||
dec := rtpaac.NewDecoder(aacConf.SampleRate)
|
||||
dec := rtpaac.NewDecoder(clockRate)
|
||||
|
||||
// called when a RTP packet arrives
|
||||
c.OnPacketRTP = func(trackID int, payload []byte) {
|
||||
|
@@ -41,9 +41,13 @@ func main() {
|
||||
}
|
||||
|
||||
// find the H264 track
|
||||
var sps []byte
|
||||
var pps []byte
|
||||
h264Track := func() int {
|
||||
for i, track := range tracks {
|
||||
if track.IsH264() {
|
||||
if h264t, ok := track.(*gortsplib.TrackH264); ok {
|
||||
sps = h264t.SPS()
|
||||
pps = h264t.PPS()
|
||||
return i
|
||||
}
|
||||
}
|
||||
@@ -53,17 +57,11 @@ func main() {
|
||||
panic("H264 track not found")
|
||||
}
|
||||
|
||||
// get track config
|
||||
h264Conf, err := tracks[h264Track].ExtractConfigH264()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// setup decoder
|
||||
dec := rtph264.NewDecoder()
|
||||
|
||||
// setup encoder
|
||||
enc, err := newMPEGTSEncoder(h264Conf)
|
||||
enc, err := newMPEGTSEncoder(sps, pps)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@@ -7,14 +7,14 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/aler9/gortsplib"
|
||||
"github.com/aler9/gortsplib/pkg/h264"
|
||||
"github.com/asticode/go-astits"
|
||||
)
|
||||
|
||||
// mpegtsEncoder allows to encode H264 NALUs into MPEG-TS.
|
||||
type mpegtsEncoder struct {
|
||||
h264Conf *gortsplib.TrackConfigH264
|
||||
sps []byte
|
||||
pps []byte
|
||||
|
||||
f *os.File
|
||||
b *bufio.Writer
|
||||
@@ -25,7 +25,11 @@ type mpegtsEncoder struct {
|
||||
}
|
||||
|
||||
// newMPEGTSEncoder allocates a mpegtsEncoder.
|
||||
func newMPEGTSEncoder(h264Conf *gortsplib.TrackConfigH264) (*mpegtsEncoder, error) {
|
||||
func newMPEGTSEncoder(sps []byte, pps []byte) (*mpegtsEncoder, error) {
|
||||
if sps == nil || pps == nil {
|
||||
return nil, fmt.Errorf("SPS or PPS not provided")
|
||||
}
|
||||
|
||||
f, err := os.Create("mystream.ts")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -40,7 +44,8 @@ func newMPEGTSEncoder(h264Conf *gortsplib.TrackConfigH264) (*mpegtsEncoder, erro
|
||||
mux.SetPCRPID(256)
|
||||
|
||||
return &mpegtsEncoder{
|
||||
h264Conf: h264Conf,
|
||||
sps: sps,
|
||||
pps: pps,
|
||||
f: f,
|
||||
b: b,
|
||||
mux: mux,
|
||||
@@ -87,7 +92,7 @@ func (e *mpegtsEncoder) encode(nalus [][]byte, pts time.Duration) error {
|
||||
|
||||
case h264.NALUTypeIDR:
|
||||
// add SPS and PPS before every IDR
|
||||
filteredNALUs = append(filteredNALUs, e.h264Conf.SPS, e.h264Conf.PPS)
|
||||
filteredNALUs = append(filteredNALUs, e.sps, e.pps)
|
||||
}
|
||||
|
||||
filteredNALUs = append(filteredNALUs, nalu)
|
||||
|
@@ -45,7 +45,7 @@ func main() {
|
||||
// find the H264 track
|
||||
h264Track := func() int {
|
||||
for i, track := range tracks {
|
||||
if track.IsH264() {
|
||||
if _, ok := track.(*gortsplib.TrackH264); ok {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
@@ -45,9 +45,9 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// setup only video tracks, skipping audio or application tracks
|
||||
// setup only H264 tracks, skipping audio or application tracks
|
||||
for _, t := range tracks {
|
||||
if t.Media.MediaName.Media == "video" {
|
||||
if _, ok := t.(*gortsplib.TrackH264); ok {
|
||||
_, err := c.Setup(true, t, baseURL, 0, 0)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@@ -5,7 +5,6 @@ import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -27,14 +26,9 @@ func invalidURLAnnounceReq(t *testing.T, control string) base.Request {
|
||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||
},
|
||||
Body: func() []byte {
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: control,
|
||||
})
|
||||
track.setControl(control)
|
||||
|
||||
sout := &psdp.SessionDescription{
|
||||
SessionName: psdp.SessionName("Stream"),
|
||||
@@ -48,7 +42,7 @@ func invalidURLAnnounceReq(t *testing.T, control string) base.Request {
|
||||
{Timing: psdp.Timing{0, 0}}, //nolint:govet
|
||||
},
|
||||
MediaDescriptions: []*psdp.MediaDescription{
|
||||
track.Media,
|
||||
track.mediaDescription(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -236,6 +230,9 @@ func TestServerPublishSetupPath(t *testing.T) {
|
||||
s := &Server{
|
||||
Handler: &testServerHandler{
|
||||
onAnnounce: func(ctx *ServerHandlerOnAnnounceCtx) (*base.Response, error) {
|
||||
// make sure that track URLs are not overridden by NewServerStream()
|
||||
NewServerStream(ctx.Tracks)
|
||||
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusOK,
|
||||
}, nil
|
||||
@@ -260,14 +257,10 @@ func TestServerPublishSetupPath(t *testing.T) {
|
||||
defer conn.Close()
|
||||
br := bufio.NewReader(conn)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: ca.control,
|
||||
})
|
||||
|
||||
track.setControl(ca.control)
|
||||
|
||||
sout := &psdp.SessionDescription{
|
||||
SessionName: psdp.SessionName("Stream"),
|
||||
@@ -281,7 +274,7 @@ func TestServerPublishSetupPath(t *testing.T) {
|
||||
{Timing: psdp.Timing{0, 0}}, //nolint:govet
|
||||
},
|
||||
MediaDescriptions: []*psdp.MediaDescription{
|
||||
track.Media,
|
||||
track.mediaDescription(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -362,18 +355,11 @@ func TestServerPublishErrorSetupDifferentPaths(t *testing.T) {
|
||||
defer conn.Close()
|
||||
br := bufio.NewReader(conn)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := Tracks{track}
|
||||
for i, t := range tracks {
|
||||
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
||||
})
|
||||
}
|
||||
tracks.setControls()
|
||||
|
||||
res, err := writeReqReadRes(conn, br, base.Request{
|
||||
Method: base.Announce,
|
||||
@@ -451,18 +437,11 @@ func TestServerPublishErrorSetupTrackTwice(t *testing.T) {
|
||||
defer conn.Close()
|
||||
br := bufio.NewReader(conn)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := Tracks{track}
|
||||
for i, t := range tracks {
|
||||
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
||||
})
|
||||
}
|
||||
tracks.setControls()
|
||||
|
||||
res, err := writeReqReadRes(conn, br, base.Request{
|
||||
Method: base.Announce,
|
||||
@@ -557,23 +536,14 @@ func TestServerPublishErrorRecordPartialTracks(t *testing.T) {
|
||||
defer conn.Close()
|
||||
br := bufio.NewReader(conn)
|
||||
|
||||
track1, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
track2, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track2, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := Tracks{track1, track2}
|
||||
for i, t := range tracks {
|
||||
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
||||
})
|
||||
}
|
||||
tracks.setControls()
|
||||
|
||||
res, err := writeReqReadRes(conn, br, base.Request{
|
||||
Method: base.Announce,
|
||||
@@ -715,18 +685,11 @@ func TestServerPublish(t *testing.T) {
|
||||
|
||||
<-connOpened
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := Tracks{track}
|
||||
for i, t := range tracks {
|
||||
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
||||
})
|
||||
}
|
||||
tracks.setControls()
|
||||
|
||||
res, err := writeReqReadRes(conn, br, base.Request{
|
||||
Method: base.Announce,
|
||||
@@ -914,15 +877,11 @@ func TestServerPublishNonStandardFrameSize(t *testing.T) {
|
||||
br := bufio.NewReader(conn)
|
||||
var bb bytes.Buffer
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
track.Media.Attributes = append(track.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=0",
|
||||
})
|
||||
tracks := Tracks{track}
|
||||
tracks.setControls()
|
||||
|
||||
res, err := writeReqReadRes(conn, br, base.Request{
|
||||
Method: base.Announce,
|
||||
@@ -931,7 +890,7 @@ func TestServerPublishNonStandardFrameSize(t *testing.T) {
|
||||
"CSeq": base.HeaderValue{"1"},
|
||||
"Content-Type": base.HeaderValue{"application/sdp"},
|
||||
},
|
||||
Body: Tracks{track}.Write(false),
|
||||
Body: tracks.Write(false),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, base.StatusOK, res.StatusCode)
|
||||
@@ -1023,18 +982,11 @@ func TestServerPublishErrorInvalidProtocol(t *testing.T) {
|
||||
br := bufio.NewReader(conn)
|
||||
var bb bytes.Buffer
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := Tracks{track}
|
||||
for i, t := range tracks {
|
||||
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
||||
})
|
||||
}
|
||||
tracks.setControls()
|
||||
|
||||
res, err := writeReqReadRes(conn, br, base.Request{
|
||||
Method: base.Announce,
|
||||
@@ -1134,18 +1086,11 @@ func TestServerPublishRTCPReport(t *testing.T) {
|
||||
defer conn.Close()
|
||||
br := bufio.NewReader(conn)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := Tracks{track}
|
||||
for i, t := range tracks {
|
||||
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
||||
})
|
||||
}
|
||||
tracks.setControls()
|
||||
|
||||
res, err := writeReqReadRes(conn, br, base.Request{
|
||||
Method: base.Announce,
|
||||
@@ -1316,18 +1261,11 @@ func TestServerPublishTimeout(t *testing.T) {
|
||||
defer conn.Close()
|
||||
br := bufio.NewReader(conn)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := Tracks{track}
|
||||
for i, t := range tracks {
|
||||
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
||||
})
|
||||
}
|
||||
tracks.setControls()
|
||||
|
||||
res, err := writeReqReadRes(conn, br, base.Request{
|
||||
Method: base.Announce,
|
||||
@@ -1450,18 +1388,11 @@ func TestServerPublishWithoutTeardown(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
br := bufio.NewReader(conn)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := Tracks{track}
|
||||
for i, t := range tracks {
|
||||
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
||||
})
|
||||
}
|
||||
tracks.setControls()
|
||||
|
||||
res, err := writeReqReadRes(conn, br, base.Request{
|
||||
Method: base.Announce,
|
||||
@@ -1576,18 +1507,11 @@ func TestServerPublishUDPChangeConn(t *testing.T) {
|
||||
defer conn.Close()
|
||||
br := bufio.NewReader(conn)
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := Tracks{track}
|
||||
for i, t := range tracks {
|
||||
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
||||
})
|
||||
}
|
||||
tracks.setControls()
|
||||
|
||||
res, err := writeReqReadRes(conn, br, base.Request{
|
||||
Method: base.Announce,
|
||||
|
@@ -91,9 +91,7 @@ func TestServerReadSetupPath(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track, track, track, track, track})
|
||||
@@ -152,9 +150,7 @@ func TestServerReadSetupErrors(t *testing.T) {
|
||||
t.Run(ca, func(t *testing.T) {
|
||||
connClosed := make(chan struct{})
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -262,9 +258,7 @@ func TestServerRead(t *testing.T) {
|
||||
sessionClosed := make(chan struct{})
|
||||
framesReceived := make(chan struct{})
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -572,9 +566,7 @@ func TestServerRead(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerReadVLCMulticast(t *testing.T) {
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -622,9 +614,7 @@ func TestServerReadVLCMulticast(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerReadNonStandardFrameSize(t *testing.T) {
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -711,9 +701,7 @@ func TestServerReadTCPResponseBeforeFrames(t *testing.T) {
|
||||
writerDone := make(chan struct{})
|
||||
writerTerminate := make(chan struct{})
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -811,9 +799,7 @@ func TestServerReadTCPResponseBeforeFrames(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerReadPlayPlay(t *testing.T) {
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -899,9 +885,7 @@ func TestServerReadPlayPausePlay(t *testing.T) {
|
||||
writerDone := make(chan struct{})
|
||||
writerTerminate := make(chan struct{})
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -1023,9 +1007,7 @@ func TestServerReadPlayPausePause(t *testing.T) {
|
||||
writerDone := make(chan struct{})
|
||||
writerTerminate := make(chan struct{})
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -1157,9 +1139,7 @@ func TestServerReadTimeout(t *testing.T) {
|
||||
t.Run(transport, func(t *testing.T) {
|
||||
sessionClosed := make(chan struct{})
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -1255,9 +1235,7 @@ func TestServerReadWithoutTeardown(t *testing.T) {
|
||||
connClosed := make(chan struct{})
|
||||
sessionClosed := make(chan struct{})
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -1359,9 +1337,7 @@ func TestServerReadWithoutTeardown(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerReadUDPChangeConn(t *testing.T) {
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -1463,14 +1439,10 @@ func TestServerReadUDPChangeConn(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerReadPartialTracks(t *testing.T) {
|
||||
track1, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track1, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
track2, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track2, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track1, track2})
|
||||
@@ -1643,9 +1615,7 @@ func TestServerReadAdditionalInfos(t *testing.T) {
|
||||
return &ri, ssrcs
|
||||
}
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track, track})
|
||||
@@ -1765,9 +1735,7 @@ func TestServerReadAdditionalInfos(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerReadErrorUDPSamePorts(t *testing.T) {
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
|
@@ -13,7 +13,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
psdp "github.com/pion/sdp/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aler9/gortsplib/pkg/base"
|
||||
@@ -690,9 +689,7 @@ func TestServerErrorInvalidMethod(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerErrorTCPTwoConnOneSession(t *testing.T) {
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -794,9 +791,7 @@ func TestServerErrorTCPTwoConnOneSession(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServerErrorTCPOneConnTwoSessions(t *testing.T) {
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -1064,9 +1059,7 @@ func TestServerSessionClose(t *testing.T) {
|
||||
func TestServerSessionAutoClose(t *testing.T) {
|
||||
sessionClosed := make(chan struct{})
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -1133,9 +1126,7 @@ func TestServerErrorInvalidPath(t *testing.T) {
|
||||
t.Run(string(method), func(t *testing.T) {
|
||||
connClosed := make(chan struct{})
|
||||
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
stream := NewServerStream(Tracks{track})
|
||||
@@ -1177,18 +1168,11 @@ func TestServerErrorInvalidPath(t *testing.T) {
|
||||
sxID := ""
|
||||
|
||||
if method == base.Record {
|
||||
track, err := NewTrackH264(96, &TrackConfigH264{
|
||||
[]byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04},
|
||||
})
|
||||
track, err := NewTrackH264(96, []byte{0x01, 0x02, 0x03, 0x04}, []byte{0x01, 0x02, 0x03, 0x04})
|
||||
require.NoError(t, err)
|
||||
|
||||
tracks := Tracks{track}
|
||||
for i, t := range tracks {
|
||||
t.Media.Attributes = append(t.Media.Attributes, psdp.Attribute{
|
||||
Key: "control",
|
||||
Value: "trackID=" + strconv.FormatInt(int64(i), 10),
|
||||
})
|
||||
}
|
||||
tracks.setControls()
|
||||
|
||||
res, err := writeReqReadRes(conn, br, base.Request{
|
||||
Method: base.Announce,
|
||||
|
@@ -76,7 +76,7 @@ func setupGetTrackIDPathQuery(
|
||||
}
|
||||
|
||||
for trackID, track := range announcedTracks {
|
||||
u, _ := track.track.URL(setuppedBaseURL)
|
||||
u, _ := track.track.url(setuppedBaseURL)
|
||||
if u.String() == url.String() {
|
||||
return trackID, *setuppedPath, *setuppedQuery, nil
|
||||
}
|
||||
@@ -150,7 +150,7 @@ type ServerSessionSetuppedTrack struct {
|
||||
|
||||
// ServerSessionAnnouncedTrack is an announced track of a ServerSession.
|
||||
type ServerSessionAnnouncedTrack struct {
|
||||
track *Track
|
||||
track Track
|
||||
rtcpReceiver *rtcpreceiver.RTCPReceiver
|
||||
}
|
||||
|
||||
@@ -508,7 +508,7 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base
|
||||
}
|
||||
|
||||
for _, track := range tracks {
|
||||
trackURL, err := track.URL(req.URL)
|
||||
trackURL, err := track.url(req.URL)
|
||||
if err != nil {
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusBadRequest,
|
||||
@@ -795,8 +795,8 @@ func (ss *ServerSession) handleRequest(sc *ServerConn, req *base.Request) (*base
|
||||
ss.setuppedTracks[trackID] = sst
|
||||
|
||||
if ss.state == ServerSessionStatePrePublish && *ss.setuppedTransport != TransportTCP {
|
||||
clockRate, _ := ss.announcedTracks[trackID].track.ClockRate()
|
||||
ss.announcedTracks[trackID].rtcpReceiver = rtcpreceiver.New(nil, clockRate)
|
||||
ss.announcedTracks[trackID].rtcpReceiver = rtcpreceiver.New(nil,
|
||||
ss.announcedTracks[trackID].track.ClockRate())
|
||||
}
|
||||
|
||||
res.Header["Transport"] = th.Write()
|
||||
|
@@ -34,13 +34,15 @@ type ServerStream struct {
|
||||
|
||||
// NewServerStream allocates a ServerStream.
|
||||
func NewServerStream(tracks Tracks) *ServerStream {
|
||||
tracks = tracks.clone()
|
||||
tracks.setControls()
|
||||
|
||||
st := &ServerStream{
|
||||
tracks: tracks,
|
||||
readersUnicast: make(map[*ServerSession]struct{}),
|
||||
readers: make(map[*ServerSession]struct{}),
|
||||
}
|
||||
|
||||
st.tracks = cloneAndClearTracks(tracks)
|
||||
|
||||
st.trackInfos = make([]*trackInfo, len(tracks))
|
||||
for i := range st.trackInfos {
|
||||
st.trackInfos[i] = &trackInfo{}
|
||||
@@ -90,14 +92,13 @@ func (st *ServerStream) ssrc(trackID int) uint32 {
|
||||
func (st *ServerStream) timestamp(trackID int) uint32 {
|
||||
lastTimeRTP := atomic.LoadUint32(&st.trackInfos[trackID].lastTimeRTP)
|
||||
lastTimeNTP := atomic.LoadInt64(&st.trackInfos[trackID].lastTimeNTP)
|
||||
clockRate, _ := st.tracks[trackID].ClockRate()
|
||||
|
||||
if lastTimeRTP == 0 || lastTimeNTP == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return uint32(uint64(lastTimeRTP) +
|
||||
uint64(time.Since(time.Unix(lastTimeNTP, 0)).Seconds()*float64(clockRate)))
|
||||
uint64(time.Since(time.Unix(lastTimeNTP, 0)).Seconds()*float64(st.tracks[trackID].ClockRate())))
|
||||
}
|
||||
|
||||
func (st *ServerStream) lastSequenceNumber(trackID int) uint16 {
|
||||
|
233
track.go
233
track.go
@@ -8,47 +8,83 @@ import (
|
||||
psdp "github.com/pion/sdp/v3"
|
||||
|
||||
"github.com/aler9/gortsplib/pkg/base"
|
||||
"github.com/aler9/gortsplib/pkg/sdp"
|
||||
)
|
||||
|
||||
// Track is a RTSP track.
|
||||
type Track struct {
|
||||
// attributes in SDP format
|
||||
Media *psdp.MediaDescription
|
||||
type Track interface {
|
||||
// ClockRate returns the track clock rate.
|
||||
ClockRate() int
|
||||
clone() Track
|
||||
getControl() string
|
||||
setControl(string)
|
||||
url(*base.URL) (*base.URL, error)
|
||||
mediaDescription() *psdp.MediaDescription
|
||||
}
|
||||
|
||||
func (t *Track) hasControlAttribute() bool {
|
||||
for _, attr := range t.Media.Attributes {
|
||||
if attr.Key == "control" {
|
||||
return true
|
||||
func newTrackFromMediaDescription(md *psdp.MediaDescription) (Track, error) {
|
||||
if md.MediaName.Media == "video" {
|
||||
if rtpmap, ok := md.Attribute("rtpmap"); ok {
|
||||
rtpmap = strings.TrimSpace(rtpmap)
|
||||
|
||||
if vals := strings.Split(rtpmap, " "); len(vals) == 2 && vals[1] == "H264/90000" {
|
||||
tmp, err := strconv.ParseInt(vals[0], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid payload type '%s'", vals[0])
|
||||
}
|
||||
payloadType := uint8(tmp)
|
||||
|
||||
return newTrackH264FromMediaDescription(payloadType, md)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// URL returns the track URL.
|
||||
func (t *Track) URL(contentBase *base.URL) (*base.URL, error) {
|
||||
if contentBase == nil {
|
||||
return nil, fmt.Errorf("no Content-Base header provided")
|
||||
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)
|
||||
}
|
||||
|
||||
controlAttr := func() string {
|
||||
for _, attr := range t.Media.Attributes {
|
||||
if strings.HasPrefix(vals[1], "opus/") {
|
||||
return newTrackOpusFromMediaDescription(payloadType, md)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newTrackGenericFromMediaDescription(md)
|
||||
}
|
||||
|
||||
func trackFindControl(md *psdp.MediaDescription) string {
|
||||
for _, attr := range md.Attributes {
|
||||
if attr.Key == "control" {
|
||||
return attr.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}()
|
||||
}
|
||||
|
||||
func trackURL(t Track, contentBase *base.URL) (*base.URL, error) {
|
||||
if contentBase == nil {
|
||||
return nil, fmt.Errorf("no Content-Base header provided")
|
||||
}
|
||||
|
||||
control := t.getControl()
|
||||
|
||||
// no control attribute, use base URL
|
||||
if controlAttr == "" {
|
||||
if control == "" {
|
||||
return contentBase, nil
|
||||
}
|
||||
|
||||
// control attribute contains an absolute path
|
||||
if strings.HasPrefix(controlAttr, "rtsp://") {
|
||||
ur, err := base.ParseURL(controlAttr)
|
||||
if strings.HasPrefix(control, "rtsp://") {
|
||||
ur, err := base.ParseURL(control)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -64,163 +100,10 @@ func (t *Track) URL(contentBase *base.URL) (*base.URL, error) {
|
||||
// if there's a query, insert it after the query
|
||||
// otherwise insert it after the path
|
||||
strURL := contentBase.String()
|
||||
if controlAttr[0] != '?' && !strings.HasSuffix(strURL, "/") {
|
||||
if control[0] != '?' && !strings.HasSuffix(strURL, "/") {
|
||||
strURL += "/"
|
||||
}
|
||||
ur, _ := base.ParseURL(strURL + controlAttr)
|
||||
|
||||
ur, _ := base.ParseURL(strURL + control)
|
||||
return ur, nil
|
||||
}
|
||||
|
||||
// ClockRate returns the clock rate of the track.
|
||||
func (t *Track) ClockRate() (int, error) {
|
||||
if len(t.Media.MediaName.Formats) < 1 {
|
||||
return 0, fmt.Errorf("no formats provided")
|
||||
}
|
||||
|
||||
// get clock rate from payload type
|
||||
switch t.Media.MediaName.Formats[0] {
|
||||
case "0", "1", "2", "3", "4", "5", "7", "8", "9", "12", "13", "15", "18":
|
||||
return 8000, nil
|
||||
|
||||
case "6":
|
||||
return 16000, nil
|
||||
|
||||
case "10", "11":
|
||||
return 44100, nil
|
||||
|
||||
case "14", "25", "26", "28", "31", "32", "33", "34":
|
||||
return 90000, nil
|
||||
|
||||
case "16":
|
||||
return 11025, nil
|
||||
|
||||
case "17":
|
||||
return 22050, nil
|
||||
}
|
||||
|
||||
// get clock rate from rtpmap
|
||||
// https://tools.ietf.org/html/rfc4566
|
||||
// a=rtpmap:<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
|
||||
}
|
||||
|
160
track_aac.go
160
track_aac.go
@@ -9,79 +9,43 @@ import (
|
||||
psdp "github.com/pion/sdp/v3"
|
||||
|
||||
"github.com/aler9/gortsplib/pkg/aac"
|
||||
"github.com/aler9/gortsplib/pkg/base"
|
||||
)
|
||||
|
||||
// TrackConfigAAC is the configuration of an AAC track.
|
||||
type TrackConfigAAC struct {
|
||||
Type int
|
||||
SampleRate int
|
||||
ChannelCount int
|
||||
AOTSpecificConfig []byte
|
||||
// TrackAAC is an AAC track.
|
||||
type TrackAAC struct {
|
||||
control string
|
||||
payloadType uint8
|
||||
sampleRate int
|
||||
channelCount int
|
||||
mpegConf []byte
|
||||
}
|
||||
|
||||
// NewTrackAAC initializes an AAC track.
|
||||
func NewTrackAAC(payloadType uint8, conf *TrackConfigAAC) (*Track, error) {
|
||||
// NewTrackAAC allocates a TrackAAC.
|
||||
func NewTrackAAC(payloadType uint8, typ int, sampleRate int,
|
||||
channelCount int, aotSpecificConfig []byte) (*TrackAAC, error) {
|
||||
mpegConf, err := aac.MPEG4AudioConfig{
|
||||
Type: aac.MPEG4AudioType(conf.Type),
|
||||
SampleRate: conf.SampleRate,
|
||||
ChannelCount: conf.ChannelCount,
|
||||
AOTSpecificConfig: conf.AOTSpecificConfig,
|
||||
Type: aac.MPEG4AudioType(typ),
|
||||
SampleRate: sampleRate,
|
||||
ChannelCount: channelCount,
|
||||
AOTSpecificConfig: aotSpecificConfig,
|
||||
}.Encode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("invalid configuration: %s", err)
|
||||
}
|
||||
|
||||
typ := strconv.FormatInt(int64(payloadType), 10)
|
||||
|
||||
return &Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{typ},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: typ + " mpeg4-generic/" + strconv.FormatInt(int64(conf.SampleRate), 10) +
|
||||
"/" + strconv.FormatInt(int64(conf.ChannelCount), 10),
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: typ + " profile-level-id=1; " +
|
||||
"mode=AAC-hbr; " +
|
||||
"sizelength=13; " +
|
||||
"indexlength=3; " +
|
||||
"indexdeltalength=3; " +
|
||||
"config=" + hex.EncodeToString(mpegConf),
|
||||
},
|
||||
},
|
||||
},
|
||||
return &TrackAAC{
|
||||
payloadType: payloadType,
|
||||
sampleRate: sampleRate,
|
||||
channelCount: channelCount,
|
||||
mpegConf: mpegConf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsAAC checks whether the track is an AAC track.
|
||||
func (t *Track) IsAAC() bool {
|
||||
if t.Media.MediaName.Media != "audio" {
|
||||
return false
|
||||
}
|
||||
func newTrackAACFromMediaDescription(payloadType uint8, md *psdp.MediaDescription) (*TrackAAC, error) {
|
||||
control := trackFindControl(md)
|
||||
|
||||
v, ok := t.Media.Attribute("rtpmap")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
vals := strings.Split(v, " ")
|
||||
if len(vals) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.HasPrefix(strings.ToLower(vals[1]), "mpeg4-generic/")
|
||||
}
|
||||
|
||||
// ExtractConfigAAC extracts the configuration of an AAC track.
|
||||
func (t *Track) ExtractConfigAAC() (*TrackConfigAAC, error) {
|
||||
v, ok := t.Media.Attribute("fmtp")
|
||||
v, ok := md.Attribute("fmtp")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("fmtp attribute is missing")
|
||||
}
|
||||
@@ -115,16 +79,80 @@ func (t *Track) ExtractConfigAAC() (*TrackConfigAAC, error) {
|
||||
return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1])
|
||||
}
|
||||
|
||||
conf := &TrackConfigAAC{
|
||||
Type: int(mpegConf.Type),
|
||||
SampleRate: mpegConf.SampleRate,
|
||||
ChannelCount: mpegConf.ChannelCount,
|
||||
AOTSpecificConfig: mpegConf.AOTSpecificConfig,
|
||||
// re-encode the conf to normalize it
|
||||
enc, err = mpegConf.Encode()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid AAC config (%v)", tmp[1])
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
return &TrackAAC{
|
||||
control: control,
|
||||
payloadType: payloadType,
|
||||
sampleRate: mpegConf.SampleRate,
|
||||
channelCount: mpegConf.ChannelCount,
|
||||
mpegConf: enc,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("config is missing (%v)", v)
|
||||
}
|
||||
|
||||
// ClockRate returns the track clock rate.
|
||||
func (t *TrackAAC) ClockRate() int {
|
||||
return t.sampleRate
|
||||
}
|
||||
|
||||
func (t *TrackAAC) clone() Track {
|
||||
return &TrackAAC{
|
||||
control: t.control,
|
||||
payloadType: t.payloadType,
|
||||
sampleRate: t.sampleRate,
|
||||
channelCount: t.channelCount,
|
||||
mpegConf: t.mpegConf,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TrackAAC) getControl() string {
|
||||
return t.control
|
||||
}
|
||||
|
||||
func (t *TrackAAC) setControl(c string) {
|
||||
t.control = c
|
||||
}
|
||||
|
||||
func (t *TrackAAC) url(contentBase *base.URL) (*base.URL, error) {
|
||||
return trackURL(t, contentBase)
|
||||
}
|
||||
|
||||
func (t *TrackAAC) mediaDescription() *psdp.MediaDescription {
|
||||
typ := strconv.FormatInt(int64(t.payloadType), 10)
|
||||
|
||||
return &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{typ},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: typ + " mpeg4-generic/" + strconv.FormatInt(int64(t.sampleRate), 10) +
|
||||
"/" + strconv.FormatInt(int64(t.channelCount), 10),
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: typ + " profile-level-id=1; " +
|
||||
"mode=AAC-hbr; " +
|
||||
"sizelength=13; " +
|
||||
"indexlength=3; " +
|
||||
"indexdeltalength=3; " +
|
||||
"config=" + hex.EncodeToString(t.mpegConf),
|
||||
},
|
||||
{
|
||||
Key: "control",
|
||||
Value: t.control,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@@ -8,99 +8,19 @@ import (
|
||||
)
|
||||
|
||||
func TestTrackAACNew(t *testing.T) {
|
||||
track, err := NewTrackAAC(96, &TrackConfigAAC{
|
||||
Type: 2,
|
||||
SampleRate: 48000,
|
||||
ChannelCount: 2,
|
||||
})
|
||||
_, err := NewTrackAAC(96, 2, 48000, 2, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 mpeg4-generic/48000/2",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, track)
|
||||
}
|
||||
|
||||
func TestTrackIsAAC(t *testing.T) {
|
||||
func TestTrackAACNewFromMediaDescription(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
track *Track
|
||||
}{
|
||||
{
|
||||
"standard",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 mpeg4-generic/48000/2",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"uppercase",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 MPEG4-GENERIC/48000/2",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
require.Equal(t, true, ca.track.IsAAC())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackExtractConfigAAC(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
track *Track
|
||||
conf *TrackConfigAAC
|
||||
md *psdp.MediaDescription
|
||||
track *TrackAAC
|
||||
}{
|
||||
{
|
||||
"generic",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -117,17 +37,16 @@ func TestTrackExtractConfigAAC(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&TrackConfigAAC{
|
||||
Type: 2,
|
||||
SampleRate: 48000,
|
||||
ChannelCount: 2,
|
||||
&TrackAAC{
|
||||
payloadType: 96,
|
||||
sampleRate: 48000,
|
||||
channelCount: 2,
|
||||
mpegConf: []byte{0x11, 0x90},
|
||||
},
|
||||
},
|
||||
{
|
||||
"vlc rtsp server",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -144,32 +63,31 @@ func TestTrackExtractConfigAAC(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&TrackConfigAAC{
|
||||
Type: 2,
|
||||
SampleRate: 48000,
|
||||
ChannelCount: 2,
|
||||
&TrackAAC{
|
||||
payloadType: 96,
|
||||
sampleRate: 48000,
|
||||
channelCount: 2,
|
||||
mpegConf: []byte{0x11, 0x90},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
conf, err := ca.track.ExtractConfigAAC()
|
||||
track, err := newTrackAACFromMediaDescription(96, ca.md)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ca.conf, conf)
|
||||
require.Equal(t, ca.track, track)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackConfigAACErrors(t *testing.T) {
|
||||
func TestTrackAACNewFromMediaDescriptionErrors(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
track *Track
|
||||
md *psdp.MediaDescription
|
||||
err string
|
||||
}{
|
||||
{
|
||||
"missing fmtp",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -182,13 +100,11 @@ func TestTrackConfigAACErrors(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"fmtp attribute is missing",
|
||||
},
|
||||
{
|
||||
"invalid fmtp",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -205,13 +121,11 @@ func TestTrackConfigAACErrors(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid fmtp (96)",
|
||||
},
|
||||
{
|
||||
"fmtp without key",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -228,13 +142,11 @@ func TestTrackConfigAACErrors(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid fmtp (96 profile-level-id)",
|
||||
},
|
||||
{
|
||||
"missing config",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -251,13 +163,11 @@ func TestTrackConfigAACErrors(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"config is missing (96 profile-level-id=1)",
|
||||
},
|
||||
{
|
||||
"invalid config",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -274,13 +184,39 @@ func TestTrackConfigAACErrors(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid AAC config (zz)",
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
_, err := ca.track.ExtractConfigAAC()
|
||||
_, err := newTrackAACFromMediaDescription(96, ca.md)
|
||||
require.EqualError(t, err, ca.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackAACMediaDescription(t *testing.T) {
|
||||
track, err := NewTrackAAC(96, 2, 48000, 2, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 mpeg4-generic/48000/2",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
|
||||
},
|
||||
{
|
||||
Key: "control",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}, track.mediaDescription())
|
||||
}
|
||||
|
171
track_generic.go
Normal file
171
track_generic.go
Normal 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
84
track_generic_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
220
track_h264.go
220
track_h264.go
@@ -8,28 +8,139 @@ import (
|
||||
"strings"
|
||||
|
||||
psdp "github.com/pion/sdp/v3"
|
||||
|
||||
"github.com/aler9/gortsplib/pkg/base"
|
||||
)
|
||||
|
||||
// TrackConfigH264 is the configuration of an H264 track.
|
||||
type TrackConfigH264 struct {
|
||||
SPS []byte
|
||||
PPS []byte
|
||||
func trackH264GetSPSPPS(md *psdp.MediaDescription) ([]byte, []byte, error) {
|
||||
v, ok := md.Attribute("fmtp")
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("fmtp attribute is missing")
|
||||
}
|
||||
|
||||
// NewTrackH264 initializes an H264 track.
|
||||
func NewTrackH264(payloadType uint8, conf *TrackConfigH264) (*Track, error) {
|
||||
if len(conf.SPS) < 4 {
|
||||
return nil, fmt.Errorf("invalid SPS")
|
||||
tmp := strings.SplitN(v, " ", 2)
|
||||
if len(tmp) != 2 {
|
||||
return nil, nil, fmt.Errorf("invalid fmtp attribute (%v)", v)
|
||||
}
|
||||
|
||||
spropParameterSets := base64.StdEncoding.EncodeToString(conf.SPS) +
|
||||
"," + base64.StdEncoding.EncodeToString(conf.PPS)
|
||||
profileLevelID := strings.ToUpper(hex.EncodeToString(conf.SPS[1:4]))
|
||||
for _, kv := range strings.Split(tmp[1], ";") {
|
||||
kv = strings.Trim(kv, " ")
|
||||
|
||||
typ := strconv.FormatInt(int64(payloadType), 10)
|
||||
if len(kv) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
return &Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
tmp := strings.SplitN(kv, "=", 2)
|
||||
if len(tmp) != 2 {
|
||||
return nil, nil, fmt.Errorf("invalid fmtp attribute (%v)", v)
|
||||
}
|
||||
|
||||
if tmp[0] == "sprop-parameter-sets" {
|
||||
tmp := strings.Split(tmp[1], ",")
|
||||
if len(tmp) < 2 {
|
||||
return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
|
||||
}
|
||||
|
||||
sps, err := base64.StdEncoding.DecodeString(tmp[0])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
|
||||
}
|
||||
|
||||
pps, err := base64.StdEncoding.DecodeString(tmp[1])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
|
||||
}
|
||||
|
||||
return sps, pps, nil
|
||||
}
|
||||
}
|
||||
|
||||
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"},
|
||||
@@ -42,83 +153,12 @@ func NewTrackH264(payloadType uint8, conf *TrackConfigH264) (*Track, error) {
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: typ + " packetization-mode=1; " +
|
||||
"sprop-parameter-sets=" + spropParameterSets + "; " +
|
||||
"profile-level-id=" + profileLevelID,
|
||||
Value: fmtp,
|
||||
},
|
||||
{
|
||||
Key: "control",
|
||||
Value: t.control,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, 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 {
|
||||
return false
|
||||
}
|
||||
|
||||
v = strings.TrimSpace(v)
|
||||
vals := strings.Split(v, " ")
|
||||
if len(vals) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return vals[1] == "H264/90000"
|
||||
}
|
||||
|
||||
// ExtractConfigH264 extracts the configuration of an H264 track.
|
||||
func (t *Track) ExtractConfigH264() (*TrackConfigH264, error) {
|
||||
v, ok := t.Media.Attribute("fmtp")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("fmtp attribute is missing")
|
||||
}
|
||||
|
||||
tmp := strings.SplitN(v, " ", 2)
|
||||
if len(tmp) != 2 {
|
||||
return nil, fmt.Errorf("invalid fmtp attribute (%v)", v)
|
||||
}
|
||||
|
||||
for _, kv := range strings.Split(tmp[1], ";") {
|
||||
kv = strings.Trim(kv, " ")
|
||||
|
||||
if len(kv) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
tmp := strings.SplitN(kv, "=", 2)
|
||||
if len(tmp) != 2 {
|
||||
return nil, fmt.Errorf("invalid fmtp attribute (%v)", v)
|
||||
}
|
||||
|
||||
if tmp[0] == "sprop-parameter-sets" {
|
||||
tmp := strings.SplitN(tmp[1], ",", 3)
|
||||
if len(tmp) < 2 {
|
||||
return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
|
||||
}
|
||||
|
||||
sps, err := base64.StdEncoding.DecodeString(tmp[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
|
||||
}
|
||||
|
||||
pps, err := base64.StdEncoding.DecodeString(tmp[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sprop-parameter-sets (%v)", v)
|
||||
}
|
||||
|
||||
conf := &TrackConfigH264{
|
||||
SPS: sps,
|
||||
PPS: pps,
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("sprop-parameter-sets is missing (%v)", v)
|
||||
}
|
||||
|
@@ -7,49 +7,32 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTrackH264New(t *testing.T) {
|
||||
tr, err := NewTrackH264(96, &TrackConfigH264{
|
||||
SPS: []byte{
|
||||
0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,
|
||||
0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00,
|
||||
0x00, 0x03, 0x00, 0x3d, 0x08,
|
||||
},
|
||||
PPS: []byte{
|
||||
0x68, 0xee, 0x3c, 0x80,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "video",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 H264/90000",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 packetization-mode=1; " +
|
||||
"sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, tr)
|
||||
}
|
||||
|
||||
func TestTrackIsH264(t *testing.T) {
|
||||
func TestTrackH264GetSPSPPSErrors(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
track *Track
|
||||
md *psdp.MediaDescription
|
||||
err string
|
||||
}{
|
||||
{
|
||||
"standard",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
"missing fmtp",
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "video",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 H264/90000",
|
||||
},
|
||||
},
|
||||
},
|
||||
"fmtp attribute is missing",
|
||||
},
|
||||
{
|
||||
"invalid fmtp",
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "video",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -62,17 +45,15 @@ func TestTrackIsH264(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 packetization-mode=1; " +
|
||||
"sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C",
|
||||
},
|
||||
Value: "96",
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid fmtp attribute (96)",
|
||||
},
|
||||
{
|
||||
"space at the end rtpmap",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
"fmtp without key",
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "video",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -83,27 +64,128 @@ func TestTrackIsH264(t *testing.T) {
|
||||
Key: "rtpmap",
|
||||
Value: "96 H264/90000",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 packetization-mode",
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid fmtp attribute (96 packetization-mode)",
|
||||
},
|
||||
{
|
||||
"missing sprop-parameter-set",
|
||||
&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 is missing (96 packetization-mode=1)",
|
||||
},
|
||||
{
|
||||
"invalid sprop-parameter-set 1",
|
||||
&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 sprop-parameter-sets=aaaaaa",
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa)",
|
||||
},
|
||||
{
|
||||
"invalid sprop-parameter-set 2",
|
||||
&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 sprop-parameter-sets=aaaaaa,bbb",
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa,bbb)",
|
||||
},
|
||||
{
|
||||
"invalid sprop-parameter-set 3",
|
||||
&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 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb",
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid sprop-parameter-sets (96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb)",
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
require.Equal(t, true, ca.track.IsH264())
|
||||
_, _, err := trackH264GetSPSPPS(ca.md)
|
||||
require.EqualError(t, err, ca.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackExtractConfigH264(t *testing.T) {
|
||||
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
|
||||
track *Track
|
||||
conf *TrackConfigH264
|
||||
md *psdp.MediaDescription
|
||||
track *TrackH264
|
||||
}{
|
||||
{
|
||||
"generic",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "video",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -121,22 +203,21 @@ func TestTrackExtractConfigH264(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&TrackConfigH264{
|
||||
SPS: []byte{
|
||||
&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{
|
||||
pps: []byte{
|
||||
0x68, 0xee, 0x3c, 0x80,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"vlc rtsp server",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "video",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -154,195 +235,60 @@ func TestTrackExtractConfigH264(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&TrackConfigH264{
|
||||
SPS: []byte{
|
||||
&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{
|
||||
pps: []byte{
|
||||
0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0,
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
conf, err := ca.track.ExtractConfigH264()
|
||||
track, err := newTrackH264FromMediaDescription(96, ca.md)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ca.conf, conf)
|
||||
require.Equal(t, ca.track, track)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackConfigH264Errors(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
track *Track
|
||||
err string
|
||||
}{
|
||||
{
|
||||
"missing fmtp",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "video",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
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,
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 H264/90000",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"fmtp attribute is missing",
|
||||
},
|
||||
{
|
||||
"invalid fmtp",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "video",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 H264/90000",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid fmtp attribute (96)",
|
||||
},
|
||||
{
|
||||
"fmtp without key",
|
||||
&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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid fmtp attribute (96 packetization-mode)",
|
||||
},
|
||||
{
|
||||
"missing sprop-parameter-set",
|
||||
&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 is missing (96 packetization-mode=1)",
|
||||
},
|
||||
{
|
||||
"invalid sprop-parameter-set 1",
|
||||
&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 sprop-parameter-sets=aaaaaa",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa)",
|
||||
},
|
||||
{
|
||||
"invalid sprop-parameter-set 2",
|
||||
&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 sprop-parameter-sets=aaaaaa,bbb",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid sprop-parameter-sets (96 sprop-parameter-sets=aaaaaa,bbb)",
|
||||
},
|
||||
{
|
||||
"invalid sprop-parameter-set 3",
|
||||
&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 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid sprop-parameter-sets (96 sprop-parameter-sets=Z2QAH6zZQFAFuwFsgAAAAwCAAAAeB4wYyw==,bbb)",
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
_, err := ca.track.ExtractConfigH264()
|
||||
require.EqualError(t, err, ca.err)
|
||||
[]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())
|
||||
}
|
||||
|
137
track_opus.go
137
track_opus.go
@@ -6,67 +6,32 @@ import (
|
||||
"strings"
|
||||
|
||||
psdp "github.com/pion/sdp/v3"
|
||||
|
||||
"github.com/aler9/gortsplib/pkg/base"
|
||||
)
|
||||
|
||||
// TrackConfigOpus is the configuration of an Opus track.
|
||||
type TrackConfigOpus struct {
|
||||
SampleRate int
|
||||
ChannelCount int
|
||||
// TrackOpus is a Opus track.
|
||||
type TrackOpus struct {
|
||||
control string
|
||||
payloadType uint8
|
||||
sampleRate int
|
||||
channelCount int
|
||||
}
|
||||
|
||||
// NewTrackOpus initializes an Opus track.
|
||||
func NewTrackOpus(payloadType uint8, conf *TrackConfigOpus) (*Track, error) {
|
||||
typ := strconv.FormatInt(int64(payloadType), 10)
|
||||
|
||||
return &Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{typ},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: typ + " opus/" + strconv.FormatInt(int64(conf.SampleRate), 10) +
|
||||
"/" + strconv.FormatInt(int64(conf.ChannelCount), 10),
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: typ + " sprop-stereo=" + func() string {
|
||||
if conf.ChannelCount == 2 {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
// NewTrackOpus allocates a TrackOpus.
|
||||
func NewTrackOpus(payloadType uint8, sampleRate int, channelCount int) (*TrackOpus, error) {
|
||||
return &TrackOpus{
|
||||
payloadType: payloadType,
|
||||
sampleRate: sampleRate,
|
||||
channelCount: channelCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsOpus checks whether the track is an Opus track.
|
||||
func (t *Track) IsOpus() bool {
|
||||
if t.Media.MediaName.Media != "audio" {
|
||||
return false
|
||||
}
|
||||
func newTrackOpusFromMediaDescription(payloadType uint8,
|
||||
md *psdp.MediaDescription) (*TrackOpus, error) {
|
||||
control := trackFindControl(md)
|
||||
|
||||
v, ok := t.Media.Attribute("rtpmap")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
vals := strings.Split(v, " ")
|
||||
if len(vals) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.HasPrefix(vals[1], "opus/")
|
||||
}
|
||||
|
||||
// ExtractConfigOpus extracts the configuration of an Opus track.
|
||||
func (t *Track) ExtractConfigOpus() (*TrackConfigOpus, error) {
|
||||
v, ok := t.Media.Attribute("rtpmap")
|
||||
v, ok := md.Attribute("rtpmap")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("rtpmap attribute is missing")
|
||||
}
|
||||
@@ -86,8 +51,68 @@ func (t *Track) ExtractConfigOpus() (*TrackConfigOpus, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TrackConfigOpus{
|
||||
SampleRate: int(sampleRate),
|
||||
ChannelCount: int(channelCount),
|
||||
return &TrackOpus{
|
||||
control: control,
|
||||
payloadType: payloadType,
|
||||
sampleRate: int(sampleRate),
|
||||
channelCount: int(channelCount),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ClockRate returns the track clock rate.
|
||||
func (t *TrackOpus) ClockRate() int {
|
||||
return t.sampleRate
|
||||
}
|
||||
|
||||
func (t *TrackOpus) clone() Track {
|
||||
return &TrackOpus{
|
||||
control: t.control,
|
||||
payloadType: t.payloadType,
|
||||
sampleRate: t.sampleRate,
|
||||
channelCount: t.channelCount,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TrackOpus) getControl() string {
|
||||
return t.control
|
||||
}
|
||||
|
||||
func (t *TrackOpus) setControl(c string) {
|
||||
t.control = c
|
||||
}
|
||||
|
||||
func (t *TrackOpus) url(contentBase *base.URL) (*base.URL, error) {
|
||||
return trackURL(t, contentBase)
|
||||
}
|
||||
|
||||
func (t *TrackOpus) mediaDescription() *psdp.MediaDescription {
|
||||
typ := strconv.FormatInt(int64(t.payloadType), 10)
|
||||
|
||||
return &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{typ},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: typ + " opus/" + strconv.FormatInt(int64(t.sampleRate), 10) +
|
||||
"/" + strconv.FormatInt(int64(t.channelCount), 10),
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: typ + " sprop-stereo=" + func() string {
|
||||
if t.channelCount == 2 {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}(),
|
||||
},
|
||||
{
|
||||
Key: "control",
|
||||
Value: t.control,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@@ -8,76 +8,19 @@ import (
|
||||
)
|
||||
|
||||
func TestTrackOpusNew(t *testing.T) {
|
||||
track, err := NewTrackOpus(96, &TrackConfigOpus{
|
||||
SampleRate: 48000,
|
||||
ChannelCount: 2,
|
||||
})
|
||||
_, err := NewTrackOpus(96, 48000, 2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 opus/48000/2",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 sprop-stereo=1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, track)
|
||||
}
|
||||
|
||||
func TestTrackIsOpus(t *testing.T) {
|
||||
func TestTrackOpusNewFromMediaDescription(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
track *Track
|
||||
}{
|
||||
{
|
||||
"standard",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 opus/48000/2",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 sprop-stereo=1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
require.Equal(t, true, ca.track.IsOpus())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackExtractConfigOpus(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
track *Track
|
||||
conf *TrackConfigOpus
|
||||
md *psdp.MediaDescription
|
||||
track *TrackOpus
|
||||
}{
|
||||
{
|
||||
"generic",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -94,31 +37,30 @@ func TestTrackExtractConfigOpus(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&TrackConfigOpus{
|
||||
SampleRate: 48000,
|
||||
ChannelCount: 2,
|
||||
&TrackOpus{
|
||||
payloadType: 96,
|
||||
sampleRate: 48000,
|
||||
channelCount: 2,
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
conf, err := ca.track.ExtractConfigOpus()
|
||||
track, err := newTrackOpusFromMediaDescription(96, ca.md)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ca.conf, conf)
|
||||
require.Equal(t, ca.track, track)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackConfigOpusErrors(t *testing.T) {
|
||||
func TestTrackOpusNewFromMediaDescriptionErrors(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
track *Track
|
||||
md *psdp.MediaDescription
|
||||
err string
|
||||
}{
|
||||
{
|
||||
"missing rtpmap",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -126,13 +68,11 @@ func TestTrackConfigOpusErrors(t *testing.T) {
|
||||
},
|
||||
Attributes: []psdp.Attribute{},
|
||||
},
|
||||
},
|
||||
"rtpmap attribute is missing",
|
||||
},
|
||||
{
|
||||
"invalid rtpmap",
|
||||
&Track{
|
||||
Media: &psdp.MediaDescription{
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
@@ -145,13 +85,39 @@ func TestTrackConfigOpusErrors(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid rtpmap (96)",
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
_, err := ca.track.ExtractConfigOpus()
|
||||
_, err := newTrackOpusFromMediaDescription(96, ca.md)
|
||||
require.EqualError(t, err, ca.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackOpusMediaDescription(t *testing.T) {
|
||||
track, err := NewTrackOpus(96, 48000, 2)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, &psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 opus/48000/2",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 sprop-stereo=1",
|
||||
},
|
||||
{
|
||||
Key: "control",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}, track.mediaDescription())
|
||||
}
|
||||
|
290
track_test.go
290
track_test.go
@@ -3,11 +3,199 @@ package gortsplib
|
||||
import (
|
||||
"testing"
|
||||
|
||||
psdp "github.com/pion/sdp/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aler9/gortsplib/pkg/base"
|
||||
)
|
||||
|
||||
func TestTrackNewFromMediaDescription(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
md *psdp.MediaDescription
|
||||
track Track
|
||||
}{
|
||||
{
|
||||
"pcma",
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"8"},
|
||||
},
|
||||
},
|
||||
&TrackGeneric{
|
||||
clockRate: 8000,
|
||||
media: "audio",
|
||||
formats: []string{"8"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"aac",
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 mpeg4-generic/48000/2",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
|
||||
},
|
||||
},
|
||||
},
|
||||
&TrackAAC{
|
||||
payloadType: 96,
|
||||
sampleRate: 48000,
|
||||
channelCount: 2,
|
||||
mpegConf: []byte{0x11, 0x90},
|
||||
},
|
||||
},
|
||||
{
|
||||
"aac uppercase",
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 MPEG4-GENERIC/48000/2",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 profile-level-id=1; mode=AAC-hbr; sizelength=13; indexlength=3; indexdeltalength=3; config=1190",
|
||||
},
|
||||
},
|
||||
},
|
||||
&TrackAAC{
|
||||
payloadType: 96,
|
||||
sampleRate: 48000,
|
||||
channelCount: 2,
|
||||
mpegConf: []byte{0x11, 0x90},
|
||||
},
|
||||
},
|
||||
{
|
||||
"opus",
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "audio",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 opus/48000/2",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 sprop-stereo=1",
|
||||
},
|
||||
},
|
||||
},
|
||||
&TrackOpus{
|
||||
payloadType: 96,
|
||||
sampleRate: 48000,
|
||||
channelCount: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
"h264",
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "video",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 H264/90000",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 packetization-mode=1; " +
|
||||
"sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==; profile-level-id=64000C",
|
||||
},
|
||||
},
|
||||
},
|
||||
&TrackH264{
|
||||
payloadType: 96,
|
||||
sps: []byte{
|
||||
0x67, 0x64, 0x00, 0x0c, 0xac, 0x3b, 0x50, 0xb0,
|
||||
0x4b, 0x42, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00,
|
||||
0x00, 0x03, 0x00, 0x3d, 0x08,
|
||||
},
|
||||
pps: []byte{
|
||||
0x68, 0xee, 0x3c, 0x80,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"h264 with a space at the end of rtpmap",
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "video",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 H264/90000 ",
|
||||
},
|
||||
},
|
||||
},
|
||||
&TrackH264{
|
||||
payloadType: 96,
|
||||
},
|
||||
},
|
||||
{
|
||||
"h265",
|
||||
&psdp.MediaDescription{
|
||||
MediaName: psdp.MediaName{
|
||||
Media: "video",
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
Formats: []string{"96"},
|
||||
},
|
||||
Attributes: []psdp.Attribute{
|
||||
{
|
||||
Key: "rtpmap",
|
||||
Value: "96 H265/90000",
|
||||
},
|
||||
{
|
||||
Key: "fmtp",
|
||||
Value: "96 96 sprop-vps=QAEMAf//AWAAAAMAkAAAAwAAAwB4mZgJ; " +
|
||||
"sprop-sps=QgEBAWAAAAMAkAAAAwAAAwB4oAPAgBDllmZpJMrgEAAAAwAQAAADAeCA; sprop-pps=RAHBcrRiQA==",
|
||||
},
|
||||
},
|
||||
},
|
||||
&TrackGeneric{
|
||||
clockRate: 90000,
|
||||
media: "video",
|
||||
formats: []string{"96"},
|
||||
rtpmap: "96 H265/90000",
|
||||
fmtp: "96 96 sprop-vps=QAEMAf//AWAAAAMAkAAAAwAAAwB4mZgJ; " +
|
||||
"sprop-sps=QgEBAWAAAAMAkAAAAwAAAwB4oAPAgBDllmZpJMrgEAAAAwAQAAADAeCA; sprop-pps=RAHBcrRiQA==",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
track, err := newTrackFromMediaDescription(ca.md)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ca.track, track)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackURL(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
@@ -112,109 +300,9 @@ func TestTrackURL(t *testing.T) {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
tracks, err := ReadTracks(ca.sdp)
|
||||
require.NoError(t, err)
|
||||
ur, err := tracks[0].URL(ca.baseURL)
|
||||
ur, err := tracks[0].url(ca.baseURL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ca.ur, ur)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackClockRate(t *testing.T) {
|
||||
for _, ca := range []struct {
|
||||
name string
|
||||
sdp []byte
|
||||
clockRate int
|
||||
}{
|
||||
{
|
||||
"empty encoding parameters",
|
||||
[]byte("v=0\r\n" +
|
||||
"o=- 38990265062388 38990265062388 IN IP4 192.168.1.142\r\n" +
|
||||
"s=RTSP Session\r\n" +
|
||||
"c=IN IP4 192.168.1.142\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"a=control:*\r\n" +
|
||||
"a=range:npt=0-\r\n" +
|
||||
"m=video 0 RTP/AVP 96\r\n" +
|
||||
"a=rtpmap:96 H264/90000 \r\n" +
|
||||
"a=range:npt=0-\r\n" +
|
||||
"a=framerate:0S\r\n" +
|
||||
"a=fmtp:96 profile-level-id=64000c; packetization-mode=1; " +
|
||||
"sprop-parameter-sets=Z2QADKw7ULBLQgAAAwACAAADAD0I,aO48gA==\r\n" +
|
||||
"a=framerate:25\r\n" +
|
||||
"a=control:trackID=3\r\n"),
|
||||
90000,
|
||||
},
|
||||
{
|
||||
"static payload type 1",
|
||||
[]byte("v=0\r\n" +
|
||||
"o=- 38990265062388 38990265062388 IN IP4 192.168.1.142\r\n" +
|
||||
"s=RTSP Session\r\n" +
|
||||
"c=IN IP4 192.168.1.142\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"a=control:*\r\n" +
|
||||
"a=range:npt=0-\r\n" +
|
||||
"m=audio 0 RTP/AVP 8\r\n" +
|
||||
"a=control:trackID=4"),
|
||||
8000,
|
||||
},
|
||||
{
|
||||
"static payload type 2",
|
||||
[]byte("v=0\r\n" +
|
||||
"o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" +
|
||||
"s=SDP Seminar\r\n" +
|
||||
"i=A Seminar on the session description protocol\r\n" +
|
||||
"u=http://www.example.com/seminars/sdp.pdf\r\n" +
|
||||
"e=j.doe@example.com (Jane Doe)\r\n" +
|
||||
"p=+1 617 555-6011\r\n" +
|
||||
"c=IN IP4 224.2.17.12/127\r\n" +
|
||||
"b=X-YZ:128\r\n" +
|
||||
"b=AS:12345\r\n" +
|
||||
"t=2873397496 2873404696\r\n" +
|
||||
"t=3034423619 3042462419\r\n" +
|
||||
"r=604800 3600 0 90000\r\n" +
|
||||
"z=2882844526 -3600 2898848070 0\r\n" +
|
||||
"k=prompt\r\n" +
|
||||
"a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" +
|
||||
"a=recvonly\r\n" +
|
||||
"m=audio 49170 RTP/AVP 0\r\n" +
|
||||
"i=Vivamus a posuere nisl\r\n" +
|
||||
"c=IN IP4 203.0.113.1\r\n" +
|
||||
"b=X-YZ:128\r\n" +
|
||||
"k=prompt\r\n" +
|
||||
"a=sendrecv\r\n"),
|
||||
8000,
|
||||
},
|
||||
{
|
||||
"multiple formats",
|
||||
[]byte("v=0\r\n" +
|
||||
"o=RTSP 1853326073 627916868 IN IP4 0.0.0.0\r\n" +
|
||||
"s=RTSP server\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"a=control:*\r\n" +
|
||||
"a=etag:1234567890\r\n" +
|
||||
"a=range:npt=0-\r\n" +
|
||||
"a=control:*\r\n" +
|
||||
"m=video 0 RTP/AVP 98 96\r\n" +
|
||||
"a=control:trackID=1\r\n" +
|
||||
"b=AS:0\r\n" +
|
||||
"a=rtpmap:98 H265/90000\r\n" +
|
||||
"a=fmtp:98 profile-id=1; sprop-vps=QAEMAf//AWAAAAMAAAMAAAMAAAMAlqwJ; " +
|
||||
"sprop-sps=QgEBAWAAAAMAAAMAAAMAAAMAlqADwIAQ5Za5JMmuWcBSSgAAB9AAAHUwgkA=; sprop-pps=RAHgdrAwxmQ=\r\n" +
|
||||
"m=application 0 RTP/AVP 107\r\n" +
|
||||
"a=control:trackID=3\r\n" +
|
||||
"a=rtpmap:107 vnd.onvif.metadata/90000"),
|
||||
90000,
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
tracks, err := ReadTracks(ca.sdp)
|
||||
require.NoError(t, err)
|
||||
|
||||
clockRate, err := tracks[0].ClockRate()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, clockRate, ca.clockRate)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
83
tracks.go
Normal file
83
tracks.go
Normal 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
42
tracks_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user