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