Add Retransmission and FEC to TrackLocal

If the MediaEngine contains support for them a SSRC will be generated
appropriately

Co-authored-by: aggresss <aggresss@163.com>
Co-authored-by: Kevin Wang <kevmo314@gmail.com>

Resolves #1989
Resolves #1675
This commit is contained in:
Sean DuBois
2024-10-01 09:23:24 -04:00
parent bd2309f7f4
commit 4541b73b1a
21 changed files with 467 additions and 119 deletions

2
go.mod
View File

@@ -6,7 +6,7 @@ require (
github.com/pion/datachannel v1.5.9 github.com/pion/datachannel v1.5.9
github.com/pion/dtls/v3 v3.0.2 github.com/pion/dtls/v3 v3.0.2
github.com/pion/ice/v4 v4.0.1 github.com/pion/ice/v4 v4.0.1
github.com/pion/interceptor v0.1.31 github.com/pion/interceptor v0.1.32
github.com/pion/logging v0.2.2 github.com/pion/logging v0.2.2
github.com/pion/randutil v0.1.0 github.com/pion/randutil v0.1.0
github.com/pion/rtcp v1.2.14 github.com/pion/rtcp v1.2.14

4
go.sum
View File

@@ -41,8 +41,8 @@ github.com/pion/dtls/v3 v3.0.2 h1:425DEeJ/jfuTTghhUDW0GtYZYIwwMtnKKJNMcWccTX0=
github.com/pion/dtls/v3 v3.0.2/go.mod h1:dfIXcFkKoujDQ+jtd8M6RgqKK3DuaUilm3YatAbGp5k= github.com/pion/dtls/v3 v3.0.2/go.mod h1:dfIXcFkKoujDQ+jtd8M6RgqKK3DuaUilm3YatAbGp5k=
github.com/pion/ice/v4 v4.0.1 h1:2d3tPoTR90F3TcGYeXUwucGlXI3hds96cwv4kjZmb9s= github.com/pion/ice/v4 v4.0.1 h1:2d3tPoTR90F3TcGYeXUwucGlXI3hds96cwv4kjZmb9s=
github.com/pion/ice/v4 v4.0.1/go.mod h1:2dpakjpd7+74L5j3TAe6gvkbI5UIzOgAnkimm9SuHvA= github.com/pion/ice/v4 v4.0.1/go.mod h1:2dpakjpd7+74L5j3TAe6gvkbI5UIzOgAnkimm9SuHvA=
github.com/pion/interceptor v0.1.31 h1:9enhHjP1fDfMI8sqvpO5c/9QuTQnCf2dzPHwwIH4x5w= github.com/pion/interceptor v0.1.32 h1:DYbusOBhWKjPMiA5ifyczW03Tnh12gCaYn4VOvLMGk4=
github.com/pion/interceptor v0.1.31/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= github.com/pion/interceptor v0.1.32/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=

View File

@@ -159,7 +159,7 @@ func (i *interceptorToTrackLocalWriter) Write(b []byte) (int, error) {
return i.WriteRTP(&packet.Header, packet.Payload) return i.WriteRTP(&packet.Header, packet.Payload)
} }
func createStreamInfo(id string, ssrc SSRC, payloadType PayloadType, codec RTPCodecCapability, webrtcHeaderExtensions []RTPHeaderExtensionParameter) *interceptor.StreamInfo { func createStreamInfo(id string, ssrc, ssrcFEC, ssrcRTX SSRC, payloadType PayloadType, codec RTPCodecCapability, webrtcHeaderExtensions []RTPHeaderExtensionParameter) *interceptor.StreamInfo {
headerExtensions := make([]interceptor.RTPHeaderExtension, 0, len(webrtcHeaderExtensions)) headerExtensions := make([]interceptor.RTPHeaderExtension, 0, len(webrtcHeaderExtensions))
for _, h := range webrtcHeaderExtensions { for _, h := range webrtcHeaderExtensions {
headerExtensions = append(headerExtensions, interceptor.RTPHeaderExtension{ID: h.ID, URI: h.URI}) headerExtensions = append(headerExtensions, interceptor.RTPHeaderExtension{ID: h.ID, URI: h.URI})
@@ -171,15 +171,17 @@ func createStreamInfo(id string, ssrc SSRC, payloadType PayloadType, codec RTPCo
} }
return &interceptor.StreamInfo{ return &interceptor.StreamInfo{
ID: id, ID: id,
Attributes: interceptor.Attributes{}, Attributes: interceptor.Attributes{},
SSRC: uint32(ssrc), SSRC: uint32(ssrc),
PayloadType: uint8(payloadType), SSRCRetransmission: uint32(ssrcRTX),
RTPHeaderExtensions: headerExtensions, SSRCForwardErrorCorrection: uint32(ssrcFEC),
MimeType: codec.MimeType, PayloadType: uint8(payloadType),
ClockRate: codec.ClockRate, RTPHeaderExtensions: headerExtensions,
Channels: codec.Channels, MimeType: codec.MimeType,
SDPFmtpLine: codec.SDPFmtpLine, ClockRate: codec.ClockRate,
RTCPFeedback: feedbacks, Channels: codec.Channels,
SDPFmtpLine: codec.SDPFmtpLine,
RTCPFeedback: feedbacks,
} }
} }

View File

@@ -196,10 +196,10 @@ func Test_Interceptor_BindUnbind(t *testing.T) {
if cnt := atomic.LoadUint32(&cntUnbindLocalStream); cnt != 1 { if cnt := atomic.LoadUint32(&cntUnbindLocalStream); cnt != 1 {
t.Errorf("UnbindLocalStreamFn is expected to be called once, but called %d times", cnt) t.Errorf("UnbindLocalStreamFn is expected to be called once, but called %d times", cnt)
} }
if cnt := atomic.LoadUint32(&cntBindRemoteStream); cnt != 1 { if cnt := atomic.LoadUint32(&cntBindRemoteStream); cnt != 2 {
t.Errorf("BindRemoteStreamFn is expected to be called once, but called %d times", cnt) t.Errorf("BindRemoteStreamFn is expected to be called once, but called %d times", cnt)
} }
if cnt := atomic.LoadUint32(&cntUnbindRemoteStream); cnt != 1 { if cnt := atomic.LoadUint32(&cntUnbindRemoteStream); cnt != 2 {
t.Errorf("UnbindRemoteStreamFn is expected to be called once, but called %d times", cnt) t.Errorf("UnbindRemoteStreamFn is expected to be called once, but called %d times", cnt)
} }
@@ -207,7 +207,7 @@ func Test_Interceptor_BindUnbind(t *testing.T) {
if cnt := atomic.LoadUint32(&cntBindRTCPWriter); cnt != 2 { if cnt := atomic.LoadUint32(&cntBindRTCPWriter); cnt != 2 {
t.Errorf("BindRTCPWriterFn is expected to be called twice, but called %d times", cnt) t.Errorf("BindRTCPWriterFn is expected to be called twice, but called %d times", cnt)
} }
if cnt := atomic.LoadUint32(&cntBindRTCPReader); cnt != 2 { if cnt := atomic.LoadUint32(&cntBindRTCPReader); cnt != 3 {
t.Errorf("BindRTCPReaderFn is expected to be called twice, but called %d times", cnt) t.Errorf("BindRTCPReaderFn is expected to be called twice, but called %d times", cnt)
} }
if cnt := atomic.LoadUint32(&cntClose); cnt != 2 { if cnt := atomic.LoadUint32(&cntClose); cnt != 2 {

View File

@@ -47,6 +47,12 @@ const (
// MimeTypePCMA PCMA MIME type // MimeTypePCMA PCMA MIME type
// Note: Matching should be case insensitive. // Note: Matching should be case insensitive.
MimeTypePCMA = "audio/PCMA" MimeTypePCMA = "audio/PCMA"
// MimeTypeRTX RTX MIME type
// Note: Matching should be case insensitive.
MimeTypeRTX = "video/rtx"
// MimeTypeFlexFEC FEC MIME Type
// Note: Matching should be case insensitive.
MimeTypeFlexFEC = "video/flexfec"
) )
type mediaEngineHeaderExtension struct { type mediaEngineHeaderExtension struct {
@@ -106,7 +112,7 @@ func (m *MediaEngine) RegisterDefaultCodecs() error {
PayloadType: 96, PayloadType: 96,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=96", nil},
PayloadType: 97, PayloadType: 97,
}, },
@@ -115,7 +121,7 @@ func (m *MediaEngine) RegisterDefaultCodecs() error {
PayloadType: 102, PayloadType: 102,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=102", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=102", nil},
PayloadType: 103, PayloadType: 103,
}, },
@@ -124,7 +130,7 @@ func (m *MediaEngine) RegisterDefaultCodecs() error {
PayloadType: 104, PayloadType: 104,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=104", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=104", nil},
PayloadType: 105, PayloadType: 105,
}, },
@@ -133,7 +139,7 @@ func (m *MediaEngine) RegisterDefaultCodecs() error {
PayloadType: 106, PayloadType: 106,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=106", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=106", nil},
PayloadType: 107, PayloadType: 107,
}, },
@@ -142,7 +148,7 @@ func (m *MediaEngine) RegisterDefaultCodecs() error {
PayloadType: 108, PayloadType: 108,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=108", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=108", nil},
PayloadType: 109, PayloadType: 109,
}, },
@@ -151,7 +157,7 @@ func (m *MediaEngine) RegisterDefaultCodecs() error {
PayloadType: 127, PayloadType: 127,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=127", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=127", nil},
PayloadType: 125, PayloadType: 125,
}, },
@@ -160,7 +166,7 @@ func (m *MediaEngine) RegisterDefaultCodecs() error {
PayloadType: 39, PayloadType: 39,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=39", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=39", nil},
PayloadType: 40, PayloadType: 40,
}, },
@@ -169,7 +175,7 @@ func (m *MediaEngine) RegisterDefaultCodecs() error {
PayloadType: 45, PayloadType: 45,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=45", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=45", nil},
PayloadType: 46, PayloadType: 46,
}, },
@@ -178,7 +184,7 @@ func (m *MediaEngine) RegisterDefaultCodecs() error {
PayloadType: 98, PayloadType: 98,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=98", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=98", nil},
PayloadType: 99, PayloadType: 99,
}, },
@@ -187,7 +193,7 @@ func (m *MediaEngine) RegisterDefaultCodecs() error {
PayloadType: 100, PayloadType: 100,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=100", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=100", nil},
PayloadType: 101, PayloadType: 101,
}, },
@@ -196,7 +202,7 @@ func (m *MediaEngine) RegisterDefaultCodecs() error {
PayloadType: 112, PayloadType: 112,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=112", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=112", nil},
PayloadType: 113, PayloadType: 113,
}, },
} { } {
@@ -702,3 +708,23 @@ func payloaderForCodec(codec RTPCodecCapability) (rtp.Payloader, error) {
return nil, ErrNoPayloaderForCodec return nil, ErrNoPayloaderForCodec
} }
} }
func (m *MediaEngine) isRTXEnabled(typ RTPCodecType, directions []RTPTransceiverDirection) bool {
for _, p := range m.getRTPParametersByKind(typ, directions).Codecs {
if p.MimeType == MimeTypeRTX {
return true
}
}
return false
}
func (m *MediaEngine) isFECEnabled(typ RTPCodecType, directions []RTPTransceiverDirection) bool {
for _, p := range m.getRTPParametersByKind(typ, directions).Codecs {
if strings.Contains(p.MimeType, MimeTypeFlexFEC) {
return true
}
}
return false
}

View File

@@ -364,7 +364,7 @@ a=fmtp:97 apt=96
PayloadType: 96, PayloadType: 96,
}, RTPCodecTypeVideo)) }, RTPCodecTypeVideo))
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=96", nil},
PayloadType: 97, PayloadType: 97,
}, RTPCodecTypeVideo)) }, RTPCodecTypeVideo))
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
@@ -372,7 +372,7 @@ a=fmtp:97 apt=96
PayloadType: 102, PayloadType: 102,
}, RTPCodecTypeVideo)) }, RTPCodecTypeVideo))
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=102", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=102", nil},
PayloadType: 103, PayloadType: 103,
}, RTPCodecTypeVideo)) }, RTPCodecTypeVideo))
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
@@ -380,7 +380,7 @@ a=fmtp:97 apt=96
PayloadType: 104, PayloadType: 104,
}, RTPCodecTypeVideo)) }, RTPCodecTypeVideo))
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=104", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=104", nil},
PayloadType: 105, PayloadType: 105,
}, RTPCodecTypeVideo)) }, RTPCodecTypeVideo))
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
@@ -388,7 +388,7 @@ a=fmtp:97 apt=96
PayloadType: 98, PayloadType: 98,
}, RTPCodecTypeVideo)) }, RTPCodecTypeVideo))
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=98", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=98", nil},
PayloadType: 99, PayloadType: 99,
}, RTPCodecTypeVideo)) }, RTPCodecTypeVideo))
assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels))) assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels)))
@@ -400,7 +400,7 @@ a=fmtp:97 apt=96
assert.Equal(t, vp9Codec.MimeType, MimeTypeVP9) assert.Equal(t, vp9Codec.MimeType, MimeTypeVP9)
vp9RTX, _, err := m.getCodecByPayload(97) vp9RTX, _, err := m.getCodecByPayload(97)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, vp9RTX.MimeType, "video/rtx") assert.Equal(t, vp9RTX.MimeType, MimeTypeRTX)
h264P1Codec, _, err := m.getCodecByPayload(106) h264P1Codec, _, err := m.getCodecByPayload(106)
assert.NoError(t, err) assert.NoError(t, err)
@@ -408,7 +408,7 @@ a=fmtp:97 apt=96
assert.Equal(t, h264P1Codec.SDPFmtpLine, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f") assert.Equal(t, h264P1Codec.SDPFmtpLine, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f")
h264P1RTX, _, err := m.getCodecByPayload(107) h264P1RTX, _, err := m.getCodecByPayload(107)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, h264P1RTX.MimeType, "video/rtx") assert.Equal(t, h264P1RTX.MimeType, MimeTypeRTX)
assert.Equal(t, h264P1RTX.SDPFmtpLine, "apt=106") assert.Equal(t, h264P1RTX.SDPFmtpLine, "apt=106")
h264P0Codec, _, err := m.getCodecByPayload(108) h264P0Codec, _, err := m.getCodecByPayload(108)
@@ -417,7 +417,7 @@ a=fmtp:97 apt=96
assert.Equal(t, h264P0Codec.SDPFmtpLine, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f") assert.Equal(t, h264P0Codec.SDPFmtpLine, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f")
h264P0RTX, _, err := m.getCodecByPayload(109) h264P0RTX, _, err := m.getCodecByPayload(109)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, h264P0RTX.MimeType, "video/rtx") assert.Equal(t, h264P0RTX.MimeType, MimeTypeRTX)
assert.Equal(t, h264P0RTX.SDPFmtpLine, "apt=108") assert.Equal(t, h264P0RTX.SDPFmtpLine, "apt=108")
}) })
@@ -443,7 +443,7 @@ a=fmtp:97 apt=96
PayloadType: 96, PayloadType: 96,
}, RTPCodecTypeVideo)) }, RTPCodecTypeVideo))
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=96", nil},
PayloadType: 97, PayloadType: 97,
}, RTPCodecTypeVideo)) }, RTPCodecTypeVideo))
assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels))) assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels)))

View File

@@ -1048,6 +1048,11 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
return err return err
} }
// Disable RTX/FEC on RTPSenders if the remote didn't support it
for _, sender := range pc.GetSenders() {
sender.configureRTXAndFEC()
}
var t *RTPTransceiver var t *RTPTransceiver
localTransceivers := append([]*RTPTransceiver{}, pc.GetTransceivers()...) localTransceivers := append([]*RTPTransceiver{}, pc.GetTransceivers()...)
detectedPlanB := descriptionIsPlanB(pc.RemoteDescription(), pc.log) detectedPlanB := descriptionIsPlanB(pc.RemoteDescription(), pc.log)
@@ -1616,7 +1621,7 @@ func (pc *PeerConnection) handleIncomingSSRC(rtpStream io.Reader, ssrc SSRC) err
return err return err
} }
streamInfo := createStreamInfo("", ssrc, params.Codecs[0].PayloadType, params.Codecs[0].RTPCodecCapability, params.HeaderExtensions) streamInfo := createStreamInfo("", ssrc, 0, 0, params.Codecs[0].PayloadType, params.Codecs[0].RTPCodecCapability, params.HeaderExtensions)
readStream, interceptor, rtcpReadStream, rtcpInterceptor, err := pc.dtlsTransport.streamsForSSRC(ssrc, *streamInfo) readStream, interceptor, rtcpReadStream, rtcpInterceptor, err := pc.dtlsTransport.streamsForSSRC(ssrc, *streamInfo)
if err != nil { if err != nil {
return err return err

View File

@@ -21,7 +21,6 @@ import (
"time" "time"
"github.com/pion/logging" "github.com/pion/logging"
"github.com/pion/randutil"
"github.com/pion/rtcp" "github.com/pion/rtcp"
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/pion/sdp/v3" "github.com/pion/sdp/v3"
@@ -778,7 +777,7 @@ func TestAddTransceiverFromTrackFailsRecvOnly(t *testing.T) {
func TestPlanBMediaExchange(t *testing.T) { func TestPlanBMediaExchange(t *testing.T) {
runTest := func(trackCount int, t *testing.T) { runTest := func(trackCount int, t *testing.T) {
addSingleTrack := func(p *PeerConnection) *TrackLocalStaticSample { addSingleTrack := func(p *PeerConnection) *TrackLocalStaticSample {
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()), fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32())) track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, fmt.Sprintf("video-%d", util.RandUint32()), fmt.Sprintf("video-%d", util.RandUint32()))
assert.NoError(t, err) assert.NoError(t, err)
_, err = p.AddTrack(track) _, err = p.AddTrack(track)
@@ -1020,7 +1019,7 @@ func TestPeerConnection_Simulcast_Probe(t *testing.T) {
if len(track.bindings) == 1 { if len(track.bindings) == 1 {
_, err = track.bindings[0].writeStream.WriteRTP(&rtp.Header{ _, err = track.bindings[0].writeStream.WriteRTP(&rtp.Header{
Version: 2, Version: 2,
SSRC: randutil.NewMathRandomGenerator().Uint32(), SSRC: util.RandUint32(),
}, []byte{0, 1, 2, 3, 4, 5}) }, []byte{0, 1, 2, 3, 4, 5})
assert.NoError(t, err) assert.NoError(t, err)
} }

View File

@@ -10,9 +10,9 @@ import (
"io" "io"
"os" "os"
"github.com/pion/randutil"
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/pion/rtp/codecs" "github.com/pion/rtp/codecs"
"github.com/pion/webrtc/v4/internal/util"
) )
const ( const (
@@ -68,7 +68,7 @@ func NewWith(out io.Writer, sampleRate uint32, channelCount uint16) (*OggWriter,
stream: out, stream: out,
sampleRate: sampleRate, sampleRate: sampleRate,
channelCount: channelCount, channelCount: channelCount,
serial: randutil.NewMathRandomGenerator().Uint32(), serial: util.RandUint32(),
checksumTable: generateChecksumTable(), checksumTable: generateChecksumTable(),
// Timestamp and Granule MUST start from 1 // Timestamp and Granule MUST start from 1

View File

@@ -4,6 +4,7 @@
package webrtc package webrtc
import ( import (
"fmt"
"strings" "strings"
"github.com/pion/webrtc/v4/internal/fmtp" "github.com/pion/webrtc/v4/internal/fmtp"
@@ -123,3 +124,15 @@ func codecParametersFuzzySearch(needle RTPCodecParameters, haystack []RTPCodecPa
return RTPCodecParameters{}, codecMatchNone return RTPCodecParameters{}, codecMatchNone
} }
// Given a CodecParameters find the RTX CodecParameters if one exists
func findRTXCodecParameters(needle PayloadType, haystack []RTPCodecParameters) (RTPCodecParameters, bool) {
aptStr := fmt.Sprintf("apt=%d", needle)
for _, c := range haystack {
if aptStr == c.SDPFmtpLine {
return c, true
}
}
return RTPCodecParameters{}, false
}

View File

@@ -9,6 +9,12 @@ type RTPRtxParameters struct {
SSRC SSRC `json:"ssrc"` SSRC SSRC `json:"ssrc"`
} }
// RTPFecParameters dictionary contains information relating to forward error correction (FEC) settings.
// https://draft.ortc.org/#dom-rtcrtpfecparameters
type RTPFecParameters struct {
SSRC SSRC `json:"ssrc"`
}
// RTPCodingParameters provides information relating to both encoding and decoding. // RTPCodingParameters provides information relating to both encoding and decoding.
// This is a subset of the RFC since Pion WebRTC doesn't implement encoding/decoding itself // This is a subset of the RFC since Pion WebRTC doesn't implement encoding/decoding itself
// http://draft.ortc.org/#dom-rtcrtpcodingparameters // http://draft.ortc.org/#dom-rtcrtpcodingparameters
@@ -17,4 +23,5 @@ type RTPCodingParameters struct {
SSRC SSRC `json:"ssrc"` SSRC SSRC `json:"ssrc"`
PayloadType PayloadType `json:"payloadType"` PayloadType PayloadType `json:"payloadType"`
RTX RTPRtxParameters `json:"rtx"` RTX RTPRtxParameters `json:"rtx"`
FEC RTPFecParameters `json:"fec"`
} }

View File

@@ -210,14 +210,14 @@ func (r *RTPReceiver) startReceive(parameters RTPReceiveParameters) error {
return fmt.Errorf("%w: %d", errRTPReceiverWithSSRCTrackStreamNotFound, parameters.Encodings[i].SSRC) return fmt.Errorf("%w: %d", errRTPReceiverWithSSRCTrackStreamNotFound, parameters.Encodings[i].SSRC)
} }
t.streamInfo = createStreamInfo("", parameters.Encodings[i].SSRC, 0, codec, globalParams.HeaderExtensions) t.streamInfo = createStreamInfo("", parameters.Encodings[i].SSRC, 0, 0, 0, codec, globalParams.HeaderExtensions)
var err error var err error
if t.rtpReadStream, t.rtpInterceptor, t.rtcpReadStream, t.rtcpInterceptor, err = r.transport.streamsForSSRC(parameters.Encodings[i].SSRC, *t.streamInfo); err != nil { if t.rtpReadStream, t.rtpInterceptor, t.rtcpReadStream, t.rtcpInterceptor, err = r.transport.streamsForSSRC(parameters.Encodings[i].SSRC, *t.streamInfo); err != nil {
return err return err
} }
if rtxSsrc := parameters.Encodings[i].RTX.SSRC; rtxSsrc != 0 { if rtxSsrc := parameters.Encodings[i].RTX.SSRC; rtxSsrc != 0 {
streamInfo := createStreamInfo("", rtxSsrc, 0, codec, globalParams.HeaderExtensions) streamInfo := createStreamInfo("", rtxSsrc, 0, 0, 0, codec, globalParams.HeaderExtensions)
rtpReadStream, rtpInterceptor, rtcpReadStream, rtcpInterceptor, err := r.transport.streamsForSSRC(rtxSsrc, *streamInfo) rtpReadStream, rtpInterceptor, rtcpReadStream, rtcpInterceptor, err := r.transport.streamsForSSRC(rtxSsrc, *streamInfo)
if err != nil { if err != nil {
return err return err

View File

@@ -7,18 +7,13 @@
package webrtc package webrtc
import ( import (
"bufio"
"context" "context"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt"
"io" "io"
"strconv"
"strings"
"testing" "testing"
"time" "time"
"github.com/pion/randutil"
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/pion/sdp/v3" "github.com/pion/sdp/v3"
"github.com/pion/transport/v3/test" "github.com/pion/transport/v3/test"
@@ -86,19 +81,18 @@ func TestSetRTPParameters(t *testing.T) {
func Test_RTX_Read(t *testing.T) { func Test_RTX_Read(t *testing.T) {
defer test.TimeOut(time.Second * 30).Stop() defer test.TimeOut(time.Second * 30).Stop()
var ssrc *uint32
ssrcLines := ""
rtxSsrc := randutil.NewMathRandomGenerator().Uint32()
pcOffer, pcAnswer, err := newPair() pcOffer, pcAnswer, err := newPair()
assert.NoError(t, err) assert.NoError(t, err)
track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "track-id", "stream-id") track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "track-id", "stream-id")
assert.NoError(t, err) assert.NoError(t, err)
_, err = pcOffer.AddTrack(track) rtpSender, err := pcOffer.AddTrack(track)
assert.NoError(t, err) assert.NoError(t, err)
rtxSsrc := rtpSender.GetParameters().Encodings[0].RTX.SSRC
ssrc := rtpSender.GetParameters().Encodings[0].SSRC
rtxRead, rtxReadCancel := context.WithCancel(context.Background()) rtxRead, rtxReadCancel := context.WithCancel(context.Background())
pcAnswer.OnTrack(func(track *TrackRemote, _ *RTPReceiver) { pcAnswer.OnTrack(func(track *TrackRemote, _ *RTPReceiver) {
for { for {
@@ -111,7 +105,7 @@ func Test_RTX_Read(t *testing.T) {
assert.NoError(t, readRTPErr) assert.NoError(t, readRTPErr)
assert.NotNil(t, pkt) assert.NotNil(t, pkt)
assert.Equal(t, pkt.SSRC, *ssrc) assert.Equal(t, pkt.SSRC, uint32(ssrc))
assert.Equal(t, pkt.PayloadType, uint8(96)) assert.Equal(t, pkt.PayloadType, uint8(96))
assert.Equal(t, pkt.Payload, []byte{0xB, 0xA, 0xD}) assert.Equal(t, pkt.Payload, []byte{0xB, 0xA, 0xD})
@@ -120,7 +114,7 @@ func Test_RTX_Read(t *testing.T) {
rtxSSRC := attributes.Get(AttributeRtxSsrc) rtxSSRC := attributes.Get(AttributeRtxSsrc)
if rtxPayloadType != nil && rtxSequenceNumber != nil && rtxSSRC != nil { if rtxPayloadType != nil && rtxSequenceNumber != nil && rtxSSRC != nil {
assert.Equal(t, rtxPayloadType, uint8(97)) assert.Equal(t, rtxPayloadType, uint8(97))
assert.Equal(t, rtxSSRC, rtxSsrc) assert.Equal(t, rtxSSRC, uint32(rtxSsrc))
assert.Equal(t, rtxSequenceNumber, pkt.SequenceNumber+500) assert.Equal(t, rtxSequenceNumber, pkt.SequenceNumber+500)
rtxReadCancel() rtxReadCancel()
@@ -128,42 +122,14 @@ func Test_RTX_Read(t *testing.T) {
} }
}) })
assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(offer string) (modified string) { assert.NoError(t, signalPair(pcOffer, pcAnswer))
scanner := bufio.NewScanner(strings.NewReader(offer))
for scanner.Scan() {
l := scanner.Text()
if strings.HasPrefix(l, "a=ssrc") {
if ssrc == nil {
lineSplit := strings.Split(l, " ")[0]
parsed, atoiErr := strconv.ParseUint(strings.TrimPrefix(lineSplit, "a=ssrc:"), 10, 32)
assert.NoError(t, atoiErr)
parsedSsrc := uint32(parsed)
ssrc = &parsedSsrc
modified += fmt.Sprintf("a=ssrc-group:FID %d %d\r\n", *ssrc, rtxSsrc)
}
ssrcLines += l + "\n"
} else if ssrcLines != "" {
ssrcLines = strings.ReplaceAll(ssrcLines, fmt.Sprintf("%d", *ssrc), fmt.Sprintf("%d", rtxSsrc))
modified += ssrcLines
ssrcLines = ""
}
modified += l + "\n"
}
return modified
}))
func() { func() {
for i := uint16(0); ; i++ { for i := uint16(0); ; i++ {
pkt := rtp.Packet{ pkt := rtp.Packet{
Header: rtp.Header{ Header: rtp.Header{
Version: 2, Version: 2,
SSRC: *ssrc, SSRC: uint32(ssrc),
PayloadType: 96, PayloadType: 96,
SequenceNumber: i, SequenceNumber: i,
}, },
@@ -182,7 +148,7 @@ func Test_RTX_Read(t *testing.T) {
// Send the RTX // Send the RTX
_, err = track.bindings[0].writeStream.WriteRTP(&rtp.Header{ _, err = track.bindings[0].writeStream.WriteRTP(&rtp.Header{
Version: 2, Version: 2,
SSRC: rtxSsrc, SSRC: uint32(rtxSsrc),
PayloadType: 97, PayloadType: 97,
SequenceNumber: i + 500, SequenceNumber: i + 500,
}, rtxPayload) }, rtxPayload)

View File

@@ -29,7 +29,7 @@ type trackEncoding struct {
context *baseTrackLocalContext context *baseTrackLocalContext
ssrc SSRC ssrc, ssrcRTX, ssrcFEC SSRC
} }
// RTPSender allows an application to control how a given Track is encoded and transmitted to a remote peer // RTPSender allows an application to control how a given Track is encoded and transmitted to a remote peer
@@ -110,7 +110,9 @@ func (r *RTPSender) Transport() *DTLSTransport {
return r.transport return r.transport
} }
func (r *RTPSender) getParameters() RTPSendParameters { // GetParameters describes the current configuration for the encoding and
// transmission of media on the sender's track.
func (r *RTPSender) GetParameters() RTPSendParameters {
r.mu.RLock() r.mu.RLock()
defer r.mu.RUnlock() defer r.mu.RUnlock()
@@ -124,6 +126,8 @@ func (r *RTPSender) getParameters() RTPSendParameters {
RTPCodingParameters: RTPCodingParameters{ RTPCodingParameters: RTPCodingParameters{
RID: rid, RID: rid,
SSRC: trackEncoding.ssrc, SSRC: trackEncoding.ssrc,
RTX: RTPRtxParameters{SSRC: trackEncoding.ssrcRTX},
FEC: RTPFecParameters{SSRC: trackEncoding.ssrcFEC},
PayloadType: r.payloadType, PayloadType: r.payloadType,
}, },
}) })
@@ -143,14 +147,6 @@ func (r *RTPSender) getParameters() RTPSendParameters {
return sendParameters return sendParameters
} }
// GetParameters describes the current configuration for the encoding and
// transmission of media on the sender's track.
func (r *RTPSender) GetParameters() RTPSendParameters {
r.mu.RLock()
defer r.mu.RUnlock()
return r.getParameters()
}
// AddEncoding adds an encoding to RTPSender. Used by simulcast senders. // AddEncoding adds an encoding to RTPSender. Used by simulcast senders.
func (r *RTPSender) AddEncoding(track TrackLocal) error { func (r *RTPSender) AddEncoding(track TrackLocal) error {
r.mu.Lock() r.mu.Lock()
@@ -201,7 +197,15 @@ func (r *RTPSender) AddEncoding(track TrackLocal) error {
func (r *RTPSender) addEncoding(track TrackLocal) { func (r *RTPSender) addEncoding(track TrackLocal) {
trackEncoding := &trackEncoding{ trackEncoding := &trackEncoding{
track: track, track: track,
ssrc: SSRC(randutil.NewMathRandomGenerator().Uint32()), ssrc: SSRC(util.RandUint32()),
}
if r.api.mediaEngine.isRTXEnabled(r.kind, []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}) {
trackEncoding.ssrcRTX = SSRC(util.RandUint32())
}
if r.api.mediaEngine.isFECEnabled(r.kind, []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}) {
trackEncoding.ssrcFEC = SSRC(util.RandUint32())
} }
r.trackEncodings = append(r.trackEncodings, trackEncoding) r.trackEncodings = append(r.trackEncodings, trackEncoding)
@@ -261,6 +265,8 @@ func (r *RTPSender) ReplaceTrack(track TrackLocal) error {
id: context.ID(), id: context.ID(),
params: r.api.mediaEngine.getRTPParametersByKind(track.Kind(), []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}), params: r.api.mediaEngine.getRTPParametersByKind(track.Kind(), []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}),
ssrc: context.SSRC(), ssrc: context.SSRC(),
ssrcRTX: context.SSRCRetransmission(),
ssrcFEC: context.SSRCForwardErrorCorrection(),
writeStream: context.WriteStream(), writeStream: context.WriteStream(),
rtcpInterceptor: context.RTCPReader(), rtcpInterceptor: context.RTCPReader(),
}) })
@@ -302,10 +308,14 @@ func (r *RTPSender) Send(parameters RTPSendParameters) error {
trackEncoding.srtpStream = srtpStream trackEncoding.srtpStream = srtpStream
trackEncoding.ssrc = parameters.Encodings[idx].SSRC trackEncoding.ssrc = parameters.Encodings[idx].SSRC
trackEncoding.ssrcRTX = parameters.Encodings[idx].RTX.SSRC
trackEncoding.ssrcFEC = parameters.Encodings[idx].FEC.SSRC
trackEncoding.context = &baseTrackLocalContext{ trackEncoding.context = &baseTrackLocalContext{
id: r.id, id: r.id,
params: r.api.mediaEngine.getRTPParametersByKind(trackEncoding.track.Kind(), []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}), params: r.api.mediaEngine.getRTPParametersByKind(trackEncoding.track.Kind(), []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}),
ssrc: parameters.Encodings[idx].SSRC, ssrc: parameters.Encodings[idx].SSRC,
ssrcFEC: parameters.Encodings[idx].FEC.SSRC,
ssrcRTX: parameters.Encodings[idx].RTX.SSRC,
writeStream: writeStream, writeStream: writeStream,
rtcpInterceptor: trackEncoding.rtcpInterceptor, rtcpInterceptor: trackEncoding.rtcpInterceptor,
} }
@@ -319,6 +329,8 @@ func (r *RTPSender) Send(parameters RTPSendParameters) error {
trackEncoding.streamInfo = *createStreamInfo( trackEncoding.streamInfo = *createStreamInfo(
r.id, r.id,
parameters.Encodings[idx].SSRC, parameters.Encodings[idx].SSRC,
parameters.Encodings[idx].RTX.SSRC,
parameters.Encodings[idx].FEC.SSRC,
codec.PayloadType, codec.PayloadType,
codec.RTPCodecCapability, codec.RTPCodecCapability,
parameters.HeaderExtensions, parameters.HeaderExtensions,
@@ -467,3 +479,20 @@ func (r *RTPSender) hasStopped() bool {
return false return false
} }
} }
// Set a SSRC for FEC and RTX if MediaEngine has them enabled
// If the remote doesn't support FEC or RTX we disable locally
func (r *RTPSender) configureRTXAndFEC() {
r.mu.RLock()
defer r.mu.RUnlock()
for _, trackEncoding := range r.trackEncodings {
if !r.api.mediaEngine.isRTXEnabled(r.kind, []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}) {
trackEncoding.ssrcRTX = SSRC(0)
}
if !r.api.mediaEngine.isFECEnabled(r.kind, []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}) {
trackEncoding.ssrcFEC = SSRC(0)
}
}
}

View File

@@ -401,3 +401,85 @@ func Test_RTPSender_Add_Encoding(t *testing.T) {
assert.NoError(t, peerConnection.Close()) assert.NoError(t, peerConnection.Close())
} }
// nolint: dupl
func Test_RTPSender_FEC_Support(t *testing.T) {
t.Run("FEC disabled by default", func(t *testing.T) {
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
peerConnection, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
rtpSender, err := peerConnection.AddTrack(track)
assert.NoError(t, err)
assert.Zero(t, rtpSender.GetParameters().Encodings[0].FEC.SSRC)
assert.NoError(t, peerConnection.Close())
})
t.Run("FEC can be enabled", func(t *testing.T) {
m := MediaEngine{}
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
PayloadType: 94,
}, RTPCodecTypeVideo))
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{MimeTypeFlexFEC, 90000, 0, "", nil},
PayloadType: 95,
}, RTPCodecTypeVideo))
api := NewAPI(WithMediaEngine(&m))
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
peerConnection, err := api.NewPeerConnection(Configuration{})
assert.NoError(t, err)
rtpSender, err := peerConnection.AddTrack(track)
assert.NoError(t, err)
assert.NotZero(t, rtpSender.GetParameters().Encodings[0].FEC.SSRC)
assert.NoError(t, peerConnection.Close())
})
}
// nolint: dupl
func Test_RTPSender_RTX_Support(t *testing.T) {
t.Run("RTX SSRC by Default", func(t *testing.T) {
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
peerConnection, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
rtpSender, err := peerConnection.AddTrack(track)
assert.NoError(t, err)
assert.NotZero(t, rtpSender.GetParameters().Encodings[0].RTX.SSRC)
assert.NoError(t, peerConnection.Close())
})
t.Run("RTX can be disabled", func(t *testing.T) {
m := MediaEngine{}
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
PayloadType: 94,
}, RTPCodecTypeVideo))
api := NewAPI(WithMediaEngine(&m))
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
peerConnection, err := api.NewPeerConnection(Configuration{})
assert.NoError(t, err)
rtpSender, err := peerConnection.AddTrack(track)
assert.NoError(t, err)
assert.Zero(t, rtpSender.GetParameters().Encodings[0].RTX.SSRC)
assert.NoError(t, peerConnection.Close())
})
}

View File

@@ -60,7 +60,7 @@ func Test_RTPTransceiver_SetCodecPreferences(t *testing.T) {
PayloadType: 96, PayloadType: 96,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=96", nil},
PayloadType: 97, PayloadType: 97,
}, },
@@ -69,7 +69,7 @@ func Test_RTPTransceiver_SetCodecPreferences(t *testing.T) {
PayloadType: 98, PayloadType: 98,
}, },
{ {
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=98", nil}, RTPCodecCapability: RTPCodecCapability{MimeTypeRTX, 90000, 0, "apt=98", nil},
PayloadType: 99, PayloadType: 99,
}, },
}, },

9
sdp.go
View File

@@ -392,7 +392,16 @@ func addSenderSDP(
sendParameters := sender.GetParameters() sendParameters := sender.GetParameters()
for _, encoding := range sendParameters.Encodings { for _, encoding := range sendParameters.Encodings {
if encoding.RTX.SSRC != 0 {
media = media.WithValueAttribute("ssrc-group", fmt.Sprintf("FID %d %d", encoding.SSRC, encoding.RTX.SSRC))
}
if encoding.FEC.SSRC != 0 {
media = media.WithValueAttribute("ssrc-group", fmt.Sprintf("FEC-FR %d %d", encoding.SSRC, encoding.FEC.SSRC))
}
media = media.WithMediaSource(uint32(encoding.SSRC), track.StreamID() /* cname */, track.StreamID() /* streamLabel */, track.ID()) media = media.WithMediaSource(uint32(encoding.SSRC), track.StreamID() /* cname */, track.StreamID() /* streamLabel */, track.ID())
if !isPlanB { if !isPlanB {
media = media.WithPropertyAttribute("msid:" + track.StreamID() + " " + track.ID()) media = media.WithPropertyAttribute("msid:" + track.StreamID() + " " + track.ID())
} }

View File

@@ -14,6 +14,7 @@ import (
"testing" "testing"
"github.com/pion/sdp/v3" "github.com/pion/sdp/v3"
"github.com/pion/transport/v3/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -746,3 +747,166 @@ func TestRtpExtensionsFromMediaDescription(t *testing.T) {
assert.Equal(t, extensions[sdp.ABSSendTimeURI], 1) assert.Equal(t, extensions[sdp.ABSSendTimeURI], 1)
assert.Equal(t, extensions[sdp.SDESMidURI], 3) assert.Equal(t, extensions[sdp.SDESMidURI], 3)
} }
// Assert that FEC and RTX SSRCes are present if they are enabled in the MediaEngine
func Test_SSRC_Groups(t *testing.T) {
const offerWithRTX = `v=0
o=- 930222930247584370 1727933945 IN IP4 0.0.0.0
s=-
t=0 0
a=msid-semantic:WMS*
a=fingerprint:sha-256 11:3F:1C:8D:D4:1D:8D:E7:E1:3E:AF:38:06:0D:1D:40:22:DC:FE:C9:93:E4:80:D8:0B:17:9F:2E:C1:CA:C8:3D
a=extmap-allow-mixed
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 101
c=IN IP4 0.0.0.0
a=setup:actpass
a=mid:0
a=ice-ufrag:yIgpPUMarFReduuM
a=ice-pwd:VmnVaqCByWiOTatFoDBbMGhSFGlsxviz
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:101 opus/90000
a=rtcp-fb:101 transport-cc
a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=ssrc:3566446228 cname:stream-id
a=ssrc:3566446228 msid:stream-id audio-id
a=ssrc:3566446228 mslabel:stream-id
a=ssrc:3566446228 label:audio-id
a=msid:stream-id audio-id
a=sendrecv
m=video 9 UDP/TLS/RTP/SAVPF 96 97
c=IN IP4 0.0.0.0
a=setup:actpass
a=mid:1
a=ice-ufrag:yIgpPUMarFReduuM
a=ice-pwd:VmnVaqCByWiOTatFoDBbMGhSFGlsxviz
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtcp-fb:96 transport-cc
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=ssrc-group:FID 1701050765 2578535262
a=ssrc:1701050765 cname:stream-id
a=ssrc:1701050765 msid:stream-id track-id
a=ssrc:1701050765 mslabel:stream-id
a=ssrc:1701050765 label:track-id
a=msid:stream-id track-id
a=sendrecv
`
const offerNoRTX = `v=0
o=- 930222930247584370 1727933945 IN IP4 0.0.0.0
s=-
t=0 0
a=msid-semantic:WMS*
a=fingerprint:sha-256 11:3F:1C:8D:D4:1D:8D:E7:E1:3E:AF:38:06:0D:1D:40:22:DC:FE:C9:93:E4:80:D8:0B:17:9F:2E:C1:CA:C8:3D
a=extmap-allow-mixed
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 101
a=mid:0
a=ice-ufrag:yIgpPUMarFReduuM
a=ice-pwd:VmnVaqCByWiOTatFoDBbMGhSFGlsxviz
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:101 opus/90000
a=rtcp-fb:101 transport-cc
a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=ssrc:3566446228 cname:stream-id
a=ssrc:3566446228 msid:stream-id audio-id
a=ssrc:3566446228 mslabel:stream-id
a=ssrc:3566446228 label:audio-id
a=msid:stream-id audio-id
a=sendrecv
m=video 9 UDP/TLS/RTP/SAVPF 96
c=IN IP4 0.0.0.0
a=setup:actpass
a=mid:1
a=ice-ufrag:yIgpPUMarFReduuM
a=ice-pwd:VmnVaqCByWiOTatFoDBbMGhSFGlsxviz
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtcp-fb:96 transport-cc
a=ssrc-group:FID 1701050765 2578535262
a=ssrc:1701050765 cname:stream-id
a=ssrc:1701050765 msid:stream-id track-id
a=ssrc:1701050765 mslabel:stream-id
a=ssrc:1701050765 label:track-id
a=msid:stream-id track-id
a=sendrecv
`
defer test.CheckRoutines(t)()
for _, testCase := range []struct {
name string
enableRTXInMediaEngine bool
rtxExpected bool
remoteOffer string
}{
{"Offer", true, true, ""},
{"Offer no Local Groups", false, false, ""},
{"Answer", true, true, offerWithRTX},
{"Answer No Local Groups", false, false, offerWithRTX},
{"Answer No Remote Groups", true, false, offerNoRTX},
} {
t.Run(testCase.name, func(t *testing.T) {
checkRTXSupport := func(s *sdp.SessionDescription) {
// RTX is never enabled for audio
assert.Nil(t, trackDetailsFromSDP(nil, s)[0].repairSsrc)
// RTX is conditionally enabled for video
if testCase.rtxExpected {
assert.NotNil(t, trackDetailsFromSDP(nil, s)[1].repairSsrc)
} else {
assert.Nil(t, trackDetailsFromSDP(nil, s)[1].repairSsrc)
}
}
m := &MediaEngine{}
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeOpus, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
PayloadType: 101,
}, RTPCodecTypeAudio))
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
PayloadType: 96,
}, RTPCodecTypeVideo))
if testCase.enableRTXInMediaEngine {
assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeRTX, ClockRate: 90000, Channels: 0, SDPFmtpLine: "apt=96", RTCPFeedback: nil},
PayloadType: 97,
}, RTPCodecTypeVideo))
}
peerConnection, err := NewAPI(WithMediaEngine(m)).NewPeerConnection(Configuration{})
assert.NoError(t, err)
audioTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "audio-id", "stream-id")
assert.NoError(t, err)
_, err = peerConnection.AddTrack(audioTrack)
assert.NoError(t, err)
videoTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video-id", "stream-id")
assert.NoError(t, err)
_, err = peerConnection.AddTrack(videoTrack)
assert.NoError(t, err)
if testCase.remoteOffer == "" {
offer, err := peerConnection.CreateOffer(nil)
assert.NoError(t, err)
checkRTXSupport(offer.parsed)
} else {
assert.NoError(t, peerConnection.SetRemoteDescription(SessionDescription{Type: SDPTypeOffer, SDP: testCase.remoteOffer}))
answer, err := peerConnection.CreateAnswer(nil)
assert.NoError(t, err)
checkRTXSupport(answer.parsed)
}
assert.NoError(t, peerConnection.Close())
})
}
}

View File

@@ -21,17 +21,22 @@ type TrackLocalWriter interface {
// in Interceptors. // in Interceptors.
type TrackLocalContext interface { type TrackLocalContext interface {
// CodecParameters returns the negotiated RTPCodecParameters. These are the codecs supported by both // CodecParameters returns the negotiated RTPCodecParameters. These are the codecs supported by both
// PeerConnections and the SSRC/PayloadTypes // PeerConnections and the PayloadTypes
CodecParameters() []RTPCodecParameters CodecParameters() []RTPCodecParameters
// HeaderExtensions returns the negotiated RTPHeaderExtensionParameters. These are the header extensions supported by // HeaderExtensions returns the negotiated RTPHeaderExtensionParameters. These are the header extensions supported by
// both PeerConnections and the SSRC/PayloadTypes // both PeerConnections and the URI/IDs
HeaderExtensions() []RTPHeaderExtensionParameter HeaderExtensions() []RTPHeaderExtensionParameter
// SSRC requires the negotiated SSRC of this track // SSRC returns the negotiated SSRC of this track
// This track may have multiple if RTX is enabled
SSRC() SSRC SSRC() SSRC
// SSRCRetransmission returns the negotiated SSRC used to send retransmissions for this track
SSRCRetransmission() SSRC
// SSRCForwardErrorCorrection returns the negotiated SSRC to send forward error correction for this track
SSRCForwardErrorCorrection() SSRC
// WriteStream returns the WriteStream for this TrackLocal. The implementer writes the outbound // WriteStream returns the WriteStream for this TrackLocal. The implementer writes the outbound
// media packets to it // media packets to it
WriteStream() TrackLocalWriter WriteStream() TrackLocalWriter
@@ -44,11 +49,11 @@ type TrackLocalContext interface {
} }
type baseTrackLocalContext struct { type baseTrackLocalContext struct {
id string id string
params RTPParameters params RTPParameters
ssrc SSRC ssrc, ssrcRTX, ssrcFEC SSRC
writeStream TrackLocalWriter writeStream TrackLocalWriter
rtcpInterceptor interceptor.RTCPReader rtcpInterceptor interceptor.RTCPReader
} }
// CodecParameters returns the negotiated RTPCodecParameters. These are the codecs supported by both // CodecParameters returns the negotiated RTPCodecParameters. These are the codecs supported by both
@@ -64,11 +69,20 @@ func (t *baseTrackLocalContext) HeaderExtensions() []RTPHeaderExtensionParameter
} }
// SSRC requires the negotiated SSRC of this track // SSRC requires the negotiated SSRC of this track
// This track may have multiple if RTX is enabled
func (t *baseTrackLocalContext) SSRC() SSRC { func (t *baseTrackLocalContext) SSRC() SSRC {
return t.ssrc return t.ssrc
} }
// SSRCRetransmission returns the negotiated SSRC used to send retransmissions for this track
func (t *baseTrackLocalContext) SSRCRetransmission() SSRC {
return t.ssrcRTX
}
// SSRCForwardErrorCorrection returns the negotiated SSRC to send forward error correction for this track
func (t *baseTrackLocalContext) SSRCForwardErrorCorrection() SSRC {
return t.ssrcFEC
}
// WriteStream returns the WriteStream for this TrackLocal. The implementer writes the outbound // WriteStream returns the WriteStream for this TrackLocal. The implementer writes the outbound
// media packets to it // media packets to it
func (t *baseTrackLocalContext) WriteStream() TrackLocalWriter { func (t *baseTrackLocalContext) WriteStream() TrackLocalWriter {

View File

@@ -19,10 +19,10 @@ import (
// Bind can be called multiple times, this stores the // Bind can be called multiple times, this stores the
// result for a single bind call so that it can be used when writing // result for a single bind call so that it can be used when writing
type trackBinding struct { type trackBinding struct {
id string id string
ssrc SSRC ssrc, ssrcRTX, ssrcFEC SSRC
payloadType PayloadType payloadType, payloadTypeRTX PayloadType
writeStream TrackLocalWriter writeStream TrackLocalWriter
} }
// TrackLocalStaticRTP is a TrackLocal that has a pre-set codec and accepts RTP Packets. // TrackLocalStaticRTP is a TrackLocal that has a pre-set codec and accepts RTP Packets.
@@ -59,19 +59,28 @@ func WithRTPStreamID(rid string) func(*TrackLocalStaticRTP) {
// Bind is called by the PeerConnection after negotiation is complete // Bind is called by the PeerConnection after negotiation is complete
// This asserts that the code requested is supported by the remote peer. // This asserts that the code requested is supported by the remote peer.
// If so it setups all the state (SSRC and PayloadType) to have a call // If so it sets up all the state (SSRC and PayloadType) to have a call
func (s *TrackLocalStaticRTP) Bind(t TrackLocalContext) (RTPCodecParameters, error) { func (s *TrackLocalStaticRTP) Bind(t TrackLocalContext) (RTPCodecParameters, error) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
parameters := RTPCodecParameters{RTPCodecCapability: s.codec} parameters := RTPCodecParameters{RTPCodecCapability: s.codec}
if codec, matchType := codecParametersFuzzySearch(parameters, t.CodecParameters()); matchType != codecMatchNone { if codec, matchType := codecParametersFuzzySearch(parameters, t.CodecParameters()); matchType != codecMatchNone {
var payloadTypeRTX PayloadType
if rtxParameters, ok := findRTXCodecParameters(codec.PayloadType, t.CodecParameters()); ok {
payloadTypeRTX = rtxParameters.PayloadType
}
s.bindings = append(s.bindings, trackBinding{ s.bindings = append(s.bindings, trackBinding{
ssrc: t.SSRC(), ssrc: t.SSRC(),
payloadType: codec.PayloadType, ssrcRTX: t.SSRCRetransmission(),
writeStream: t.WriteStream(), ssrcFEC: t.SSRCForwardErrorCorrection(),
id: t.ID(), payloadType: codec.PayloadType,
payloadTypeRTX: payloadTypeRTX,
writeStream: t.WriteStream(),
id: t.ID(),
}) })
return codec, nil return codec, nil
} }

View File

@@ -313,3 +313,26 @@ func Test_TrackLocalStatic_Padding(t *testing.T) {
closePairNow(t, offerer, answerer) closePairNow(t, offerer, answerer)
} }
func Test_TrackLocalStatic_RTX(t *testing.T) {
defer test.TimeOut(time.Second * 30).Stop()
defer test.CheckRoutines(t)()
offerer, answerer, err := newPair()
assert.NoError(t, err)
track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = offerer.AddTrack(track)
assert.NoError(t, err)
assert.NoError(t, signalPair(offerer, answerer))
track.mu.Lock()
assert.NotZero(t, track.bindings[0].ssrcRTX)
assert.NotZero(t, track.bindings[0].payloadTypeRTX)
track.mu.Unlock()
closePairNow(t, offerer, answerer)
}