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 {