diff --git a/datachannel_ortc_test.go b/datachannel_ortc_test.go index 35ac2c47c59..dac24908ae3 100644 --- a/datachannel_ortc_test.go +++ b/datachannel_ortc_test.go @@ -118,12 +118,18 @@ func (s *testORTCStack) setSignal(sig *testORTCSignal, isOffer bool) error { } func (s *testORTCStack) getSignal() (*testORTCSignal, error) { - // Gather candidates - err := s.gatherer.Gather() - if err != nil { + gatherFinished := make(chan struct{}) + s.gatherer.OnLocalCandidate(func(i *ICECandidate) { + if i == nil { + close(gatherFinished) + } + }) + + if err := s.gatherer.Gather(); err != nil { return nil, err } + <-gatherFinished iceCandidates, err := s.gatherer.GetLocalCandidates() if err != nil { return nil, err diff --git a/examples/broadcast/main.go b/examples/broadcast/main.go index 5d3593df709..b8e973e04a0 100644 --- a/examples/broadcast/main.go +++ b/examples/broadcast/main.go @@ -100,14 +100,22 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if 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 + // Get the LocalDescription and take it to base64 so we can paste in browser - fmt.Println(signal.Encode(answer)) + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) localTrack := <-localTrackChan for { @@ -140,13 +148,21 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete = webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if 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 + // Get the LocalDescription and take it to base64 so we can paste in browser - fmt.Println(signal.Encode(answer)) + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) } } diff --git a/examples/custom-logger/main.go b/examples/custom-logger/main.go index e3cb265ff96..4f344dab34d 100644 --- a/examples/custom-logger/main.go +++ b/examples/custom-logger/main.go @@ -67,6 +67,26 @@ func main() { panic(err) } + // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate + // send it to the other peer + answerPeerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { + if i != nil { + if iceErr := offerPeerConnection.AddICECandidate(i.ToJSON()); iceErr != nil { + panic(iceErr) + } + } + }) + + // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate + // send it to the other peer + offerPeerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { + if i != nil { + if iceErr := answerPeerConnection.AddICECandidate(i.ToJSON()); iceErr != nil { + panic(iceErr) + } + } + }) + // Create an offer for the other PeerConnection offer, err := offerPeerConnection.CreateOffer(nil) if err != nil { @@ -90,6 +110,11 @@ func main() { panic(err) } + // Set the answerer's LocalDescription + if err = answerPeerConnection.SetLocalDescription(answer); err != nil { + panic(err) + } + // SetRemoteDescription on original PeerConnection, this finishes our signaling // bother PeerConnections should be able to communicate with each other now if err = offerPeerConnection.SetRemoteDescription(answer); err != nil { diff --git a/examples/data-channels-close/main.go b/examples/data-channels-close/main.go index 561a44e056a..94e928250dd 100644 --- a/examples/data-channels-close/main.go +++ b/examples/data-channels-close/main.go @@ -97,14 +97,22 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if 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 + // Output the answer in base64 so we can paste it in browser - fmt.Println(signal.Encode(answer)) + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} diff --git a/examples/data-channels-create/README.md b/examples/data-channels-create/README.md index ca126a0651d..c40e35ba831 100644 --- a/examples/data-channels-create/README.md +++ b/examples/data-channels-create/README.md @@ -23,8 +23,8 @@ Hit the 'Start Session' button in the browser. You should see `have-remote-offer Meanwhile text has appeared in the second text area of the jsfiddle. Copy the text and paste it into `data-channels-create` and hit ENTER. In the browser you'll now see `connected` as the connection is created. If everything worked you should see `New DataChannel data`. -Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your browser! +Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your terminal! -You can also type in your terminal, and when you hit enter it will appear in your web browser. +Pion WebRTC will send random messages every 5 seconds that will appear in your browser. Congrats, you have used Pion WebRTC! Now start building something cool diff --git a/examples/data-channels-create/main.go b/examples/data-channels-create/main.go index 0e3a86443c1..a2f875a404f 100644 --- a/examples/data-channels-create/main.go +++ b/examples/data-channels-create/main.go @@ -66,14 +66,22 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(offer) if err != nil { panic(err) } - // Output the offer in base64 so we can paste it in browser - fmt.Println(signal.Encode(offer)) + // 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 + + // Output the answer in base64 so we can paste it in browser + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Wait for the answer to be pasted answer := webrtc.SessionDescription{} diff --git a/examples/data-channels-detach-create/main.go b/examples/data-channels-detach-create/main.go index 68728f7ab39..02f13f4cc27 100644 --- a/examples/data-channels-detach-create/main.go +++ b/examples/data-channels-detach-create/main.go @@ -76,14 +76,22 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(offer) if 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 + // Output the offer in base64 so we can paste it in browser - fmt.Println(signal.Encode(offer)) + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Wait for the answer to be pasted answer := webrtc.SessionDescription{} diff --git a/examples/data-channels-detach/main.go b/examples/data-channels-detach/main.go index 4a97826a4a5..83bc0a72675 100644 --- a/examples/data-channels-detach/main.go +++ b/examples/data-channels-detach/main.go @@ -85,14 +85,22 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if 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 + // Output the answer in base64 so we can paste it in browser - fmt.Println(signal.Encode(answer)) + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} diff --git a/examples/data-channels-flow-control/main.go b/examples/data-channels-flow-control/main.go index e576729c320..9e99f15ec96 100644 --- a/examples/data-channels-flow-control/main.go +++ b/examples/data-channels-flow-control/main.go @@ -122,6 +122,22 @@ func main() { offerPC := createOfferer() answerPC := createAnswerer() + // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate + // send it to the other peer + answerPC.OnICECandidate(func(i *webrtc.ICECandidate) { + if i != nil { + check(offerPC.AddICECandidate(i.ToJSON())) + } + }) + + // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate + // send it to the other peer + offerPC.OnICECandidate(func(i *webrtc.ICECandidate) { + if i != nil { + check(answerPC.AddICECandidate(i.ToJSON())) + } + }) + // Now, create an offer offer, err := offerPC.CreateOffer(nil) check(err) diff --git a/examples/data-channels/README.md b/examples/data-channels/README.md index eea50d69546..0b8cdaec643 100644 --- a/examples/data-channels/README.md +++ b/examples/data-channels/README.md @@ -24,8 +24,8 @@ Copy the text that `data-channels` just emitted and copy into second text area ### Hit 'Start Session' in jsfiddle Under Start Session you should see 'Checking' as it starts connecting. If everything worked you should see `New DataChannel foo 1` -Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your browser! +Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your terminal! -You can also type in your terminal, and when you hit enter it will appear in your web browser. +Pion WebRTC will send random messages every 5 seconds that will appear in your browser. Congrats, you have used Pion WebRTC! Now start building something cool diff --git a/examples/data-channels/main.go b/examples/data-channels/main.go index ee53521579b..89f74002cee 100644 --- a/examples/data-channels/main.go +++ b/examples/data-channels/main.go @@ -74,14 +74,22 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if 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 + // Output the answer in base64 so we can paste it in browser - fmt.Println(signal.Encode(answer)) + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} diff --git a/examples/insertable-streams/main.go b/examples/insertable-streams/main.go index 3ed1910e7b9..e69efcb4b8f 100644 --- a/examples/insertable-streams/main.go +++ b/examples/insertable-streams/main.go @@ -126,13 +126,21 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners 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 + // Output the answer in base64 so we can paste it in browser - fmt.Println(signal.Encode(answer)) + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} diff --git a/examples/ortc-quic/main.go b/examples/ortc-quic/main.go index 0fa020615a1..0fce0a46610 100644 --- a/examples/ortc-quic/main.go +++ b/examples/ortc-quic/main.go @@ -59,12 +59,21 @@ func main() { go WriteLoop(stream) }) + gatherFinished := make(chan struct{}) + gatherer.OnLocalCandidate(func(i *webrtc.ICECandidate) { + if i == nil { + close(gatherFinished) + } + }) + // Gather candidates err = gatherer.Gather() if err != nil { panic(err) } + <-gatherFinished + iceCandidates, err := gatherer.GetLocalCandidates() if err != nil { panic(err) diff --git a/examples/ortc/main.go b/examples/ortc/main.go index eb89643e1b5..03df902ea9b 100644 --- a/examples/ortc/main.go +++ b/examples/ortc/main.go @@ -55,12 +55,21 @@ func main() { }) }) + gatherFinished := make(chan struct{}) + gatherer.OnLocalCandidate(func(i *webrtc.ICECandidate) { + if i == nil { + close(gatherFinished) + } + }) + // Gather candidates err = gatherer.Gather() if err != nil { panic(err) } + <-gatherFinished + iceCandidates, err := gatherer.GetLocalCandidates() if err != nil { panic(err) diff --git a/examples/pion-to-pion-trickle/README.md b/examples/pion-to-pion-trickle/README.md deleted file mode 100644 index eb4dd92b840..00000000000 --- a/examples/pion-to-pion-trickle/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# pion-to-pion-trickle -pion-to-pion-trickle is an example of two pion instances communicating directly! -This example uses Trickle ICE, this allows communication to begin before gathering -has completed. - -See `pion-to-pion` example of a non-Trickle version of this. - -The SDP offer and answer are exchanged automatically over HTTP. -The `answer` side acts like a HTTP server and should therefore be ran first. - -## Instructions -First run `answer`: -```sh -go install github.com/pion/webrtc/examples/pion-to-pion/answer -answer -``` -Next, run `offer`: -```sh -go install github.com/pion/webrtc/examples/pion-to-pion/offer -offer -``` - -You should see them connect and start to exchange messages. diff --git a/examples/pion-to-pion-trickle/answer/main.go b/examples/pion-to-pion-trickle/answer/main.go deleted file mode 100644 index 239c0126972..00000000000 --- a/examples/pion-to-pion-trickle/answer/main.go +++ /dev/null @@ -1,174 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "flag" - "fmt" - "io/ioutil" - "net/http" - "sync" - "time" - - "github.com/pion/webrtc/v3" - - "github.com/pion/webrtc/v3/examples/internal/signal" -) - -func signalCandidate(addr string, c *webrtc.ICECandidate) error { - payload := []byte(c.ToJSON().Candidate) - resp, err := http.Post(fmt.Sprintf("http://%s/candidate", addr), - "application/json; charset=utf-8", bytes.NewReader(payload)) - - if err != nil { - return err - } - - if closeErr := resp.Body.Close(); closeErr != nil { - return closeErr - } - - return nil -} - -func main() { - offerAddr := flag.String("offer-address", "localhost:50000", "Address that the Offer HTTP server is hosted on.") - answerAddr := flag.String("answer-address", ":60000", "Address that the Answer HTTP server is hosted on.") - flag.Parse() - - var candidatesMux sync.Mutex - pendingCandidates := make([]*webrtc.ICECandidate, 0) - // Everything below is the Pion WebRTC API! Thanks for using it ❤️. - - // Prepare the configuration - config := webrtc.Configuration{ - ICEServers: []webrtc.ICEServer{ - { - URLs: []string{"stun:stun.l.google.com:19302"}, - }, - }, - } - - // Create a new API with Trickle ICE enabled - // This SettingEngine allows non-standard WebRTC behavior - s := webrtc.SettingEngine{} - s.SetTrickle(true) - api := webrtc.NewAPI(webrtc.WithSettingEngine(s)) - - // Create a new RTCPeerConnection - peerConnection, err := api.NewPeerConnection(config) - if err != nil { - panic(err) - } - - // When an ICE candidate is available send to the other Pion instance - // the other Pion instance will add this candidate by calling AddICECandidate - peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) { - if c == nil { - return - } - - candidatesMux.Lock() - defer candidatesMux.Unlock() - - desc := peerConnection.RemoteDescription() - if desc == nil { - pendingCandidates = append(pendingCandidates, c) - } else if onICECandidateErr := signalCandidate(*offerAddr, c); onICECandidateErr != nil { - panic(onICECandidateErr) - } - }) - - // A HTTP handler that allows the other Pion instance to send us ICE candidates - // This allows us to add ICE candidates faster, we don't have to wait for STUN or TURN - // candidates which may be slower - http.HandleFunc("/candidate", func(w http.ResponseWriter, r *http.Request) { - candidate, candidateErr := ioutil.ReadAll(r.Body) - if candidateErr != nil { - panic(candidateErr) - } - if candidateErr := peerConnection.AddICECandidate(webrtc.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil { - panic(candidateErr) - } - }) - - // A HTTP handler that processes a SessionDescription given to us from the other Pion process - http.HandleFunc("/sdp", func(w http.ResponseWriter, r *http.Request) { - sdp := webrtc.SessionDescription{} - if err := json.NewDecoder(r.Body).Decode(&sdp); err != nil { - panic(err) - } - - if err := peerConnection.SetRemoteDescription(sdp); err != nil { - panic(err) - } - - // Create an answer to send to the other process - answer, err := peerConnection.CreateAnswer(nil) - if err != nil { - panic(err) - } - - // Send our answer to the HTTP server listening in the other process - payload, err := json.Marshal(answer) - if err != nil { - panic(err) - } - resp, err := http.Post(fmt.Sprintf("http://%s/sdp", *offerAddr), "application/json; charset=utf-8", bytes.NewReader(payload)) - if err != nil { - panic(err) - } else if closeErr := resp.Body.Close(); closeErr != nil { - panic(closeErr) - } - - // Sets the LocalDescription, and starts our UDP listeners - err = peerConnection.SetLocalDescription(answer) - if err != nil { - panic(err) - } - - candidatesMux.Lock() - for _, c := range pendingCandidates { - onICECandidateErr := signalCandidate(*offerAddr, c) - if onICECandidateErr != nil { - panic(onICECandidateErr) - } - } - candidatesMux.Unlock() - }) - - // 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()) - }) - - // Register data channel creation handling - peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { - fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID()) - - // Register channel opening handling - d.OnOpen(func() { - fmt.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds\n", d.Label(), d.ID()) - - for range time.NewTicker(5 * time.Second).C { - message := signal.RandSeq(15) - fmt.Printf("Sending '%s'\n", message) - - // Send the message as text - sendTextErr := d.SendText(message) - if sendTextErr != nil { - panic(sendTextErr) - } - } - }) - - // Register text message handling - d.OnMessage(func(msg webrtc.DataChannelMessage) { - fmt.Printf("Message from DataChannel '%s': '%s'\n", d.Label(), string(msg.Data)) - }) - }) - - // Start HTTP server that accepts requests from the offer process to exchange SDP and Candidates - panic(http.ListenAndServe(*answerAddr, nil)) -} diff --git a/examples/pion-to-pion-trickle/offer/main.go b/examples/pion-to-pion-trickle/offer/main.go deleted file mode 100644 index 4f2e7593971..00000000000 --- a/examples/pion-to-pion-trickle/offer/main.go +++ /dev/null @@ -1,178 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "flag" - "fmt" - "io/ioutil" - "net/http" - "sync" - "time" - - "github.com/pion/webrtc/v3" - - "github.com/pion/webrtc/v3/examples/internal/signal" -) - -func signalCandidate(addr string, c *webrtc.ICECandidate) error { - payload := []byte(c.ToJSON().Candidate) - resp, err := http.Post(fmt.Sprintf("http://%s/candidate", addr), - "application/json; charset=utf-8", bytes.NewReader(payload)) - - if err != nil { - return err - } - - if closeErr := resp.Body.Close(); closeErr != nil { - return closeErr - } - - return nil -} - -func main() { - offerAddr := flag.String("offer-address", ":50000", "Address that the Offer HTTP server is hosted on.") - answerAddr := flag.String("answer-address", "127.0.0.1:60000", "Address that the Answer HTTP server is hosted on.") - flag.Parse() - - var candidatesMux sync.Mutex - pendingCandidates := make([]*webrtc.ICECandidate, 0) - - // Everything below is the Pion WebRTC API! Thanks for using it ❤️. - - // Prepare the configuration - config := webrtc.Configuration{ - ICEServers: []webrtc.ICEServer{ - { - URLs: []string{"stun:stun.l.google.com:19302"}, - }, - }, - } - - // Create a new API with Trickle ICE enabled - // This SettingEngine allows non-standard WebRTC behavior - s := webrtc.SettingEngine{} - s.SetTrickle(true) - api := webrtc.NewAPI(webrtc.WithSettingEngine(s)) - - // Create a new RTCPeerConnection - peerConnection, err := api.NewPeerConnection(config) - if err != nil { - panic(err) - } - - // When an ICE candidate is available send to the other Pion instance - // the other Pion instance will add this candidate by calling AddICECandidate - peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) { - if c == nil { - return - } - - candidatesMux.Lock() - defer candidatesMux.Unlock() - - desc := peerConnection.RemoteDescription() - if desc == nil { - pendingCandidates = append(pendingCandidates, c) - } else if onICECandidateErr := signalCandidate(*answerAddr, c); err != nil { - panic(onICECandidateErr) - } - }) - - // A HTTP handler that allows the other Pion instance to send us ICE candidates - // This allows us to add ICE candidates faster, we don't have to wait for STUN or TURN - // candidates which may be slower - http.HandleFunc("/candidate", func(w http.ResponseWriter, r *http.Request) { - candidate, candidateErr := ioutil.ReadAll(r.Body) - if candidateErr != nil { - panic(candidateErr) - } - if candidateErr := peerConnection.AddICECandidate(webrtc.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil { - panic(candidateErr) - } - }) - - // A HTTP handler that processes a SessionDescription given to us from the other Pion process - http.HandleFunc("/sdp", func(w http.ResponseWriter, r *http.Request) { - sdp := webrtc.SessionDescription{} - if sdpErr := json.NewDecoder(r.Body).Decode(&sdp); sdpErr != nil { - panic(sdpErr) - } - - if sdpErr := peerConnection.SetRemoteDescription(sdp); sdpErr != nil { - panic(sdpErr) - } - - candidatesMux.Lock() - defer candidatesMux.Unlock() - - for _, c := range pendingCandidates { - if onICECandidateErr := signalCandidate(*answerAddr, c); onICECandidateErr != nil { - panic(onICECandidateErr) - } - } - }) - // Start HTTP server that accepts requests from the answer process - go func() { panic(http.ListenAndServe(*offerAddr, nil)) }() - - // Create a datachannel with label 'data' - dataChannel, err := peerConnection.CreateDataChannel("data", nil) - 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()) - }) - - // Register channel opening handling - dataChannel.OnOpen(func() { - fmt.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds\n", dataChannel.Label(), dataChannel.ID()) - - for range time.NewTicker(5 * time.Second).C { - message := signal.RandSeq(15) - fmt.Printf("Sending '%s'\n", message) - - // Send the message as text - sendTextErr := dataChannel.SendText(message) - if sendTextErr != nil { - panic(sendTextErr) - } - } - }) - - // Register text message handling - dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) { - fmt.Printf("Message from DataChannel '%s': '%s'\n", dataChannel.Label(), string(msg.Data)) - }) - - // Create an offer to send to the other process - offer, err := peerConnection.CreateOffer(nil) - if err != nil { - panic(err) - } - - // Sets the LocalDescription, and starts our UDP listeners - // Note: this will start the gathering of ICE candidates - if err = peerConnection.SetLocalDescription(offer); err != nil { - panic(err) - } - - // Send our offer to the HTTP server listening in the other process - payload, err := json.Marshal(offer) - if err != nil { - panic(err) - } - resp, err := http.Post(fmt.Sprintf("http://%s/sdp", *answerAddr), "application/json; charset=utf-8", bytes.NewReader(payload)) - if err != nil { - panic(err) - } else if err := resp.Body.Close(); err != nil { - panic(err) - } - - // Block forever - select {} -} diff --git a/examples/pion-to-pion/answer/main.go b/examples/pion-to-pion/answer/main.go index 4502801f0a0..c1be3ab0acc 100644 --- a/examples/pion-to-pion/answer/main.go +++ b/examples/pion-to-pion/answer/main.go @@ -1,10 +1,13 @@ package main import ( + "bytes" "encoding/json" "flag" "fmt" + "io/ioutil" "net/http" + "sync" "time" "github.com/pion/webrtc/v3" @@ -12,10 +15,29 @@ import ( "github.com/pion/webrtc/v3/examples/internal/signal" ) +func signalCandidate(addr string, c *webrtc.ICECandidate) error { + payload := []byte(c.ToJSON().Candidate) + resp, err := http.Post(fmt.Sprintf("http://%s/candidate", addr), + "application/json; charset=utf-8", bytes.NewReader(payload)) + + if err != nil { + return err + } + + if closeErr := resp.Body.Close(); closeErr != nil { + return closeErr + } + + return nil +} + func main() { - addr := flag.String("address", ":50000", "Address to host the HTTP server on.") + offerAddr := flag.String("offer-address", "localhost:50000", "Address that the Offer HTTP server is hosted on.") + answerAddr := flag.String("answer-address", ":60000", "Address that the Answer HTTP server is hosted on.") flag.Parse() + var candidatesMux sync.Mutex + pendingCandidates := make([]*webrtc.ICECandidate, 0) // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Prepare the configuration @@ -33,6 +55,82 @@ func main() { panic(err) } + // When an ICE candidate is available send to the other Pion instance + // the other Pion instance will add this candidate by calling AddICECandidate + peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) { + if c == nil { + return + } + + candidatesMux.Lock() + defer candidatesMux.Unlock() + + desc := peerConnection.RemoteDescription() + if desc == nil { + pendingCandidates = append(pendingCandidates, c) + } else if onICECandidateErr := signalCandidate(*offerAddr, c); onICECandidateErr != nil { + panic(onICECandidateErr) + } + }) + + // A HTTP handler that allows the other Pion instance to send us ICE candidates + // This allows us to add ICE candidates faster, we don't have to wait for STUN or TURN + // candidates which may be slower + http.HandleFunc("/candidate", func(w http.ResponseWriter, r *http.Request) { + candidate, candidateErr := ioutil.ReadAll(r.Body) + if candidateErr != nil { + panic(candidateErr) + } + if candidateErr := peerConnection.AddICECandidate(webrtc.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil { + panic(candidateErr) + } + }) + + // A HTTP handler that processes a SessionDescription given to us from the other Pion process + http.HandleFunc("/sdp", func(w http.ResponseWriter, r *http.Request) { + sdp := webrtc.SessionDescription{} + if err := json.NewDecoder(r.Body).Decode(&sdp); err != nil { + panic(err) + } + + if err := peerConnection.SetRemoteDescription(sdp); err != nil { + panic(err) + } + + // Create an answer to send to the other process + answer, err := peerConnection.CreateAnswer(nil) + if err != nil { + panic(err) + } + + // Send our answer to the HTTP server listening in the other process + payload, err := json.Marshal(answer) + if err != nil { + panic(err) + } + resp, err := http.Post(fmt.Sprintf("http://%s/sdp", *offerAddr), "application/json; charset=utf-8", bytes.NewReader(payload)) + if err != nil { + panic(err) + } else if closeErr := resp.Body.Close(); closeErr != nil { + panic(closeErr) + } + + // Sets the LocalDescription, and starts our UDP listeners + err = peerConnection.SetLocalDescription(answer) + if err != nil { + panic(err) + } + + candidatesMux.Lock() + for _, c := range pendingCandidates { + onICECandidateErr := signalCandidate(*offerAddr, c) + if onICECandidateErr != nil { + panic(onICECandidateErr) + } + } + candidatesMux.Unlock() + }) + // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { @@ -65,76 +163,6 @@ func main() { }) }) - // Exchange the offer/answer via HTTP - offerChan, answerChan := mustSignalViaHTTP(*addr) - - // Wait for the remote SessionDescription - offer := <-offerChan - - err = peerConnection.SetRemoteDescription(offer) - if err != nil { - panic(err) - } - - // Create answer - answer, err := peerConnection.CreateAnswer(nil) - if err != nil { - panic(err) - } - - // Sets the LocalDescription, and starts our UDP listeners - err = peerConnection.SetLocalDescription(answer) - if err != nil { - panic(err) - } - - // Send the answer - answerChan <- answer - - // Block forever - select {} -} - -// mustSignalViaHTTP exchange the SDP offer and answer using an HTTP server. -func mustSignalViaHTTP(address string) (chan webrtc.SessionDescription, chan webrtc.SessionDescription) { - offerOut := make(chan webrtc.SessionDescription) - answerIn := make(chan webrtc.SessionDescription) - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.Body == nil { - http.Error(w, "Please send a request body", 400) - return - } - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", http.MethodPost) - w.Header().Set("Access-Control-Allow-Headers", "Content-Type") - if r.Method == http.MethodOptions { - return - } - if r.Method != http.MethodPost { - http.Error(w, "Please send a "+http.MethodPost+" request", 400) - return - } - - var offer webrtc.SessionDescription - err := json.NewDecoder(r.Body).Decode(&offer) - if err != nil { - panic(err) - } - - offerOut <- offer - answer := <-answerIn - - err = json.NewEncoder(w).Encode(answer) - if err != nil { - panic(err) - } - }) - - go func() { - panic(http.ListenAndServe(address, nil)) - }() - fmt.Println("Listening on", address) - - return offerOut, answerIn + // Start HTTP server that accepts requests from the offer process to exchange SDP and Candidates + panic(http.ListenAndServe(*answerAddr, nil)) } diff --git a/examples/pion-to-pion/offer/main.go b/examples/pion-to-pion/offer/main.go index 1d35d981c79..2a31f5371b5 100644 --- a/examples/pion-to-pion/offer/main.go +++ b/examples/pion-to-pion/offer/main.go @@ -5,7 +5,9 @@ import ( "encoding/json" "flag" "fmt" + "io/ioutil" "net/http" + "sync" "time" "github.com/pion/webrtc/v3" @@ -13,10 +15,30 @@ import ( "github.com/pion/webrtc/v3/examples/internal/signal" ) +func signalCandidate(addr string, c *webrtc.ICECandidate) error { + payload := []byte(c.ToJSON().Candidate) + resp, err := http.Post(fmt.Sprintf("http://%s/candidate", addr), + "application/json; charset=utf-8", bytes.NewReader(payload)) + + if err != nil { + return err + } + + if closeErr := resp.Body.Close(); closeErr != nil { + return closeErr + } + + return nil +} + func main() { - addr := flag.String("address", "127.0.0.1:50000", "Address that the HTTP server is hosted on.") + offerAddr := flag.String("offer-address", ":50000", "Address that the Offer HTTP server is hosted on.") + answerAddr := flag.String("answer-address", "127.0.0.1:60000", "Address that the Answer HTTP server is hosted on.") flag.Parse() + var candidatesMux sync.Mutex + pendingCandidates := make([]*webrtc.ICECandidate, 0) + // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Prepare the configuration @@ -34,6 +56,60 @@ func main() { panic(err) } + // When an ICE candidate is available send to the other Pion instance + // the other Pion instance will add this candidate by calling AddICECandidate + peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) { + if c == nil { + return + } + + candidatesMux.Lock() + defer candidatesMux.Unlock() + + desc := peerConnection.RemoteDescription() + if desc == nil { + pendingCandidates = append(pendingCandidates, c) + } else if onICECandidateErr := signalCandidate(*answerAddr, c); err != nil { + panic(onICECandidateErr) + } + }) + + // A HTTP handler that allows the other Pion instance to send us ICE candidates + // This allows us to add ICE candidates faster, we don't have to wait for STUN or TURN + // candidates which may be slower + http.HandleFunc("/candidate", func(w http.ResponseWriter, r *http.Request) { + candidate, candidateErr := ioutil.ReadAll(r.Body) + if candidateErr != nil { + panic(candidateErr) + } + if candidateErr := peerConnection.AddICECandidate(webrtc.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil { + panic(candidateErr) + } + }) + + // A HTTP handler that processes a SessionDescription given to us from the other Pion process + http.HandleFunc("/sdp", func(w http.ResponseWriter, r *http.Request) { + sdp := webrtc.SessionDescription{} + if sdpErr := json.NewDecoder(r.Body).Decode(&sdp); sdpErr != nil { + panic(sdpErr) + } + + if sdpErr := peerConnection.SetRemoteDescription(sdp); sdpErr != nil { + panic(sdpErr) + } + + candidatesMux.Lock() + defer candidatesMux.Unlock() + + for _, c := range pendingCandidates { + if onICECandidateErr := signalCandidate(*answerAddr, c); onICECandidateErr != nil { + panic(onICECandidateErr) + } + } + }) + // Start HTTP server that accepts requests from the answer process + go func() { panic(http.ListenAndServe(*offerAddr, nil)) }() + // Create a datachannel with label 'data' dataChannel, err := peerConnection.CreateDataChannel("data", nil) if err != nil { @@ -67,55 +143,30 @@ func main() { fmt.Printf("Message from DataChannel '%s': '%s'\n", dataChannel.Label(), string(msg.Data)) }) - // Create an offer to send to the browser + // Create an offer to send to the other process offer, err := peerConnection.CreateOffer(nil) if err != nil { panic(err) } // Sets the LocalDescription, and starts our UDP listeners - err = peerConnection.SetLocalDescription(offer) - if err != nil { - panic(err) - } - - // Exchange the offer for the answer - answer := mustSignalViaHTTP(offer, *addr) - - // Apply the answer as the remote description - err = peerConnection.SetRemoteDescription(answer) - if err != nil { + // Note: this will start the gathering of ICE candidates + if err = peerConnection.SetLocalDescription(offer); err != nil { panic(err) } - // Block forever - select {} -} - -// mustSignalViaHTTP exchange the SDP offer and answer using an HTTP Post request. -func mustSignalViaHTTP(offer webrtc.SessionDescription, address string) webrtc.SessionDescription { - b := new(bytes.Buffer) - err := json.NewEncoder(b).Encode(offer) + // Send our offer to the HTTP server listening in the other process + payload, err := json.Marshal(offer) if err != nil { panic(err) } - - resp, err := http.Post("http://"+address, "application/json; charset=utf-8", b) + resp, err := http.Post(fmt.Sprintf("http://%s/sdp", *answerAddr), "application/json; charset=utf-8", bytes.NewReader(payload)) if err != nil { panic(err) - } - defer func() { - closeErr := resp.Body.Close() - if closeErr != nil { - panic(closeErr) - } - }() - - var answer webrtc.SessionDescription - err = json.NewDecoder(resp.Body).Decode(&answer) - if err != nil { + } else if err := resp.Body.Close(); err != nil { panic(err) } - return answer + // Block forever + select {} } diff --git a/examples/play-from-disk-renegotation/main.go b/examples/play-from-disk-renegotation/main.go index e37aedb8d4e..39b0e6b8b42 100644 --- a/examples/play-from-disk-renegotation/main.go +++ b/examples/play-from-disk-renegotation/main.go @@ -27,6 +27,9 @@ func doSignaling(w http.ResponseWriter, r *http.Request) { 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) @@ -34,7 +37,12 @@ func doSignaling(w http.ResponseWriter, r *http.Request) { panic(err) } - response, err := json.Marshal(answer) + // 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) } @@ -100,6 +108,8 @@ func main() { http.HandleFunc("/createPeerConnection", createPeerConnection) http.HandleFunc("/addVideo", addVideo) http.HandleFunc("/removeVideo", removeVideo) + + fmt.Println("Open http://localhost:8080 to access this demo") panic(http.ListenAndServe(":8080", nil)) } diff --git a/examples/play-from-disk/main.go b/examples/play-from-disk/main.go index 7edfcee6c52..a2eee908698 100644 --- a/examples/play-from-disk/main.go +++ b/examples/play-from-disk/main.go @@ -177,13 +177,21 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners 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 + // Output the answer in base64 so we can paste it in browser - fmt.Println(signal.Encode(answer)) + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} diff --git a/examples/reflect/main.go b/examples/reflect/main.go index f094e1c5cbb..dd8a80a28c3 100644 --- a/examples/reflect/main.go +++ b/examples/reflect/main.go @@ -112,14 +112,22 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if 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 + // Output the answer in base64 so we can paste it in browser - fmt.Println(signal.Encode(answer)) + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} diff --git a/examples/rtp-forwarder/main.go b/examples/rtp-forwarder/main.go index 8d139255438..5209bc95ce8 100644 --- a/examples/rtp-forwarder/main.go +++ b/examples/rtp-forwarder/main.go @@ -157,13 +157,21 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners 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 + // Output the answer in base64 so we can paste it in browser - fmt.Println(signal.Encode(answer)) + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Wait for context to be done <-ctx.Done() diff --git a/examples/rtp-to-webrtc/main.go b/examples/rtp-to-webrtc/main.go index bb0c3e1bc8e..60fee66f9a6 100644 --- a/examples/rtp-to-webrtc/main.go +++ b/examples/rtp-to-webrtc/main.go @@ -100,13 +100,21 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners 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 + // Output the answer in base64 so we can paste it in browser - fmt.Println(signal.Encode(answer)) + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Read RTP packets forever and send them to the WebRTC Client for { diff --git a/examples/save-to-disk/main.go b/examples/save-to-disk/main.go index 473ccd3f92d..7ee2cc92ade 100644 --- a/examples/save-to-disk/main.go +++ b/examples/save-to-disk/main.go @@ -142,14 +142,22 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if 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 + // Output the answer in base64 so we can paste it in browser - fmt.Println(signal.Encode(answer)) + fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} diff --git a/examples/swap-tracks/main.go b/examples/swap-tracks/main.go index 8d590901da3..3f13110ab1e 100644 --- a/examples/swap-tracks/main.go +++ b/examples/swap-tracks/main.go @@ -143,14 +143,22 @@ func main() { panic(err) } + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if 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 + // Output the answer in base64 so we can paste it in browser - fmt.Printf("Paste below base64 in browser:\n%v\n", signal.Encode(answer)) + fmt.Printf("Paste below base64 in browser:\n%v\n", signal.Encode(*peerConnection.LocalDescription())) // Asynchronously take all packets in the channel and write them out to our // track diff --git a/examples/vnet/show-network-usage/main.go b/examples/vnet/show-network-usage/main.go index bfb5ba51144..75fb95c7a61 100644 --- a/examples/vnet/show-network-usage/main.go +++ b/examples/vnet/show-network-usage/main.go @@ -108,6 +108,22 @@ func main() { answerPeerConnection, err := answerAPI.NewPeerConnection(webrtc.Configuration{}) panicIfError(err) + // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate + // send it to the other peer + answerPeerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { + if i != nil { + panicIfError(offerPeerConnection.AddICECandidate(i.ToJSON())) + } + }) + + // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate + // send it to the other peer + offerPeerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { + if i != nil { + panicIfError(answerPeerConnection.AddICECandidate(i.ToJSON())) + } + }) + offerDataChannel, err := offerPeerConnection.CreateDataChannel("label", nil) panicIfError(err) diff --git a/gathering_complete_promise.go b/gathering_complete_promise.go new file mode 100644 index 00000000000..d98a0c3f763 --- /dev/null +++ b/gathering_complete_promise.go @@ -0,0 +1,20 @@ +package webrtc + +import "context" + +// GatheringCompletePromise is a Pion specific helper function that returns a channel that is closed when gathering is complete. +// This function may be helpful in cases where you are unable to trickle your ICE Candidates. +// +// It is better to not use this function, and instead trickle candidates. If you use this function you will see longer connection startup times. +// When the call is connected you will see no impact however. +func GatheringCompletePromise(pc *PeerConnection) (gatherComplete <-chan struct{}) { + gatheringComplete, done := context.WithCancel(context.Background()) + + if pc.ICEGatheringState() == ICEGatheringStateComplete { + done() + } else { + pc.setGatherCompleteHdlr(func() { done() }) + } + + return gatheringComplete.Done() +} diff --git a/go.mod b/go.mod index 7208ef13435..f678598bb13 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.12 require ( github.com/pion/datachannel v1.4.17 github.com/pion/dtls/v2 v2.0.1 - github.com/pion/ice v0.7.15 + github.com/pion/ice/v2 v2.0.0-rc.1 github.com/pion/logging v0.2.2 github.com/pion/quic v0.1.1 github.com/pion/rtcp v1.2.3 diff --git a/go.sum b/go.sum index d66aa41c728..2c6340e4498 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -27,11 +29,10 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pion/datachannel v1.4.17 h1:8CChK5VrJoGrwKCysoTscoWvshCAFpUkgY11Tqgz5hE= github.com/pion/datachannel v1.4.17/go.mod h1:+vPQfypU9vSsyPXogYj1hBThWQ6MNXEQoQAzxoPvjYM= -github.com/pion/dtls/v2 v2.0.0/go.mod h1:VkY5VL2wtsQQOG60xQ4lkV5pdn0wwBBTzCfRJqXhp3A= github.com/pion/dtls/v2 v2.0.1 h1:ddE7+V0faYRbyh4uPsRZ2vLdRrjVZn+wmCfI7jlBfaA= github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U= -github.com/pion/ice v0.7.15 h1:s1In+gnuyVq7WKWGVQL+1p+OcrMsbfL+VfSe2isH8Ag= -github.com/pion/ice v0.7.15/go.mod h1:Z6zybEQgky5mZkKcLfmvc266JukK2srz3VZBBD1iXBw= +github.com/pion/ice/v2 v2.0.0-rc.1 h1:1/5XKZx6Ioladykw5xp9/fCZG61pcmndTjY9bZhG0Fs= +github.com/pion/ice/v2 v2.0.0-rc.1/go.mod h1:5sP3yQ8Kd/azvPS4UrVTSgs/p5jfXMy3Ft2dQZBWyI8= 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/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= @@ -49,8 +50,9 @@ github.com/pion/sdp/v2 v2.3.8 h1:iXlpgsRx3dFGdU8vhSOKZiiTVp2BjIXxh0ims6MqBzk= github.com/pion/sdp/v2 v2.3.8/go.mod h1:VyECSprlbQuv07sO/d0grXEX890kF13YLQt2F0NOqYA= github.com/pion/srtp v1.3.4 h1:idh+9/W7tLOsHjcYYketIPSShb9k2Dz+RVrqyCm2LQE= github.com/pion/srtp v1.3.4/go.mod h1:M3+LQiqLfVcV/Jo46KYJ3z9PP8DjmGPW8fUOQrF6q/M= -github.com/pion/stun v0.3.3 h1:brYuPl9bN9w/VM7OdNzRSLoqsnwlyNvD9MVeJrHjDQw= github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= +github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= +github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= github.com/pion/transport v0.10.0 h1:9M12BSneJm6ggGhJyWpDveFOstJsTiQjkLf4M44rm80= @@ -73,15 +75,12 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed h1:g4KENRiCMEx58Q7/ecwfT0N2o8z35Fnbsjig/Alf2T4= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= diff --git a/icecandidate.go b/icecandidate.go index 2998dcdf742..e4d70acb1c1 100644 --- a/icecandidate.go +++ b/icecandidate.go @@ -3,7 +3,7 @@ package webrtc import ( "fmt" - "github.com/pion/ice" + "github.com/pion/ice/v2" "github.com/pion/sdp/v2" ) diff --git a/icecandidate_test.go b/icecandidate_test.go index d8a6b73e5ae..b5a0076645a 100644 --- a/icecandidate_test.go +++ b/icecandidate_test.go @@ -3,7 +3,7 @@ package webrtc import ( "testing" - "github.com/pion/ice" + "github.com/pion/ice/v2" "github.com/stretchr/testify/assert" ) diff --git a/icecandidatetype.go b/icecandidatetype.go index ad1591042c1..8fa6e300e94 100644 --- a/icecandidatetype.go +++ b/icecandidatetype.go @@ -3,7 +3,7 @@ package webrtc import ( "fmt" - "github.com/pion/ice" + "github.com/pion/ice/v2" ) // ICECandidateType represents the type of the ICE candidate used. diff --git a/icegatherer.go b/icegatherer.go index 7656898efbe..ac18f452767 100644 --- a/icegatherer.go +++ b/icegatherer.go @@ -6,7 +6,7 @@ import ( "sync" "sync/atomic" - "github.com/pion/ice" + "github.com/pion/ice/v2" "github.com/pion/logging" ) @@ -27,6 +27,9 @@ type ICEGatherer struct { onLocalCandidateHdlr atomic.Value // func(candidate *ICECandidate) onStateChangeHdlr atomic.Value // func(state ICEGathererState) + // Used for GatheringCompletePromise + onGatheringCompleteHdlr atomic.Value // func() + api *API } @@ -58,7 +61,7 @@ func (g *ICEGatherer) createAgent() error { g.lock.Lock() defer g.lock.Unlock() - if g.agent != nil { + if g.agent != nil || g.State() != ICEGathererStateNew { return nil } @@ -85,13 +88,13 @@ func (g *ICEGatherer) createAgent() error { } config := &ice.AgentConfig{ - Trickle: g.api.settingEngine.candidates.ICETrickle, Lite: g.api.settingEngine.candidates.ICELite, Urls: g.validatedServers, PortMin: g.api.settingEngine.ephemeralUDP.PortMin, PortMax: g.api.settingEngine.ephemeralUDP.PortMax, - ConnectionTimeout: g.api.settingEngine.timeout.ICEConnection, - KeepaliveInterval: g.api.settingEngine.timeout.ICEKeepalive, + DisconnectedTimeout: g.api.settingEngine.timeout.ICEDisconnectedTimeout, + FailedTimeout: g.api.settingEngine.timeout.ICEFailedTimeout, + KeepaliveInterval: g.api.settingEngine.timeout.ICEKeepaliveInterval, LoggerFactory: g.api.settingEngine.LoggerFactory, CandidateTypes: candidateTypes, CandidateSelectionTimeout: g.api.settingEngine.timeout.ICECandidateSelectionTimeout, @@ -124,10 +127,6 @@ func (g *ICEGatherer) createAgent() error { } g.agent = agent - if !g.api.settingEngine.candidates.ICETrickle { - atomicStoreICEGathererState(&g.state, ICEGathererStateComplete) - } - return nil } @@ -142,15 +141,15 @@ func (g *ICEGatherer) Gather() error { onLocalCandidateHdlr = hdlr } + onGatheringCompleteHdlr := func() {} + if hdlr, ok := g.onGatheringCompleteHdlr.Load().(func()); ok && hdlr != nil { + onGatheringCompleteHdlr = hdlr + } + g.lock.Lock() - isTrickle := g.api.settingEngine.candidates.ICETrickle agent := g.agent g.lock.Unlock() - if !isTrickle { - return nil - } - g.setState(ICEGathererStateGathering) if err := agent.OnCandidate(func(candidate ice.Candidate) { if candidate != nil { @@ -163,6 +162,7 @@ func (g *ICEGatherer) Gather() error { } else { g.setState(ICEGathererStateComplete) + onGatheringCompleteHdlr() onLocalCandidateHdlr(nil) } }); err != nil { @@ -245,32 +245,6 @@ func (g *ICEGatherer) getAgent() *ice.Agent { return g.agent } -// SignalCandidates imitates gathering process to backward support old trickle -// false behavior. -func (g *ICEGatherer) SignalCandidates() error { - candidates, err := g.GetLocalCandidates() - if err != nil { - return err - } - - var onLocalCandidateHdlr func(*ICECandidate) - if hdlr, ok := g.onLocalCandidateHdlr.Load().(func(candidate *ICECandidate)); ok { - onLocalCandidateHdlr = hdlr - } - - if onLocalCandidateHdlr != nil { - go func() { - for i := range candidates { - onLocalCandidateHdlr(&candidates[i]) - } - // Call the handler one last time with nil. This is a signal that candidate - // gathering is complete. - onLocalCandidateHdlr(nil) - }() - } - return nil -} - func (g *ICEGatherer) collectStats(collector *statsReportCollector) { agent := g.getAgent() if agent == nil { diff --git a/icegatherer_test.go b/icegatherer_test.go index 6aa867c726a..0ce14539252 100644 --- a/icegatherer_test.go +++ b/icegatherer_test.go @@ -33,11 +33,19 @@ func TestNewICEGatherer_Success(t *testing.T) { t.Fatalf("Expected gathering state new") } - err = gatherer.Gather() - if err != nil { + gatherFinished := make(chan struct{}) + gatherer.OnLocalCandidate(func(i *ICECandidate) { + if i == nil { + close(gatherFinished) + } + }) + + if err = gatherer.Gather(); err != nil { t.Error(err) } + <-gatherFinished + params, err := gatherer.GetLocalParameters() if err != nil { t.Error(err) @@ -60,60 +68,6 @@ func TestNewICEGatherer_Success(t *testing.T) { assert.NoError(t, gatherer.Close()) } -func TestICEGather_LocalCandidateOrder(t *testing.T) { - // Limit runtime in case of deadlocks - lim := test.TimeOut(time.Second * 20) - defer lim.Stop() - - report := test.CheckRoutines(t) - defer report() - - opts := ICEGatherOptions{ - ICEServers: []ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}, - } - - gatherer, err := NewAPI().NewICEGatherer(opts) - if err != nil { - t.Error(err) - } - - if gatherer.State() != ICEGathererStateNew { - t.Fatalf("Expected gathering state new") - } - - for i := 0; i < 10; i++ { - candidate := make(chan *ICECandidate) - gatherer.OnLocalCandidate(func(c *ICECandidate) { - candidate <- c - }) - - if err := gatherer.SignalCandidates(); err != nil { - t.Error(err) - } - endGathering := false - - L: - for { - select { - case c := <-candidate: - if c == nil { - endGathering = true - } else if endGathering { - t.Error("Received a candidate after the last candidate") - break L - } - case <-time.After(100 * time.Millisecond): - if !endGathering { - t.Error("Timed out before receiving the last candidate") - } - break L - } - } - } - - assert.NoError(t, gatherer.Close()) -} - func TestICEGather_mDNSCandidateGathering(t *testing.T) { // Limit runtime in case of deadlocks lim := test.TimeOut(time.Second * 20) @@ -137,9 +91,7 @@ func TestICEGather_mDNSCandidateGathering(t *testing.T) { } }) - if err := gatherer.SignalCandidates(); err != nil { - t.Error(err) - } + assert.NoError(t, gatherer.Gather()) <-gotMulticastDNSCandidate.Done() assert.NoError(t, gatherer.Close()) diff --git a/iceserver.go b/iceserver.go index cbcfa604bb2..c5faf72829e 100644 --- a/iceserver.go +++ b/iceserver.go @@ -3,7 +3,7 @@ package webrtc import ( - "github.com/pion/ice" + "github.com/pion/ice/v2" "github.com/pion/webrtc/v3/pkg/rtcerr" ) diff --git a/iceserver_js.go b/iceserver_js.go index 7ccfd89ab3d..c7f249e0b3e 100644 --- a/iceserver_js.go +++ b/iceserver_js.go @@ -5,7 +5,7 @@ package webrtc import ( "errors" - "github.com/pion/ice" + "github.com/pion/ice/v2" ) // ICEServer describes a single STUN and TURN server that can be used by diff --git a/iceserver_test.go b/iceserver_test.go index ae6ca64c77e..54450637dd9 100644 --- a/iceserver_test.go +++ b/iceserver_test.go @@ -5,7 +5,7 @@ package webrtc import ( "testing" - "github.com/pion/ice" + "github.com/pion/ice/v2" "github.com/pion/webrtc/v3/pkg/rtcerr" "github.com/stretchr/testify/assert" ) diff --git a/icetransport.go b/icetransport.go index 06ca39d2dfd..2d43b4ab673 100644 --- a/icetransport.go +++ b/icetransport.go @@ -9,7 +9,7 @@ import ( "sync/atomic" "time" - "github.com/pion/ice" + "github.com/pion/ice/v2" "github.com/pion/logging" "github.com/pion/webrtc/v3/internal/mux" ) @@ -113,7 +113,7 @@ func (t *ICETransport) Start(gatherer *ICEGatherer, params ICEParameters, role * } t.role = *role - // Drop the lock here to allow trickle-ICE candidates to be + // Drop the lock here to allow ICE candidates to be // added so that the agent can complete a connection t.lock.Unlock() @@ -271,8 +271,7 @@ func (t *ICETransport) NewEndpoint(f mux.MatchFunc) *mux.Endpoint { func (t *ICETransport) ensureGatherer() error { if t.gatherer == nil { return errors.New("gatherer not started") - } else if t.gatherer.getAgent() == nil && t.gatherer.api.settingEngine.candidates.ICETrickle { - // Special case for trickle=true. (issue-707) + } else if t.gatherer.getAgent() == nil { if err := t.gatherer.createAgent(); err != nil { return err } diff --git a/icetransportstate.go b/icetransportstate.go index cbc02ce089b..da93e44d4ff 100644 --- a/icetransportstate.go +++ b/icetransportstate.go @@ -1,6 +1,6 @@ package webrtc -import "github.com/pion/ice" +import "github.com/pion/ice/v2" // ICETransportState represents the current state of the ICE transport. type ICETransportState int diff --git a/icetransportstate_test.go b/icetransportstate_test.go index 62c5dfa6252..c5f72626609 100644 --- a/icetransportstate_test.go +++ b/icetransportstate_test.go @@ -3,7 +3,7 @@ package webrtc import ( "testing" - "github.com/pion/ice" + "github.com/pion/ice/v2" "github.com/stretchr/testify/assert" ) diff --git a/internal/mux/endpoint.go b/internal/mux/endpoint.go index 6f0cc2f4111..3eca393ebea 100644 --- a/internal/mux/endpoint.go +++ b/internal/mux/endpoint.go @@ -5,7 +5,7 @@ import ( "net" "time" - "github.com/pion/ice" + "github.com/pion/ice/v2" "github.com/pion/transport/packetio" ) diff --git a/networktype.go b/networktype.go index 7c358592600..3c612e5b0e3 100644 --- a/networktype.go +++ b/networktype.go @@ -3,7 +3,7 @@ package webrtc import ( "fmt" - "github.com/pion/ice" + "github.com/pion/ice/v2" ) func supportedNetworkTypes() []NetworkType { diff --git a/peerconnection.go b/peerconnection.go index 71da7526106..10feac9f2ee 100644 --- a/peerconnection.go +++ b/peerconnection.go @@ -46,9 +46,8 @@ type PeerConnection struct { idpLoginURL *string - isClosed *atomicBool - negotiationNeeded bool - nonTrickleCandidatesSignaled *atomicBool + isClosed *atomicBool + negotiationNeeded bool lastOffer string lastAnswer string @@ -102,15 +101,14 @@ func (api *API) NewPeerConnection(configuration Configuration) (*PeerConnection, Certificates: []Certificate{}, ICECandidatePoolSize: 0, }, - isClosed: &atomicBool{}, - negotiationNeeded: false, - nonTrickleCandidatesSignaled: &atomicBool{}, - lastOffer: "", - lastAnswer: "", - greaterMid: -1, - signalingState: SignalingStateStable, - iceConnectionState: ICEConnectionStateNew, - connectionState: PeerConnectionStateNew, + isClosed: &atomicBool{}, + negotiationNeeded: false, + lastOffer: "", + lastAnswer: "", + greaterMid: -1, + signalingState: SignalingStateStable, + iceConnectionState: ICEConnectionStateNew, + connectionState: PeerConnectionStateNew, api: api, log: api.settingEngine.LoggerFactory.NewLogger("pc"), @@ -126,12 +124,6 @@ func (api *API) NewPeerConnection(configuration Configuration) (*PeerConnection, return nil, err } - if !pc.api.settingEngine.candidates.ICETrickle { - if err = pc.iceGatherer.Gather(); err != nil { - return nil, err - } - } - // Create the ice transport iceTransport := pc.createICETransport() pc.iceTransport = iceTransport @@ -741,18 +733,6 @@ func (pc *PeerConnection) SetLocalDescription(desc SessionDescription) error { }) } - // To support all unittests which are following the future trickle=true - // setup while also support the old trickle=false synchronous gathering - // process this is necessary to avoid calling Gather() in multiple - // places; which causes race conditions. (issue-707) - if !pc.api.settingEngine.candidates.ICETrickle && !pc.nonTrickleCandidatesSignaled.get() { - if err := pc.iceGatherer.SignalCandidates(); err != nil { - return err - } - pc.nonTrickleCandidatesSignaled.set(true) - return nil - } - if pc.iceGatherer.State() == ICEGathererStateNew { return pc.iceGatherer.Gather() } @@ -1889,3 +1869,7 @@ func (pc *PeerConnection) generateMatchedSDP(useIdentity bool, includeUnmatched return populateSDP(d, detectedPlanB, pc.api.settingEngine.candidates.ICELite, pc.api.mediaEngine, connectionRole, candidates, iceParams, mediaSections, pc.ICEGatheringState()) } + +func (pc *PeerConnection) setGatherCompleteHdlr(hdlr func()) { + pc.iceGatherer.onGatheringCompleteHdlr.Store(hdlr) +} diff --git a/peerconnection_close_test.go b/peerconnection_close_test.go index e37d69ef012..7b6f9f80b5b 100644 --- a/peerconnection_close_test.go +++ b/peerconnection_close_test.go @@ -120,13 +120,10 @@ func TestPeerConnection_Close_DuringICE(t *testing.T) { pcAnswer.OnICEConnectionStateChange(func(iceState ICEConnectionState) { if iceState == ICEConnectionStateConnected { go func() { - if err2 := pcAnswer.Close(); err2 != nil { - t.Errorf("pcAnswer.Close() failed: %v", err2) - } + assert.NoError(t, pcAnswer.Close()) close(closedAnswer) - if err2 := pcOffer.Close(); err2 != nil { - t.Errorf("pcOffer.Close() failed: %v", err2) - } + + assert.NoError(t, pcOffer.Close()) close(closedOffer) }() } @@ -136,20 +133,25 @@ func TestPeerConnection_Close_DuringICE(t *testing.T) { if err != nil { t.Fatal(err) } + offerGatheringComplete := GatheringCompletePromise(pcOffer) if err = pcOffer.SetLocalDescription(offer); err != nil { t.Fatal(err) } - if err = pcAnswer.SetRemoteDescription(offer); err != nil { + <-offerGatheringComplete + if err = pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription()); err != nil { t.Fatal(err) } + answer, err := pcAnswer.CreateAnswer(nil) if err != nil { t.Fatal(err) } + answerGatheringComplete := GatheringCompletePromise(pcAnswer) if err = pcAnswer.SetLocalDescription(answer); err != nil { t.Fatal(err) } - if err = pcOffer.SetRemoteDescription(answer); err != nil { + <-answerGatheringComplete + if err = pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()); err != nil { t.Fatal(err) } diff --git a/peerconnection_go_test.go b/peerconnection_go_test.go index e6e40d22729..ba5de119526 100644 --- a/peerconnection_go_test.go +++ b/peerconnection_go_test.go @@ -16,7 +16,7 @@ import ( "testing" "time" - "github.com/pion/ice" + "github.com/pion/ice/v2" "github.com/pion/transport/test" "github.com/pion/webrtc/v3/internal/util" "github.com/pion/webrtc/v3/pkg/rtcerr" @@ -528,84 +528,13 @@ func TestOneAttrKeyConnectionSetupPerMediaDescriptionInSDP(t *testing.T) { assert.NoError(t, pc.Close()) } -// Assert that candidates are gathered by calling SetLocalDescription, not SetRemoteDescription -// When trickle in on by default we can move this to peerconnection_test.go -func TestGatherOnSetLocalDescription(t *testing.T) { - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() - - report := test.CheckRoutines(t) - defer report() - - pcOfferGathered := make(chan SessionDescription) - pcAnswerGathered := make(chan SessionDescription) - - s := SettingEngine{} - s.SetTrickle(true) - api := NewAPI(WithSettingEngine(s)) - - pcOffer, err := api.NewPeerConnection(Configuration{}) - if err != nil { - t.Error(err.Error()) - } - - // We need to create a data channel in order to trigger ICE - if _, err = pcOffer.CreateDataChannel("initial_data_channel", nil); err != nil { - t.Error(err.Error()) - } - - pcOffer.OnICECandidate(func(i *ICECandidate) { - if i == nil { - close(pcOfferGathered) - } - }) - - offer, err := pcOffer.CreateOffer(nil) - if err != nil { - t.Error(err.Error()) - } else if err = pcOffer.SetLocalDescription(offer); err != nil { - t.Error(err.Error()) - } - - <-pcOfferGathered - - pcAnswer, err := api.NewPeerConnection(Configuration{}) - if err != nil { - t.Error(err.Error()) - } - - pcAnswer.OnICECandidate(func(i *ICECandidate) { - if i == nil { - close(pcAnswerGathered) - } - }) - - if err = pcAnswer.SetRemoteDescription(offer); err != nil { - t.Error(err.Error()) - } - - select { - case <-pcAnswerGathered: - t.Fatal("pcAnswer started gathering with no SetLocalDescription") - // Gathering is async, not sure of a better way to catch this currently - case <-time.After(3 * time.Second): - } - - answer, err := pcAnswer.CreateAnswer(nil) - if err != nil { - t.Error(err.Error()) - } else if err = pcAnswer.SetLocalDescription(answer); err != nil { - t.Error(err.Error()) - } - <-pcAnswerGathered - assert.NoError(t, pcOffer.Close()) - assert.NoError(t, pcAnswer.Close()) -} - func TestPeerConnection_OfferingLite(t *testing.T) { report := test.CheckRoutines(t) defer report() + lim := test.TimeOut(time.Second * 10) + defer lim.Stop() + s := SettingEngine{} s.SetLite(true) offerPC, err := NewAPI(WithSettingEngine(s)).NewPeerConnection(Configuration{}) @@ -642,6 +571,9 @@ func TestPeerConnection_AnsweringLite(t *testing.T) { report := test.CheckRoutines(t) defer report() + lim := test.TimeOut(time.Second * 10) + defer lim.Stop() + offerPC, err := NewAPI().NewPeerConnection(Configuration{}) if err != nil { t.Fatal(err) @@ -681,10 +613,7 @@ func TestOnICEGatheringStateChange(t *testing.T) { seenGatheringAndComplete := make(chan interface{}) seenClosed := make(chan interface{}) - s := SettingEngine{} - s.SetTrickle(true) - - peerConn, err := NewAPI(WithSettingEngine(s)).NewPeerConnection(Configuration{}) + peerConn, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) var onStateChange func(s ICEGathererState) @@ -737,19 +666,9 @@ func TestOnICEGatheringStateChange(t *testing.T) { } } -// Assert that when Trickle is enabled two connections can connect +// Assert Trickle ICE behaviors func TestPeerConnectionTrickle(t *testing.T) { - lim := test.TimeOut(time.Second * 10) - defer lim.Stop() - - report := test.CheckRoutines(t) - defer report() - - s := SettingEngine{} - s.SetTrickle(true) - - api := NewAPI(WithSettingEngine(s)) - offerPC, answerPC, err := api.newPair(Configuration{}) + offerPC, answerPC, err := newPair() assert.NoError(t, err) addOrCacheCandidate := func(pc *PeerConnection, c *ICECandidate, candidateCache []ICECandidateInit) []ICECandidateInit { @@ -854,10 +773,7 @@ func TestPopulateLocalCandidates(t *testing.T) { }) t.Run("end-of-candidates only when gathering is complete", func(t *testing.T) { - s := SettingEngine{} - s.SetTrickle(true) - - pc, err := NewAPI(WithSettingEngine(s)).NewPeerConnection(Configuration{}) + pc, err := NewAPI().NewPeerConnection(Configuration{}) assert.NoError(t, err) gatherComplete, gatherCompleteCancel := context.WithCancel(context.Background()) diff --git a/peerconnection_js.go b/peerconnection_js.go index aefd02452f7..5886e9ac8f8 100644 --- a/peerconnection_js.go +++ b/peerconnection_js.go @@ -26,6 +26,9 @@ type PeerConnection struct { onICECandidateHandler *js.Func onICEGatheringStateChangeHandler *js.Func + // Used by GatheringCompletePromise + onGatherCompleteHandler func() + // A reference to the associated API state used by this connection api *API } @@ -313,6 +316,10 @@ func (pc *PeerConnection) OnICECandidate(f func(candidate *ICECandidate)) { } onICECandidateHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { candidate := valueToICECandidate(args[0].Get("candidate")) + if candidate == nil && pc.onGatherCompleteHandler != nil { + go pc.onGatherCompleteHandler() + } + go f(candidate) return js.Undefined() }) @@ -452,6 +459,16 @@ func (pc *PeerConnection) ConnectionState() PeerConnectionState { return newPeerConnectionState(rawState) } +func (pc *PeerConnection) setGatherCompleteHdlr(hdlr func()) { + pc.onGatherCompleteHandler = hdlr + + // If no onIceCandidate handler has been set provide an empty one + // otherwise our onGatherCompleteHandler will not be executed + if pc.onICECandidateHandler == nil { + pc.OnICECandidate(func(i *ICECandidate) {}) + } +} + // Converts a Configuration to js.Value so it can be passed // through to the JavaScript WebRTC API. Any zero values are converted to // js.Undefined(), which will result in the default value being used. diff --git a/peerconnection_media_test.go b/peerconnection_media_test.go index a5d9583d740..6e46d92cbd3 100644 --- a/peerconnection_media_test.go +++ b/peerconnection_media_test.go @@ -316,7 +316,7 @@ func TestPeerConnection_Media_Disconnected(t *testing.T) { defer report() s := SettingEngine{} - s.SetConnectionTimeout(time.Duration(1)*time.Second, time.Duration(250)*time.Millisecond) + s.SetICETimeouts(1*time.Second, 5*time.Second, 250*time.Millisecond) api := NewAPI(WithSettingEngine(s)) api.mediaEngine.RegisterDefaultCodecs() @@ -493,8 +493,12 @@ func TestUndeclaredSSRC(t *testing.T) { offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) + offerGatheringComplete := GatheringCompletePromise(pcOffer) assert.NoError(t, pcOffer.SetLocalDescription(offer)) + <-offerGatheringComplete + offer = *pcOffer.LocalDescription() + // Filter SSRC lines, and remove SCTP filteredSDP := "" scanner := bufio.NewScanner(strings.NewReader(offer.SDP)) @@ -519,8 +523,12 @@ func TestUndeclaredSSRC(t *testing.T) { answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) + + answerGatheringComplete := GatheringCompletePromise(pcAnswer) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) - assert.NoError(t, pcOffer.SetRemoteDescription(answer)) + <-answerGatheringComplete + + assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())) go func() { for { @@ -1134,11 +1142,15 @@ func TestPeerConnection_Start_Only_Negotiated_Senders(t *testing.T) { offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) + offerGatheringComplete := GatheringCompletePromise(pcOffer) assert.NoError(t, pcOffer.SetLocalDescription(offer)) - assert.NoError(t, pcAnswer.SetRemoteDescription(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 // Add a new track between providing the offer and applying the answer @@ -1149,7 +1161,7 @@ func TestPeerConnection_Start_Only_Negotiated_Senders(t *testing.T) { require.NoError(t, err) // apply answer so we'll test generateMatchedSDP - assert.NoError(t, pcOffer.SetRemoteDescription(answer)) + assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())) // Wait for senders to be started by startTransports spawned goroutine pcOffer.ops.Done() diff --git a/peerconnection_renegotiation_test.go b/peerconnection_renegotiation_test.go index 6be015a676e..975b7153da4 100644 --- a/peerconnection_renegotiation_test.go +++ b/peerconnection_renegotiation_test.go @@ -277,14 +277,21 @@ func TestPeerConnection_Transceiver_Mid(t *testing.T) { offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) + offerGatheringComplete := GatheringCompletePromise(pcOffer) assert.NoError(t, pcOffer.SetLocalDescription(offer)) - assert.NoError(t, pcAnswer.SetRemoteDescription(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(answer)) + assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())) pcOffer.ops.Done() pcAnswer.ops.Done() @@ -529,7 +536,6 @@ func TestPeerConnection_Renegotiation_Trickle(t *testing.T) { defer report() settingEngine := SettingEngine{} - settingEngine.SetTrickle(true) api := NewAPI(WithSettingEngine(settingEngine)) api.mediaEngine.RegisterDefaultCodecs() @@ -651,15 +657,22 @@ 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 + offer = *pcOffer.LocalDescription() offer.SDP = strings.Split(offer.SDP, "m=application")[0] assert.NoError(t, pcAnswer.SetRemoteDescription(offer)) answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) + + answerGatheringComplete := GatheringCompletePromise(pcAnswer) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) - assert.NoError(t, pcOffer.SetRemoteDescription(answer)) + <-answerGatheringComplete + + assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())) } api := NewAPI() diff --git a/peerconnection_test.go b/peerconnection_test.go index d5c94ee6576..563c750a548 100644 --- a/peerconnection_test.go +++ b/peerconnection_test.go @@ -1,7 +1,6 @@ package webrtc import ( - "fmt" "reflect" "sync" "testing" @@ -9,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/pion/transport/test" "github.com/pion/webrtc/v3/pkg/rtcerr" ) @@ -29,16 +29,6 @@ func newPair() (pcOffer *PeerConnection, pcAnswer *PeerConnection, err error) { } func signalPair(pcOffer *PeerConnection, pcAnswer *PeerConnection) error { - iceGatheringState := pcOffer.ICEGatheringState() - offerChan := make(chan SessionDescription, 1) - - if iceGatheringState != ICEGatheringStateComplete { - pcOffer.OnICECandidate(func(candidate *ICECandidate) { - if candidate == nil { - offerChan <- *pcOffer.PendingLocalDescription() - } - }) - } // Note(albrow): We need to create a data channel in order to trigger ICE // candidate gathering in the background for the JavaScript/Wasm bindings. If // we don't do this, the complete offer including ICE candidates will never be @@ -51,36 +41,25 @@ func signalPair(pcOffer *PeerConnection, pcAnswer *PeerConnection) error { if err != nil { return err } - if err := pcOffer.SetLocalDescription(offer); err != nil { + offerGatheringComplete := GatheringCompletePromise(pcOffer) + if err = pcOffer.SetLocalDescription(offer); err != nil { return err } - - if iceGatheringState == ICEGatheringStateComplete { - offerChan <- offer + <-offerGatheringComplete + if err = pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription()); err != nil { + return err } - select { - case <-time.After(3 * time.Second): - return fmt.Errorf("timed out waiting to receive offer") - case offer := <-offerChan: - if err := pcAnswer.SetRemoteDescription(offer); err != nil { - return err - } - answer, err := pcAnswer.CreateAnswer(nil) - if err != nil { - return err - } - - if err = pcAnswer.SetLocalDescription(answer); err != nil { - return err - } - - err = pcOffer.SetRemoteDescription(answer) - if err != nil { - return err - } - return nil + answer, err := pcAnswer.CreateAnswer(nil) + if err != nil { + return err + } + answerGatheringComplete := GatheringCompletePromise(pcAnswer) + if err = pcAnswer.SetLocalDescription(answer); err != nil { + return err } + <-answerGatheringComplete + return pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()) } func TestNew(t *testing.T) { @@ -224,6 +203,8 @@ func TestPeerConnection_SetConfiguration(t *testing.T) { if got, want := err, test.wantErr; !reflect.DeepEqual(got, want) { t.Errorf("SetConfiguration %q: err = %v, want %v", test.name, got, want) } + + assert.NoError(t, pc.Close()) } } @@ -282,10 +263,12 @@ func TestSetRemoteDescription(t *testing.T) { if err != nil { t.Errorf("Case %d: got error: %v", i, err) } - err = peerConn.SetRemoteDescription(testCase.desc) - if err != nil { + + if err = peerConn.SetRemoteDescription(testCase.desc); err != nil { t.Errorf("Case %d: got error: %v", i, err) } + + assert.NoError(t, peerConn.Close()) } } @@ -320,6 +303,9 @@ func TestCreateOfferAnswer(t *testing.T) { if err != nil { t.Errorf("SetRemoteDescription (Originator): got error: %v", err) } + + assert.NoError(t, offerPeerConn.Close()) + assert.NoError(t, answerPeerConn.Close()) } func TestPeerConnection_EventHandlers(t *testing.T) { @@ -416,18 +402,21 @@ func TestPeerConnection_EventHandlers(t *testing.T) { case <-timeout: t.Fatalf("timed out waiting for one or more events handlers to be called (these *were* called: %+v)", wasCalled) } + + assert.NoError(t, pcOffer.Close()) + assert.NoError(t, pcAnswer.Close()) } func TestMultipleOfferAnswer(t *testing.T) { - nonTricklePeerConn, err := NewPeerConnection(Configuration{}) + firstPeerConn, err := NewPeerConnection(Configuration{}) if err != nil { t.Errorf("New PeerConnection: got error: %v", err) } - if _, err = nonTricklePeerConn.CreateOffer(nil); err != nil { + if _, err = firstPeerConn.CreateOffer(nil); err != nil { t.Errorf("First Offer: got error: %v", err) } - if _, err = nonTricklePeerConn.CreateOffer(nil); err != nil { + if _, err = firstPeerConn.CreateOffer(nil); err != nil { t.Errorf("Second Offer: got error: %v", err) } @@ -444,6 +433,9 @@ func TestMultipleOfferAnswer(t *testing.T) { if _, err = secondPeerConn.CreateOffer(nil); err != nil { t.Errorf("Second Offer: got error: %v", err) } + + assert.NoError(t, firstPeerConn.Close()) + assert.NoError(t, secondPeerConn.Close()) } func TestNoFingerprintInFirstMediaIfSetRemoteDescription(t *testing.T) { @@ -471,6 +463,9 @@ a=candidate:1 1 udp 2013266431 192.168.84.254 46492 typ host a=end-of-candidates ` + report := test.CheckRoutines(t) + defer report() + pc, err := NewPeerConnection(Configuration{}) if err != nil { t.Error(err.Error()) @@ -481,8 +476,81 @@ a=end-of-candidates SDP: sdpNoFingerprintInFirstMedia, } - err = pc.SetRemoteDescription(desc) + if err = pc.SetRemoteDescription(desc); err != nil { + t.Error(err.Error()) + } + + assert.NoError(t, pc.Close()) +} + +// Assert that candidates are gathered by calling SetLocalDescription, not SetRemoteDescription +func TestGatherOnSetLocalDescription(t *testing.T) { + lim := test.TimeOut(time.Second * 30) + defer lim.Stop() + + report := test.CheckRoutines(t) + defer report() + + pcOfferGathered := make(chan SessionDescription) + pcAnswerGathered := make(chan SessionDescription) + + s := SettingEngine{} + api := NewAPI(WithSettingEngine(s)) + + pcOffer, err := api.NewPeerConnection(Configuration{}) if err != nil { t.Error(err.Error()) } + + // We need to create a data channel in order to trigger ICE + if _, err = pcOffer.CreateDataChannel("initial_data_channel", nil); err != nil { + t.Error(err.Error()) + } + + pcOffer.OnICECandidate(func(i *ICECandidate) { + if i == nil { + close(pcOfferGathered) + } + }) + + offer, err := pcOffer.CreateOffer(nil) + if err != nil { + t.Error(err.Error()) + } else if err = pcOffer.SetLocalDescription(offer); err != nil { + t.Error(err.Error()) + } + + <-pcOfferGathered + + pcAnswer, err := api.NewPeerConnection(Configuration{}) + if err != nil { + t.Error(err.Error()) + } + + pcAnswer.OnICECandidate(func(i *ICECandidate) { + if i == nil { + close(pcAnswerGathered) + } + }) + + if err = pcAnswer.SetRemoteDescription(offer); err != nil { + t.Error(err.Error()) + } + + select { + case <-pcAnswerGathered: + t.Fatal("pcAnswer started gathering with no SetLocalDescription") + // Gathering is async, not sure of a better way to catch this currently + case <-time.After(3 * time.Second): + } + + answer, err := pcAnswer.CreateAnswer(nil) + if err != nil { + t.Error(err.Error()) + } else if err = pcAnswer.SetLocalDescription(answer); err != nil { + t.Error(err.Error()) + } + <-pcAnswerGathered + assert.NoError(t, pcOffer.Close()) + assert.NoError(t, pcAnswer.Close()) } diff --git a/settingengine.go b/settingengine.go index a2a1dd19f60..1e516e9564b 100644 --- a/settingengine.go +++ b/settingengine.go @@ -6,7 +6,7 @@ import ( "errors" "time" - "github.com/pion/ice" + "github.com/pion/ice/v2" "github.com/pion/logging" "github.com/pion/transport/vnet" ) @@ -23,8 +23,9 @@ type SettingEngine struct { DataChannels bool } timeout struct { - ICEConnection *time.Duration - ICEKeepalive *time.Duration + ICEDisconnectedTimeout *time.Duration + ICEFailedTimeout *time.Duration + ICEKeepaliveInterval *time.Duration ICECandidateSelectionTimeout *time.Duration ICEHostAcceptanceMinWait *time.Duration ICESrflxAcceptanceMinWait *time.Duration @@ -33,7 +34,6 @@ type SettingEngine struct { } candidates struct { ICELite bool - ICETrickle bool ICENetworkTypes []NetworkType InterfaceFilter func(string) bool NAT1To1IPs []string @@ -63,11 +63,14 @@ func (e *SettingEngine) DetachDataChannels() { e.detach.DataChannels = true } -// SetConnectionTimeout sets the amount of silence needed on a given candidate pair -// before the ICE agent considers the pair timed out. -func (e *SettingEngine) SetConnectionTimeout(connectionTimeout, keepAlive time.Duration) { - e.timeout.ICEConnection = &connectionTimeout - e.timeout.ICEKeepalive = &keepAlive +// SetICETimeouts sets the behavior around ICE Timeouts +// * disconnectedTimeout is the duration without network activity before a Agent is considered disconnected. Default is 5 Seconds +// * failedTimeout is the duration without network activity before a Agent is considered failed after disconnected. Default is 25 Seconds +// * keepAliveInterval is how often the ICE Agent sends extra traffic if there is no activity, if media is flowing no traffic will be sent. Default is 2 seconds +func (e *SettingEngine) SetICETimeouts(disconnectedTimeout, failedTimeout, keepAliveInterval time.Duration) { + e.timeout.ICEDisconnectedTimeout = &disconnectedTimeout + e.timeout.ICEFailedTimeout = &failedTimeout + e.timeout.ICEKeepaliveInterval = &keepAliveInterval } // SetCandidateSelectionTimeout sets the max ICECandidateSelectionTimeout @@ -113,12 +116,6 @@ func (e *SettingEngine) SetLite(lite bool) { e.candidates.ICELite = lite } -// SetTrickle configures whether or not the ice agent should gather candidates -// via the trickle method or synchronously. -func (e *SettingEngine) SetTrickle(trickle bool) { - e.candidates.ICETrickle = trickle -} - // SetNetworkTypes configures what types of candidate networks are supported // during local and server reflexive gathering. func (e *SettingEngine) SetNetworkTypes(candidateTypes []NetworkType) { diff --git a/settingengine_test.go b/settingengine_test.go index ee595889ba7..c9d3ae0a71d 100644 --- a/settingengine_test.go +++ b/settingengine_test.go @@ -35,19 +35,15 @@ func TestSetEphemeralUDPPortRange(t *testing.T) { func TestSetConnectionTimeout(t *testing.T) { s := SettingEngine{} - if s.timeout.ICEConnection != nil || - s.timeout.ICEKeepalive != nil { - t.Fatalf("SettingEngine defaults aren't as expected.") - } - - s.SetConnectionTimeout(5*time.Second, 1*time.Second) - - if s.timeout.ICEConnection == nil || - *s.timeout.ICEConnection != 5*time.Second || - s.timeout.ICEKeepalive == nil || - *s.timeout.ICEKeepalive != 1*time.Second { - t.Fatalf("ICE Timeouts do not reflect requested values.") - } + var nilDuration *time.Duration + assert.Equal(t, s.timeout.ICEDisconnectedTimeout, nilDuration) + assert.Equal(t, s.timeout.ICEFailedTimeout, nilDuration) + assert.Equal(t, s.timeout.ICEKeepaliveInterval, nilDuration) + + s.SetICETimeouts(1*time.Second, 2*time.Second, 3*time.Second) + assert.Equal(t, *s.timeout.ICEDisconnectedTimeout, 1*time.Second) + assert.Equal(t, *s.timeout.ICEFailedTimeout, 2*time.Second) + assert.Equal(t, *s.timeout.ICEKeepaliveInterval, 3*time.Second) } func TestDetachDataChannels(t *testing.T) { diff --git a/stats.go b/stats.go index 246a5429c97..a082e62599d 100644 --- a/stats.go +++ b/stats.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "github.com/pion/ice" + "github.com/pion/ice/v2" ) // A Stats object contains a set of statistics copies out of a monitored component