diff --git a/examples/delete-transceiver/README.md b/examples/delete-transceiver/README.md
new file mode 100644
index 00000000000..3dfa7fdc616
--- /dev/null
+++ b/examples/delete-transceiver/README.md
@@ -0,0 +1,18 @@
+# delete-transceiver
+delete-transceiver demonstrates Pion WebRTC's ability to delete rejected transceivers.
+
+## Instructions
+
+### Download delete-transceiver
+This example requires you to clone the repo since it is serving static HTML.
+
+```
+git clone https://github.com/pion/webrtc.git
+cd webrtc/examples/delete-transceiver
+```
+
+### Run delete-transceiver
+Execute `go run *.go`
+
+### Open the Web UI
+Open [http://localhost:8080](http://localhost:8080). This remote peerconnection will use single port 8443.
diff --git a/examples/delete-transceiver/index.html b/examples/delete-transceiver/index.html
new file mode 100644
index 00000000000..cbc8b93a93c
--- /dev/null
+++ b/examples/delete-transceiver/index.html
@@ -0,0 +1,117 @@
+
+
+
+
+ ice-single-port
+
+
+
+ ICE Selected Pairs
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/delete-transceiver/main.go b/examples/delete-transceiver/main.go
new file mode 100644
index 00000000000..695ae14de87
--- /dev/null
+++ b/examples/delete-transceiver/main.go
@@ -0,0 +1,133 @@
+// SPDX-FileCopyrightText: 2023 The Pion community
+// SPDX-License-Identifier: MIT
+
+//go:build !js
+// +build !js
+
+// delete-transceiver demonstrates Pion WebRTC's ability to delete rejected transceivers.
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/pion/ice/v4"
+ "github.com/pion/webrtc/v4"
+)
+
+var api *webrtc.API //nolint
+var peerConnection *webrtc.PeerConnection
+
+// Everything below is the Pion WebRTC API! Thanks for using it ❤️.
+func doOffer(w http.ResponseWriter, r *http.Request) {
+ var err error
+ peerConnection, err = api.NewPeerConnection(webrtc.Configuration{})
+ if err != nil {
+ panic(err)
+ }
+
+ // Set the handler for ICE connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
+ fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
+ })
+
+ // Send the current time via a DataChannel to the remote peer every 3 seconds
+ peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
+ d.OnOpen(func() {
+ for range time.Tick(time.Second * 3) {
+ if err = d.SendText(time.Now().String()); err != nil {
+ panic(err)
+ }
+ }
+ })
+ })
+
+ var offer webrtc.SessionDescription
+ if err = json.NewDecoder(r.Body).Decode(&offer); err != nil {
+ panic(err)
+ }
+
+ if err = peerConnection.SetRemoteDescription(offer); err != nil {
+ panic(err)
+ }
+
+ // Create channel that is blocked until ICE Gathering is complete
+ gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
+
+ answer, err := peerConnection.CreateAnswer(nil)
+ if err != nil {
+ panic(err)
+ } else if err = peerConnection.SetLocalDescription(answer); err != nil {
+ panic(err)
+ }
+
+ // Block until ICE Gathering is complete, disabling trickle ICE
+ // we do this because we only can exchange one signaling message
+ // in a production application you should exchange ICE Candidates via OnICECandidate
+ <-gatherComplete
+
+ response, err := json.Marshal(*peerConnection.LocalDescription())
+ if err != nil {
+ panic(err)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ if _, err := w.Write(response); err != nil {
+ panic(err)
+ }
+}
+
+func doUpdate(w http.ResponseWriter, r *http.Request) {
+ var err error
+ var offer webrtc.SessionDescription
+ if err = json.NewDecoder(r.Body).Decode(&offer); err != nil {
+ panic(err)
+ }
+
+ if err = peerConnection.SetRemoteDescription(offer); err != nil {
+ panic(err)
+ }
+
+ answer, err := peerConnection.CreateAnswer(nil)
+ if err != nil {
+ panic(err)
+ } else if err = peerConnection.SetLocalDescription(answer); err != nil {
+ panic(err)
+ }
+ response, err := json.Marshal(answer)
+ if err != nil {
+ panic(err)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ if _, err := w.Write(response); err != nil {
+ panic(err)
+ }
+}
+
+func main() {
+ // Create a SettingEngine, this allows non-standard WebRTC behavior
+ settingEngine := webrtc.SettingEngine{}
+
+ // Listen on UDP Port 8443, will be used for all WebRTC traffic
+ mux, err := ice.NewMultiUDPMuxFromPort(8443)
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("Listening for WebRTC traffic at %d\n", 8443)
+ settingEngine.SetICEUDPMux(mux)
+
+ // Create a new API using our SettingEngine
+ api = webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine))
+
+ http.Handle("/", http.FileServer(http.Dir(".")))
+ http.HandleFunc("/doOffer", doOffer)
+ http.HandleFunc("/doUpdate", doUpdate)
+
+ fmt.Println("Open http://localhost:8080 to access this demo")
+ // nolint: gosec
+ panic(http.ListenAndServe(":8080", nil))
+}
diff --git a/peerconnection.go b/peerconnection.go
index 5e613c224cc..9093918b01d 100644
--- a/peerconnection.go
+++ b/peerconnection.go
@@ -847,7 +847,6 @@ func (pc *PeerConnection) CreateAnswer(*AnswerOptions) (SessionDescription, erro
if err != nil {
return SessionDescription{}, err
}
-
desc := SessionDescription{
Type: SDPTypeAnswer,
SDP: string(sdpBytes),
@@ -1007,7 +1006,8 @@ func (pc *PeerConnection) SetLocalDescription(desc SessionDescription) error {
weAnswer := desc.Type == SDPTypeAnswer
remoteDesc := pc.RemoteDescription()
if weAnswer && remoteDesc != nil {
- _ = setRTPTransceiverCurrentDirection(&desc, currentTransceivers, false)
+ rejected := []string{}
+ _ = setRTPTransceiverCurrentDirection(&desc, currentTransceivers, false, &rejected)
if err := pc.startRTPSenders(currentTransceivers); err != nil {
return err
}
@@ -1015,6 +1015,9 @@ func (pc *PeerConnection) SetLocalDescription(desc SessionDescription) error {
pc.ops.Enqueue(func() {
pc.startRTP(haveLocalDescription, remoteDesc, currentTransceivers)
})
+ pc.mu.Lock()
+ pc.removeRTPTransceiver(rejected)
+ pc.mu.Unlock()
}
mediaSection, ok := selectCandidateMediaSection(desc.parsed)
@@ -1184,7 +1187,8 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
if isRenegotiation {
if weOffer {
- _ = setRTPTransceiverCurrentDirection(&desc, currentTransceivers, true)
+ rejected := []string{}
+ _ = setRTPTransceiverCurrentDirection(&desc, currentTransceivers, true, &rejected)
if err = pc.startRTPSenders(currentTransceivers); err != nil {
return err
}
@@ -1192,6 +1196,9 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
pc.ops.Enqueue(func() {
pc.startRTP(true, &desc, currentTransceivers)
})
+ pc.mu.Lock()
+ pc.removeRTPTransceiver(rejected)
+ pc.mu.Unlock()
}
return nil
}
@@ -1214,7 +1221,7 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
// Start the networking in a new routine since it will block until
// the connection is actually established.
if weOffer {
- _ = setRTPTransceiverCurrentDirection(&desc, currentTransceivers, true)
+ _ = setRTPTransceiverCurrentDirection(&desc, currentTransceivers, true, nil)
if err := pc.startRTPSenders(currentTransceivers); err != nil {
return err
}
@@ -1277,7 +1284,8 @@ func (pc *PeerConnection) startReceiver(incoming trackDetails, receiver *RTPRece
}
}
-func setRTPTransceiverCurrentDirection(answer *SessionDescription, currentTransceivers []*RTPTransceiver, weOffer bool) error {
+// rejected is only meaningful when SessionDescription is answer
+func setRTPTransceiverCurrentDirection(answer *SessionDescription, currentTransceivers []*RTPTransceiver, weOffer bool, rejected *[]string) error {
currentTransceivers = append([]*RTPTransceiver{}, currentTransceivers...)
for _, media := range answer.parsed.MediaDescriptions {
midValue := getMidValue(media)
@@ -1318,7 +1326,10 @@ func setRTPTransceiverCurrentDirection(answer *SessionDescription, currentTransc
if !weOffer && direction == RTPTransceiverDirectionSendonly && t.Sender() == nil {
direction = RTPTransceiverDirectionInactive
}
-
+ // reject transceiver if it is inactive
+ if rejected != nil && media.MediaName.Port.Value == 0 && direction == RTPTransceiverDirectionInactive {
+ *rejected = append(*rejected, midValue)
+ }
t.setCurrentDirection(direction)
}
return nil
@@ -2264,6 +2275,38 @@ func (pc *PeerConnection) addRTPTransceiver(t *RTPTransceiver) {
pc.onNegotiationNeeded()
}
+// removeRTPTransceiver remove inactive
+// and fires onNegotiationNeeded;
+// caller of this method should hold `pc.mu` lock
+func (pc *PeerConnection) removeRTPTransceiver(mids []string) {
+ if len(mids) == 0 {
+ return
+ }
+
+ midSet := make(map[string]struct{}, len(mids))
+ for _, mid := range mids {
+ if mid == "" {
+ continue
+ }
+ midSet[mid] = struct{}{}
+ }
+
+ n := 0
+ for _, transceiver := range pc.rtpTransceivers {
+ if _, exists := midSet[transceiver.Mid()]; exists {
+ err := transceiver.Stop()
+ if err != nil {
+ pc.log.Errorf("Failed to stop transceiver: %s", err)
+ }
+ } else {
+ pc.rtpTransceivers[n] = transceiver
+ n++
+ }
+ }
+ // Resize the slice to remove unwanted transceivers
+ pc.rtpTransceivers = pc.rtpTransceivers[:n]
+}
+
// CurrentLocalDescription represents the local description that was
// successfully negotiated the last time the PeerConnection transitioned
// into the stable state plus any local candidates that have been generated
@@ -2628,7 +2671,16 @@ func (pc *PeerConnection) generateMatchedSDP(transceivers []*RTPTransceiver, use
mediaTransceivers := []*RTPTransceiver{t}
extensions, _ := rtpExtensionsFromMediaDescription(media)
- mediaSections = append(mediaSections, mediaSection{id: midValue, transceivers: mediaTransceivers, matchExtensions: extensions, rids: getRids(media)})
+ rejected := false
+ if media.MediaName.Port.Value == 0 {
+ for _, attr := range media.Attributes {
+ if attr.Key == sdp.AttrKeyInactive {
+ rejected = true
+ break
+ }
+ }
+ }
+ mediaSections = append(mediaSections, mediaSection{id: midValue, transceivers: mediaTransceivers, matchExtensions: extensions, rids: getRids(media), rejected: rejected})
}
}
diff --git a/peerconnection_test.go b/peerconnection_test.go
index a87ba1fbb81..ff099d0988a 100644
--- a/peerconnection_test.go
+++ b/peerconnection_test.go
@@ -738,6 +738,50 @@ func TestAddTransceiver(t *testing.T) {
}
}
+func TestRemoveMiddleTransceiverAndRenegotiate(t *testing.T) {
+ // Initialize the offerPC PeerConnection
+ offerPC, err := NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+ defer offerPC.Close()
+
+ // Initialize the answerPC PeerConnection
+ answerPC, err := NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+ defer answerPC.Close()
+
+ // Add multiple transceivers to the offerer
+ videoTransceiver1, err := offerPC.AddTransceiverFromKind(RTPCodecTypeVideo)
+ assert.NoError(t, err)
+ audioTransceiver, err := offerPC.AddTransceiverFromKind(RTPCodecTypeAudio)
+ assert.NoError(t, err)
+ videoTransceiver2, err := offerPC.AddTransceiverFromKind(RTPCodecTypeVideo)
+ assert.NoError(t, err)
+
+ // Perform initial SDP negotiation
+ offer, err := offerPC.CreateOffer(nil)
+ assert.NoError(t, err)
+ assert.NoError(t, offerPC.SetLocalDescription(offer))
+ assert.NoError(t, answerPC.SetRemoteDescription(*offerPC.LocalDescription()))
+ answer, err := answerPC.CreateAnswer(nil)
+ assert.NoError(t, err)
+ assert.NoError(t, answerPC.SetLocalDescription(answer))
+ assert.NoError(t, offerPC.SetRemoteDescription(*answerPC.LocalDescription()))
+
+ // Ensure MIDs are assigned
+ assert.NotEmpty(t, videoTransceiver1.Mid())
+ assert.NotEmpty(t, audioTransceiver.Mid())
+ assert.NotEmpty(t, videoTransceiver2.Mid())
+
+ // Remove the middle transceiver (audio) using its MID
+ offerPC.removeRTPTransceiver([]string{audioTransceiver.Mid()})
+
+ // Verify the transceiver was removed
+ remainingTransceivers := offerPC.GetTransceivers()
+ assert.Len(t, remainingTransceivers, 2)
+ assert.Equal(t, videoTransceiver1.Mid(), remainingTransceivers[0].Mid())
+ assert.Equal(t, videoTransceiver2.Mid(), remainingTransceivers[1].Mid())
+}
+
// Assert that SCTPTransport -> DTLSTransport -> ICETransport works after connected
func TestTransportChain(t *testing.T) {
offer, answer, err := newPair()
diff --git a/sdp.go b/sdp.go
index b07a55326c9..dc8c95723dc 100644
--- a/sdp.go
+++ b/sdp.go
@@ -467,10 +467,12 @@ func addTransceiverSDP(
media.WithValueAttribute("rtcp-fb", fmt.Sprintf("%d %s %s", codec.PayloadType, feedback.Type, feedback.Parameter))
}
}
- if len(codecs) == 0 {
+ if len(codecs) == 0 || mediaSection.rejected {
// If we are sender and we have no codecs throw an error early
if t.Sender() != nil {
- return false, ErrSenderWithNoCodecs
+ if !mediaSection.rejected {
+ return false, ErrSenderWithNoCodecs
+ }
}
// Explicitly reject track if we don't have the codec
@@ -492,8 +494,13 @@ func addTransceiverSDP(
Address: "0.0.0.0",
},
},
+ Attributes: []sdp.Attribute{
+ {Key: RTPTransceiverDirectionInactive.String(), Value: ""},
+ {Key: "mid", Value: midValue},
+ },
})
return false, nil
+
}
directions := []RTPTransceiverDirection{}
@@ -564,6 +571,7 @@ type mediaSection struct {
data bool
matchExtensions map[string]int
rids []*simulcastRid
+ rejected bool
}
func bundleMatchFromRemote(matchBundleGroup *string) func(mid string) bool {