mirror of
https://github.com/pion/webrtc.git
synced 2025-10-25 00:00:30 +08:00
Introduces AddEncoding method in RTP sender to add simulcast encodings. Added UTs for AddEncoding. Also modified the Simulcast send test to use the new API.
1147 lines
33 KiB
Go
1147 lines
33 KiB
Go
//go:build !js
|
|
// +build !js
|
|
|
|
package webrtc
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pion/rtp"
|
|
"github.com/pion/transport/test"
|
|
"github.com/pion/webrtc/v3/internal/util"
|
|
"github.com/pion/webrtc/v3/pkg/media"
|
|
"github.com/pion/webrtc/v3/pkg/rtcerr"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func sendVideoUntilDone(done <-chan struct{}, t *testing.T, tracks []*TrackLocalStaticSample) {
|
|
for {
|
|
select {
|
|
case <-time.After(20 * time.Millisecond):
|
|
for _, track := range tracks {
|
|
assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}))
|
|
}
|
|
case <-done:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func sdpMidHasSsrc(offer SessionDescription, mid string, ssrc SSRC) bool {
|
|
for _, media := range offer.parsed.MediaDescriptions {
|
|
cmid, ok := media.Attribute("mid")
|
|
if !ok {
|
|
continue
|
|
}
|
|
if cmid != mid {
|
|
continue
|
|
}
|
|
cssrc, ok := media.Attribute("ssrc")
|
|
if !ok {
|
|
continue
|
|
}
|
|
parts := strings.Split(cssrc, " ")
|
|
|
|
ssrcInt64, err := strconv.ParseUint(parts[0], 10, 32)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if uint32(ssrcInt64) == uint32(ssrc) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestPeerConnection_Renegotiation_AddRecvonlyTransceiver(t *testing.T) {
|
|
type testCase struct {
|
|
name string
|
|
answererSends bool
|
|
}
|
|
|
|
testCases := []testCase{
|
|
// Assert the following behaviors:
|
|
// - Offerer can add a recvonly transceiver
|
|
// - During negotiation, answerer peer adds an inactive (or sendonly) transceiver
|
|
// - Offerer can add a track
|
|
// - Answerer can receive the RTP packets.
|
|
{"add recvonly, then receive from answerer", false},
|
|
// Assert the following behaviors:
|
|
// - Offerer can add a recvonly transceiver
|
|
// - During negotiation, answerer peer adds an inactive (or sendonly) transceiver
|
|
// - Answerer can add a track to the existing sendonly transceiver
|
|
// - Offerer can receive the RTP packets.
|
|
{"add recvonly, then send to answerer", true},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcOffer, pcAnswer, err := newPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = pcOffer.AddTransceiverFromKind(
|
|
RTPCodecTypeVideo,
|
|
RTPTransceiverInit{
|
|
Direction: RTPTransceiverDirectionRecvonly,
|
|
},
|
|
)
|
|
assert.NoError(t, err)
|
|
|
|
assert.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
|
|
localTrack, err := NewTrackLocalStaticSample(
|
|
RTPCodecCapability{MimeType: "video/VP8"}, "track-one", "stream-one",
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
if tc.answererSends {
|
|
_, err = pcAnswer.AddTrack(localTrack)
|
|
} else {
|
|
_, err = pcOffer.AddTrack(localTrack)
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
|
|
|
|
if tc.answererSends {
|
|
pcOffer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
|
|
onTrackFiredFunc()
|
|
})
|
|
} else {
|
|
pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
|
|
onTrackFiredFunc()
|
|
})
|
|
}
|
|
|
|
assert.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
|
|
sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{localTrack})
|
|
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
})
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Assert the following behaviors
|
|
* - We are able to call AddTrack after signaling
|
|
* - OnTrack is NOT called on the other side until after SetRemoteDescription
|
|
* - We are able to re-negotiate and AddTrack is properly called
|
|
*/
|
|
func TestPeerConnection_Renegotiation_AddTrack(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcOffer, pcAnswer, err := newPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
haveRenegotiated := &atomicBool{}
|
|
onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
|
|
pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
|
|
if !haveRenegotiated.get() {
|
|
t.Fatal("OnTrack was called before renegotiation")
|
|
}
|
|
onTrackFiredFunc()
|
|
})
|
|
|
|
assert.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
|
|
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
|
|
assert.NoError(t, err)
|
|
|
|
vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
|
|
assert.NoError(t, err)
|
|
|
|
sender, err := pcOffer.AddTrack(vp8Track)
|
|
assert.NoError(t, err)
|
|
|
|
// Send 10 packets, OnTrack MUST not be fired
|
|
for i := 0; i <= 10; i++ {
|
|
assert.NoError(t, vp8Track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}))
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
|
|
haveRenegotiated.set(true)
|
|
assert.False(t, sender.isNegotiated())
|
|
offer, err := pcOffer.CreateOffer(nil)
|
|
assert.True(t, sender.isNegotiated())
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, pcOffer.SetLocalDescription(offer))
|
|
assert.NoError(t, pcAnswer.SetRemoteDescription(offer))
|
|
answer, err := pcAnswer.CreateAnswer(nil)
|
|
assert.NoError(t, err)
|
|
|
|
assert.NoError(t, pcAnswer.SetLocalDescription(answer))
|
|
|
|
pcOffer.ops.Done()
|
|
assert.Equal(t, 0, len(vp8Track.rtpTrack.bindings))
|
|
|
|
assert.NoError(t, pcOffer.SetRemoteDescription(answer))
|
|
|
|
pcOffer.ops.Done()
|
|
assert.Equal(t, 1, len(vp8Track.rtpTrack.bindings))
|
|
|
|
sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{vp8Track})
|
|
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
}
|
|
|
|
// Assert that adding tracks across multiple renegotiations performs as expected
|
|
func TestPeerConnection_Renegotiation_AddTrack_Multiple(t *testing.T) {
|
|
addTrackWithLabel := func(trackID string, pcOffer, pcAnswer *PeerConnection) *TrackLocalStaticSample {
|
|
_, err := pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
|
|
assert.NoError(t, err)
|
|
|
|
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, trackID, trackID)
|
|
assert.NoError(t, err)
|
|
|
|
_, err = pcOffer.AddTrack(track)
|
|
assert.NoError(t, err)
|
|
|
|
return track
|
|
}
|
|
|
|
trackIDs := []string{util.MathRandAlpha(16), util.MathRandAlpha(16), util.MathRandAlpha(16)}
|
|
outboundTracks := []*TrackLocalStaticSample{}
|
|
onTrackCount := map[string]int{}
|
|
onTrackChan := make(chan struct{}, 1)
|
|
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcOffer, pcAnswer, err := newPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
|
|
onTrackCount[track.ID()]++
|
|
onTrackChan <- struct{}{}
|
|
})
|
|
|
|
assert.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
|
|
for i := range trackIDs {
|
|
outboundTracks = append(outboundTracks, addTrackWithLabel(trackIDs[i], pcOffer, pcAnswer))
|
|
assert.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
sendVideoUntilDone(onTrackChan, t, outboundTracks)
|
|
}
|
|
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
|
|
assert.Equal(t, onTrackCount[trackIDs[0]], 1)
|
|
assert.Equal(t, onTrackCount[trackIDs[1]], 1)
|
|
assert.Equal(t, onTrackCount[trackIDs[2]], 1)
|
|
}
|
|
|
|
// Assert that renegotiation triggers OnTrack() with correct ID and label from
|
|
// remote side, even when a transceiver was added before the actual track data
|
|
// was received. This happens when we add a transceiver on the server, create
|
|
// an offer on the server and the browser's answer contains the same SSRC, but
|
|
// a track hasn't been added on the browser side yet. The browser can add a
|
|
// track later and renegotiate, and track ID and label will be set by the time
|
|
// first packets are received.
|
|
func TestPeerConnection_Renegotiation_AddTrack_Rename(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcOffer, pcAnswer, err := newPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
haveRenegotiated := &atomicBool{}
|
|
onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
|
|
var atomicRemoteTrack atomic.Value
|
|
pcOffer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
|
|
if !haveRenegotiated.get() {
|
|
t.Fatal("OnTrack was called before renegotiation")
|
|
}
|
|
onTrackFiredFunc()
|
|
atomicRemoteTrack.Store(track)
|
|
})
|
|
|
|
_, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
|
|
assert.NoError(t, err)
|
|
vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo1", "bar1")
|
|
assert.NoError(t, err)
|
|
_, err = pcAnswer.AddTrack(vp8Track)
|
|
assert.NoError(t, err)
|
|
|
|
assert.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
|
|
vp8Track.rtpTrack.id = "foo2"
|
|
vp8Track.rtpTrack.streamID = "bar2"
|
|
|
|
haveRenegotiated.set(true)
|
|
assert.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
|
|
sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{vp8Track})
|
|
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
|
|
remoteTrack, ok := atomicRemoteTrack.Load().(*TrackRemote)
|
|
require.True(t, ok)
|
|
require.NotNil(t, remoteTrack)
|
|
assert.Equal(t, "foo2", remoteTrack.ID())
|
|
assert.Equal(t, "bar2", remoteTrack.StreamID())
|
|
}
|
|
|
|
// TestPeerConnection_Transceiver_Mid tests that we'll provide the same
|
|
// transceiver for a media id on successive offer/answer
|
|
func TestPeerConnection_Transceiver_Mid(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcOffer, err := NewPeerConnection(Configuration{})
|
|
assert.NoError(t, err)
|
|
|
|
pcAnswer, err := NewPeerConnection(Configuration{})
|
|
assert.NoError(t, err)
|
|
|
|
track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1")
|
|
require.NoError(t, err)
|
|
|
|
sender1, err := pcOffer.AddTrack(track1)
|
|
require.NoError(t, err)
|
|
|
|
track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2")
|
|
require.NoError(t, err)
|
|
|
|
sender2, err := pcOffer.AddTrack(track2)
|
|
require.NoError(t, err)
|
|
|
|
// this will create the initial offer using generateUnmatchedSDP
|
|
offer, err := pcOffer.CreateOffer(nil)
|
|
assert.NoError(t, err)
|
|
|
|
offerGatheringComplete := GatheringCompletePromise(pcOffer)
|
|
assert.NoError(t, pcOffer.SetLocalDescription(offer))
|
|
<-offerGatheringComplete
|
|
|
|
assert.NoError(t, pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription()))
|
|
|
|
answer, err := pcAnswer.CreateAnswer(nil)
|
|
assert.NoError(t, err)
|
|
|
|
answerGatheringComplete := GatheringCompletePromise(pcAnswer)
|
|
assert.NoError(t, pcAnswer.SetLocalDescription(answer))
|
|
<-answerGatheringComplete
|
|
|
|
// apply answer so we'll test generateMatchedSDP
|
|
assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()))
|
|
|
|
pcOffer.ops.Done()
|
|
pcAnswer.ops.Done()
|
|
|
|
// Must have 3 media descriptions (2 video channels)
|
|
assert.Equal(t, len(offer.parsed.MediaDescriptions), 2)
|
|
|
|
assert.True(t, sdpMidHasSsrc(offer, "0", sender1.trackEncodings[0].ssrc), "Expected mid %q with ssrc %d, offer.SDP: %s", "0", sender1.trackEncodings[0].ssrc, offer.SDP)
|
|
|
|
// Remove first track, must keep same number of media
|
|
// descriptions and same track ssrc for mid 1 as previous
|
|
assert.NoError(t, pcOffer.RemoveTrack(sender1))
|
|
|
|
offer, err = pcOffer.CreateOffer(nil)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, len(offer.parsed.MediaDescriptions), 2)
|
|
|
|
assert.True(t, sdpMidHasSsrc(offer, "1", sender2.trackEncodings[0].ssrc), "Expected mid %q with ssrc %d, offer.SDP: %s", "1", sender2.trackEncodings[0].ssrc, offer.SDP)
|
|
|
|
_, err = pcAnswer.CreateAnswer(nil)
|
|
assert.Equal(t, err, &rtcerr.InvalidStateError{Err: ErrIncorrectSignalingState})
|
|
|
|
pcOffer.ops.Done()
|
|
pcAnswer.ops.Done()
|
|
|
|
track3, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion3")
|
|
require.NoError(t, err)
|
|
|
|
sender3, err := pcOffer.AddTrack(track3)
|
|
require.NoError(t, err)
|
|
|
|
offer, err = pcOffer.CreateOffer(nil)
|
|
assert.NoError(t, err)
|
|
|
|
// We reuse the existing non-sending transceiver
|
|
assert.Equal(t, len(offer.parsed.MediaDescriptions), 2)
|
|
|
|
assert.True(t, sdpMidHasSsrc(offer, "0", sender3.trackEncodings[0].ssrc), "Expected mid %q with ssrc %d, offer.sdp: %s", "0", sender3.trackEncodings[0].ssrc, offer.SDP)
|
|
assert.True(t, sdpMidHasSsrc(offer, "1", sender2.trackEncodings[0].ssrc), "Expected mid %q with ssrc %d, offer.sdp: %s", "1", sender2.trackEncodings[0].ssrc, offer.SDP)
|
|
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
}
|
|
|
|
func TestPeerConnection_Renegotiation_CodecChange(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcOffer, err := NewPeerConnection(Configuration{})
|
|
assert.NoError(t, err)
|
|
|
|
pcAnswer, err := NewPeerConnection(Configuration{})
|
|
assert.NoError(t, err)
|
|
|
|
track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video1", "pion1")
|
|
require.NoError(t, err)
|
|
|
|
track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video2", "pion2")
|
|
require.NoError(t, err)
|
|
|
|
sender1, err := pcOffer.AddTrack(track1)
|
|
require.NoError(t, err)
|
|
|
|
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
|
|
require.NoError(t, err)
|
|
|
|
tracksCh := make(chan *TrackRemote)
|
|
tracksClosed := make(chan struct{})
|
|
pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
|
|
tracksCh <- track
|
|
for {
|
|
if _, _, readErr := track.ReadRTP(); readErr == io.EOF {
|
|
tracksClosed <- struct{}{}
|
|
return
|
|
}
|
|
}
|
|
})
|
|
|
|
err = signalPair(pcOffer, pcAnswer)
|
|
require.NoError(t, err)
|
|
|
|
transceivers := pcOffer.GetTransceivers()
|
|
require.Equal(t, 1, len(transceivers))
|
|
require.Equal(t, "0", transceivers[0].Mid())
|
|
|
|
transceivers = pcAnswer.GetTransceivers()
|
|
require.Equal(t, 1, len(transceivers))
|
|
require.Equal(t, "0", transceivers[0].Mid())
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
go sendVideoUntilDone(ctx.Done(), t, []*TrackLocalStaticSample{track1})
|
|
|
|
remoteTrack1 := <-tracksCh
|
|
cancel()
|
|
|
|
assert.Equal(t, "video1", remoteTrack1.ID())
|
|
assert.Equal(t, "pion1", remoteTrack1.StreamID())
|
|
|
|
require.NoError(t, pcOffer.RemoveTrack(sender1))
|
|
|
|
sender2, err := pcOffer.AddTrack(track2)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
<-tracksClosed
|
|
|
|
transceivers = pcOffer.GetTransceivers()
|
|
require.Equal(t, 1, len(transceivers))
|
|
require.Equal(t, "0", transceivers[0].Mid())
|
|
|
|
transceivers = pcAnswer.GetTransceivers()
|
|
require.Equal(t, 1, len(transceivers))
|
|
require.Equal(t, "0", transceivers[0].Mid())
|
|
|
|
ctx, cancel = context.WithCancel(context.Background())
|
|
go sendVideoUntilDone(ctx.Done(), t, []*TrackLocalStaticSample{track2})
|
|
|
|
remoteTrack2 := <-tracksCh
|
|
cancel()
|
|
|
|
require.NoError(t, pcOffer.RemoveTrack(sender2))
|
|
|
|
err = signalPair(pcOffer, pcAnswer)
|
|
require.NoError(t, err)
|
|
<-tracksClosed
|
|
|
|
assert.Equal(t, "video2", remoteTrack2.ID())
|
|
assert.Equal(t, "pion2", remoteTrack2.StreamID())
|
|
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
}
|
|
|
|
func TestPeerConnection_Renegotiation_RemoveTrack(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcOffer, pcAnswer, err := newPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
|
|
assert.NoError(t, err)
|
|
|
|
vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
|
|
assert.NoError(t, err)
|
|
|
|
sender, err := pcOffer.AddTrack(vp8Track)
|
|
assert.NoError(t, err)
|
|
|
|
onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
|
|
trackClosed, trackClosedFunc := context.WithCancel(context.Background())
|
|
|
|
pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
|
|
onTrackFiredFunc()
|
|
|
|
for {
|
|
if _, _, err := track.ReadRTP(); err == io.EOF {
|
|
trackClosedFunc()
|
|
return
|
|
}
|
|
}
|
|
})
|
|
|
|
assert.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{vp8Track})
|
|
|
|
assert.NoError(t, pcOffer.RemoveTrack(sender))
|
|
assert.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
|
|
<-trackClosed.Done()
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
}
|
|
|
|
func TestPeerConnection_RoleSwitch(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcFirstOfferer, pcSecondOfferer, err := newPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
|
|
pcFirstOfferer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
|
|
onTrackFiredFunc()
|
|
})
|
|
|
|
assert.NoError(t, signalPair(pcFirstOfferer, pcSecondOfferer))
|
|
|
|
// Add a new Track to the second offerer
|
|
// This asserts that it will match the ordering of the last RemoteDescription, but then also add new Transceivers to the end
|
|
_, err = pcFirstOfferer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
|
|
assert.NoError(t, err)
|
|
|
|
vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
|
|
assert.NoError(t, err)
|
|
|
|
_, err = pcSecondOfferer.AddTrack(vp8Track)
|
|
assert.NoError(t, err)
|
|
|
|
assert.NoError(t, signalPair(pcSecondOfferer, pcFirstOfferer))
|
|
sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{vp8Track})
|
|
|
|
closePairNow(t, pcFirstOfferer, pcSecondOfferer)
|
|
}
|
|
|
|
// Assert that renegotiation doesn't attempt to gather ICE twice
|
|
// Before we would attempt to gather multiple times and would put
|
|
// the PeerConnection into a broken state
|
|
func TestPeerConnection_Renegotiation_Trickle(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
settingEngine := SettingEngine{}
|
|
|
|
api := NewAPI(WithSettingEngine(settingEngine))
|
|
assert.NoError(t, api.mediaEngine.RegisterDefaultCodecs())
|
|
|
|
// Invalid STUN server on purpose, will stop ICE Gathering from completing in time
|
|
pcOffer, pcAnswer, err := api.newPair(Configuration{
|
|
ICEServers: []ICEServer{
|
|
{
|
|
URLs: []string{"stun:127.0.0.1:5000"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = pcOffer.CreateDataChannel("test-channel", nil)
|
|
assert.NoError(t, err)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
pcOffer.OnICECandidate(func(c *ICECandidate) {
|
|
if c != nil {
|
|
assert.NoError(t, pcAnswer.AddICECandidate(c.ToJSON()))
|
|
} else {
|
|
wg.Done()
|
|
}
|
|
})
|
|
pcAnswer.OnICECandidate(func(c *ICECandidate) {
|
|
if c != nil {
|
|
assert.NoError(t, pcOffer.AddICECandidate(c.ToJSON()))
|
|
} else {
|
|
wg.Done()
|
|
}
|
|
})
|
|
|
|
negotiate := func() {
|
|
offer, err := pcOffer.CreateOffer(nil)
|
|
assert.NoError(t, err)
|
|
|
|
assert.NoError(t, pcAnswer.SetRemoteDescription(offer))
|
|
assert.NoError(t, pcOffer.SetLocalDescription(offer))
|
|
|
|
answer, err := pcAnswer.CreateAnswer(nil)
|
|
assert.NoError(t, err)
|
|
|
|
assert.NoError(t, pcOffer.SetRemoteDescription(answer))
|
|
assert.NoError(t, pcAnswer.SetLocalDescription(answer))
|
|
}
|
|
negotiate()
|
|
negotiate()
|
|
|
|
pcOffer.ops.Done()
|
|
pcAnswer.ops.Done()
|
|
wg.Wait()
|
|
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
}
|
|
|
|
func TestPeerConnection_Renegotiation_SetLocalDescription(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcOffer, pcAnswer, err := newPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background())
|
|
pcOffer.OnTrack(func(track *TrackRemote, r *RTPReceiver) {
|
|
onTrackFiredFunc()
|
|
})
|
|
|
|
assert.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
|
|
pcOffer.ops.Done()
|
|
pcAnswer.ops.Done()
|
|
|
|
_, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
|
|
assert.NoError(t, err)
|
|
|
|
localTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
|
|
assert.NoError(t, err)
|
|
|
|
sender, err := pcAnswer.AddTrack(localTrack)
|
|
assert.NoError(t, err)
|
|
|
|
offer, err := pcOffer.CreateOffer(nil)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, pcOffer.SetLocalDescription(offer))
|
|
assert.NoError(t, pcAnswer.SetRemoteDescription(offer))
|
|
assert.False(t, sender.isNegotiated())
|
|
answer, err := pcAnswer.CreateAnswer(nil)
|
|
assert.NoError(t, err)
|
|
assert.True(t, sender.isNegotiated())
|
|
|
|
pcAnswer.ops.Done()
|
|
assert.Equal(t, 0, len(localTrack.rtpTrack.bindings))
|
|
|
|
assert.NoError(t, pcAnswer.SetLocalDescription(answer))
|
|
|
|
pcAnswer.ops.Done()
|
|
assert.Equal(t, 1, len(localTrack.rtpTrack.bindings))
|
|
|
|
assert.NoError(t, pcOffer.SetRemoteDescription(answer))
|
|
|
|
sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{localTrack})
|
|
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
}
|
|
|
|
// Issue #346, don't start the SCTP Subsystem if the RemoteDescription doesn't contain one
|
|
// Before we would always start it, and re-negotations would fail because SCTP was in flight
|
|
func TestPeerConnection_Renegotiation_NoApplication(t *testing.T) {
|
|
signalPairExcludeDataChannel := func(pcOffer, pcAnswer *PeerConnection) {
|
|
offer, err := pcOffer.CreateOffer(nil)
|
|
assert.NoError(t, err)
|
|
offerGatheringComplete := GatheringCompletePromise(pcOffer)
|
|
assert.NoError(t, pcOffer.SetLocalDescription(offer))
|
|
<-offerGatheringComplete
|
|
|
|
assert.NoError(t, pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription()))
|
|
|
|
answer, err := pcAnswer.CreateAnswer(nil)
|
|
assert.NoError(t, err)
|
|
|
|
answerGatheringComplete := GatheringCompletePromise(pcAnswer)
|
|
assert.NoError(t, pcAnswer.SetLocalDescription(answer))
|
|
<-answerGatheringComplete
|
|
|
|
assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()))
|
|
}
|
|
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcOffer, pcAnswer, err := newPair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
pcOfferConnected, pcOfferConnectedCancel := context.WithCancel(context.Background())
|
|
pcOffer.OnICEConnectionStateChange(func(i ICEConnectionState) {
|
|
if i == ICEConnectionStateConnected {
|
|
pcOfferConnectedCancel()
|
|
}
|
|
})
|
|
|
|
pcAnswerConnected, pcAnswerConnectedCancel := context.WithCancel(context.Background())
|
|
pcAnswer.OnICEConnectionStateChange(func(i ICEConnectionState) {
|
|
if i == ICEConnectionStateConnected {
|
|
pcAnswerConnectedCancel()
|
|
}
|
|
})
|
|
|
|
_, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionSendrecv})
|
|
assert.NoError(t, err)
|
|
|
|
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionSendrecv})
|
|
assert.NoError(t, err)
|
|
|
|
signalPairExcludeDataChannel(pcOffer, pcAnswer)
|
|
pcOffer.ops.Done()
|
|
pcAnswer.ops.Done()
|
|
|
|
signalPairExcludeDataChannel(pcOffer, pcAnswer)
|
|
pcOffer.ops.Done()
|
|
pcAnswer.ops.Done()
|
|
|
|
<-pcAnswerConnected.Done()
|
|
<-pcOfferConnected.Done()
|
|
|
|
assert.Equal(t, pcOffer.SCTP().State(), SCTPTransportStateConnecting)
|
|
assert.Equal(t, pcAnswer.SCTP().State(), SCTPTransportStateConnecting)
|
|
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
}
|
|
|
|
func TestAddDataChannelDuringRenegotation(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 10)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcOffer, err := NewPeerConnection(Configuration{})
|
|
assert.NoError(t, err)
|
|
|
|
pcAnswer, err := NewPeerConnection(Configuration{})
|
|
assert.NoError(t, err)
|
|
|
|
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
|
|
assert.NoError(t, err)
|
|
|
|
_, err = pcOffer.AddTrack(track)
|
|
assert.NoError(t, err)
|
|
|
|
offer, err := pcOffer.CreateOffer(nil)
|
|
assert.NoError(t, err)
|
|
|
|
offerGatheringComplete := GatheringCompletePromise(pcOffer)
|
|
assert.NoError(t, pcOffer.SetLocalDescription(offer))
|
|
<-offerGatheringComplete
|
|
|
|
assert.NoError(t, pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription()))
|
|
|
|
answer, err := pcAnswer.CreateAnswer(nil)
|
|
assert.NoError(t, err)
|
|
|
|
answerGatheringComplete := GatheringCompletePromise(pcAnswer)
|
|
assert.NoError(t, pcAnswer.SetLocalDescription(answer))
|
|
<-answerGatheringComplete
|
|
|
|
assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()))
|
|
|
|
_, err = pcOffer.CreateDataChannel("data-channel", nil)
|
|
assert.NoError(t, err)
|
|
|
|
// Assert that DataChannel is in offer now
|
|
offer, err = pcOffer.CreateOffer(nil)
|
|
assert.NoError(t, err)
|
|
|
|
applicationMediaSectionCount := 0
|
|
for _, d := range offer.parsed.MediaDescriptions {
|
|
if d.MediaName.Media == mediaSectionApplication {
|
|
applicationMediaSectionCount++
|
|
}
|
|
}
|
|
assert.Equal(t, applicationMediaSectionCount, 1)
|
|
|
|
onDataChannelFired, onDataChannelFiredFunc := context.WithCancel(context.Background())
|
|
pcAnswer.OnDataChannel(func(*DataChannel) {
|
|
onDataChannelFiredFunc()
|
|
})
|
|
|
|
assert.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
|
|
<-onDataChannelFired.Done()
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
}
|
|
|
|
// Assert that CreateDataChannel fires OnNegotiationNeeded
|
|
func TestNegotiationCreateDataChannel(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pc, err := NewPeerConnection(Configuration{})
|
|
assert.NoError(t, err)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
pc.OnNegotiationNeeded(func() {
|
|
defer func() {
|
|
wg.Done()
|
|
}()
|
|
})
|
|
|
|
// Create DataChannel, wait until OnNegotiationNeeded is fired
|
|
if _, err = pc.CreateDataChannel("testChannel", nil); err != nil {
|
|
t.Error(err.Error())
|
|
}
|
|
|
|
// Wait until OnNegotiationNeeded is fired
|
|
wg.Wait()
|
|
assert.NoError(t, pc.Close())
|
|
}
|
|
|
|
func TestNegotiationNeededRemoveTrack(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcOffer, err := NewPeerConnection(Configuration{})
|
|
assert.NoError(t, err)
|
|
pcAnswer, err := NewPeerConnection(Configuration{})
|
|
assert.NoError(t, err)
|
|
|
|
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
|
|
assert.NoError(t, err)
|
|
|
|
pcOffer.OnNegotiationNeeded(func() {
|
|
wg.Add(1)
|
|
offer, createOfferErr := pcOffer.CreateOffer(nil)
|
|
assert.NoError(t, createOfferErr)
|
|
|
|
offerGatheringComplete := GatheringCompletePromise(pcOffer)
|
|
assert.NoError(t, pcOffer.SetLocalDescription(offer))
|
|
|
|
<-offerGatheringComplete
|
|
assert.NoError(t, pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription()))
|
|
|
|
answer, createAnswerErr := pcAnswer.CreateAnswer(nil)
|
|
assert.NoError(t, createAnswerErr)
|
|
|
|
answerGatheringComplete := GatheringCompletePromise(pcAnswer)
|
|
assert.NoError(t, pcAnswer.SetLocalDescription(answer))
|
|
|
|
<-answerGatheringComplete
|
|
assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()))
|
|
wg.Done()
|
|
wg.Done()
|
|
})
|
|
|
|
sender, err := pcOffer.AddTrack(track)
|
|
assert.NoError(t, err)
|
|
|
|
assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}))
|
|
|
|
wg.Wait()
|
|
|
|
wg.Add(1)
|
|
assert.NoError(t, pcOffer.RemoveTrack(sender))
|
|
|
|
wg.Wait()
|
|
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
}
|
|
|
|
func TestNegotiationNeededStressOneSided(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcA, pcB, err := newPair()
|
|
assert.NoError(t, err)
|
|
|
|
const expectedTrackCount = 500
|
|
ctx, done := context.WithCancel(context.Background())
|
|
pcA.OnNegotiationNeeded(func() {
|
|
count := len(pcA.GetTransceivers())
|
|
assert.NoError(t, signalPair(pcA, pcB))
|
|
if count == expectedTrackCount {
|
|
done()
|
|
}
|
|
})
|
|
|
|
for i := 0; i < expectedTrackCount; i++ {
|
|
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
|
|
assert.NoError(t, err)
|
|
|
|
_, err = pcA.AddTrack(track)
|
|
assert.NoError(t, err)
|
|
}
|
|
<-ctx.Done()
|
|
assert.Equal(t, expectedTrackCount, len(pcB.GetTransceivers()))
|
|
closePairNow(t, pcA, pcB)
|
|
}
|
|
|
|
// TestPeerConnection_Renegotiation_DisableTrack asserts that if a remote track is set inactive
|
|
// that locally it goes inactive as well
|
|
func TestPeerConnection_Renegotiation_DisableTrack(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
pcOffer, pcAnswer, err := newPair()
|
|
assert.NoError(t, err)
|
|
|
|
// Create two transceivers
|
|
_, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo)
|
|
assert.NoError(t, err)
|
|
|
|
transceiver, err := pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo)
|
|
assert.NoError(t, err)
|
|
|
|
assert.NoError(t, signalPair(pcOffer, pcAnswer))
|
|
|
|
// Assert we have three active transceivers
|
|
offer, err := pcOffer.CreateOffer(nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, strings.Count(offer.SDP, "a=sendrecv"), 3)
|
|
|
|
// Assert we have two active transceivers, one inactive
|
|
assert.NoError(t, transceiver.Stop())
|
|
offer, err = pcOffer.CreateOffer(nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, strings.Count(offer.SDP, "a=sendrecv"), 2)
|
|
assert.Equal(t, strings.Count(offer.SDP, "a=inactive"), 1)
|
|
|
|
// Assert that the offer disabled one of our transceivers
|
|
assert.NoError(t, pcAnswer.SetRemoteDescription(offer))
|
|
answer, err := pcAnswer.CreateAnswer(nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, strings.Count(answer.SDP, "a=sendrecv"), 1) // DataChannel
|
|
assert.Equal(t, strings.Count(answer.SDP, "a=recvonly"), 1)
|
|
assert.Equal(t, strings.Count(answer.SDP, "a=inactive"), 1)
|
|
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
}
|
|
|
|
func TestPeerConnection_Renegotiation_Simulcast(t *testing.T) {
|
|
lim := test.TimeOut(time.Second * 30)
|
|
defer lim.Stop()
|
|
|
|
report := test.CheckRoutines(t)
|
|
defer report()
|
|
|
|
m := &MediaEngine{}
|
|
if err := m.RegisterDefaultCodecs(); err != nil {
|
|
panic(err)
|
|
}
|
|
registerSimulcastHeaderExtensions(m, RTPCodecTypeVideo)
|
|
|
|
originalRids := []string{"a", "b", "c"}
|
|
signalWithRids := func(sessionDescription string, rids []string) string {
|
|
sessionDescription = strings.SplitAfter(sessionDescription, "a=end-of-candidates\r\n")[0]
|
|
sessionDescription = filterSsrc(sessionDescription)
|
|
for _, rid := range rids {
|
|
sessionDescription += "a=" + sdpAttributeRid + ":" + rid + " send\r\n"
|
|
}
|
|
return sessionDescription + "a=simulcast:send " + strings.Join(rids, ";") + "\r\n"
|
|
}
|
|
|
|
var trackMapLock sync.RWMutex
|
|
trackMap := map[string]*TrackRemote{}
|
|
|
|
onTrackHandler := func(track *TrackRemote, _ *RTPReceiver) {
|
|
trackMapLock.Lock()
|
|
defer trackMapLock.Unlock()
|
|
trackMap[track.RID()] = track
|
|
}
|
|
|
|
sendUntilAllTracksFired := func(vp8Writer *TrackLocalStaticRTP, rids []string) {
|
|
allTracksFired := func() bool {
|
|
trackMapLock.Lock()
|
|
defer trackMapLock.Unlock()
|
|
|
|
return len(trackMap) == len(rids)
|
|
}
|
|
|
|
for sequenceNumber := uint16(0); !allTracksFired(); sequenceNumber++ {
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
for ssrc, rid := range rids {
|
|
header := &rtp.Header{
|
|
Version: 2,
|
|
SSRC: uint32(ssrc),
|
|
SequenceNumber: sequenceNumber,
|
|
PayloadType: 96,
|
|
}
|
|
assert.NoError(t, header.SetExtension(1, []byte("0")))
|
|
assert.NoError(t, header.SetExtension(2, []byte(rid)))
|
|
|
|
_, err := vp8Writer.bindings[0].writeStream.WriteRTP(header, []byte{0x00})
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
assertTracksClosed := func(t *testing.T) {
|
|
trackMapLock.Lock()
|
|
defer trackMapLock.Unlock()
|
|
|
|
for _, track := range trackMap {
|
|
_, _, err := track.ReadRTP() // Ignore first Read, this is our peeked data
|
|
assert.Nil(t, err)
|
|
|
|
_, _, err = track.ReadRTP()
|
|
assert.Equal(t, err, io.EOF)
|
|
}
|
|
}
|
|
|
|
t.Run("Disable Transceiver", func(t *testing.T) {
|
|
trackMap = map[string]*TrackRemote{}
|
|
pcOffer, pcAnswer, err := NewAPI(WithMediaEngine(m)).newPair(Configuration{})
|
|
assert.NoError(t, err)
|
|
|
|
vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2")
|
|
assert.NoError(t, err)
|
|
|
|
rtpTransceiver, err := pcOffer.AddTransceiverFromTrack(
|
|
vp8Writer,
|
|
RTPTransceiverInit{
|
|
Direction: RTPTransceiverDirectionSendonly,
|
|
},
|
|
)
|
|
assert.NoError(t, err)
|
|
|
|
assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string {
|
|
return signalWithRids(sessionDescription, originalRids)
|
|
}))
|
|
|
|
pcAnswer.OnTrack(onTrackHandler)
|
|
sendUntilAllTracksFired(vp8Writer, originalRids)
|
|
|
|
assert.NoError(t, pcOffer.RemoveTrack(rtpTransceiver.Sender()))
|
|
assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string {
|
|
sessionDescription = strings.SplitAfter(sessionDescription, "a=end-of-candidates\r\n")[0]
|
|
return sessionDescription
|
|
}))
|
|
|
|
assertTracksClosed(t)
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
})
|
|
|
|
t.Run("Change RID", func(t *testing.T) {
|
|
trackMap = map[string]*TrackRemote{}
|
|
pcOffer, pcAnswer, err := NewAPI(WithMediaEngine(m)).newPair(Configuration{})
|
|
assert.NoError(t, err)
|
|
|
|
vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2")
|
|
assert.NoError(t, err)
|
|
|
|
_, err = pcOffer.AddTransceiverFromTrack(
|
|
vp8Writer,
|
|
RTPTransceiverInit{
|
|
Direction: RTPTransceiverDirectionSendonly,
|
|
},
|
|
)
|
|
assert.NoError(t, err)
|
|
|
|
assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string {
|
|
return signalWithRids(sessionDescription, originalRids)
|
|
}))
|
|
|
|
pcAnswer.OnTrack(onTrackHandler)
|
|
sendUntilAllTracksFired(vp8Writer, originalRids)
|
|
|
|
newRids := []string{"d", "e", "f"}
|
|
assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string {
|
|
scanner := bufio.NewScanner(strings.NewReader(sessionDescription))
|
|
sessionDescription = ""
|
|
for scanner.Scan() {
|
|
l := scanner.Text()
|
|
if strings.HasPrefix(l, "a=rid") || strings.HasPrefix(l, "a=simulcast") {
|
|
continue
|
|
}
|
|
|
|
sessionDescription += l + "\n"
|
|
}
|
|
return signalWithRids(sessionDescription, newRids)
|
|
}))
|
|
|
|
assertTracksClosed(t)
|
|
closePairNow(t, pcOffer, pcAnswer)
|
|
})
|
|
}
|