mirror of
https://github.com/pion/webrtc.git
synced 2025-10-05 23:26:58 +08:00
Add experimental support for ICE TCP
See pion/ice issue and PR: - https://github.com/pion/ice/tree/issue-196 - https://github.com/pion/ice/pull/226
This commit is contained in:
22
examples/ice-tcp/README.md
Normal file
22
examples/ice-tcp/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# ice-tcp
|
||||
ice-tcp demonstrates Pion WebRTC's ICE TCP abilities.
|
||||
|
||||
## Instructions
|
||||
|
||||
### Download ice-tcp
|
||||
This example requires you to clone the repo since it is serving static HTML.
|
||||
|
||||
```
|
||||
mkdir -p $GOPATH/src/github.com/pion
|
||||
cd $GOPATH/src/github.com/pion
|
||||
git clone https://github.com/pion/webrtc.git
|
||||
cd webrtc/examples/ice-tcp
|
||||
```
|
||||
|
||||
### Run ice-tcp
|
||||
Execute `go run *.go`
|
||||
|
||||
### Open the Web UI
|
||||
Open [http://localhost:8080](http://localhost:8080). This will automatically start a PeerConnection. This page will now prints stats about the PeerConnection. The UDP candidates will be filtered out from the SDP.
|
||||
|
||||
Congrats, you have used Pion WebRTC! Now start building something cool
|
54
examples/ice-tcp/index.html
Normal file
54
examples/ice-tcp/index.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>ice-tcp</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<button onclick="window.doSignaling(true)"> ICE TCP </button><br />
|
||||
|
||||
|
||||
<h3> ICE Connection States </h3>
|
||||
<div id="iceConnectionStates"></div> <br />
|
||||
|
||||
<h3> Inbound DataChannel Messages </h3>
|
||||
<div id="inboundDataChannelMessages"></div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
let pc = new RTCPeerConnection()
|
||||
let dc = pc.createDataChannel('data')
|
||||
|
||||
dc.onmessage = event => {
|
||||
let el = document.createElement('p')
|
||||
el.appendChild(document.createTextNode(event.data))
|
||||
|
||||
document.getElementById('inboundDataChannelMessages').appendChild(el);
|
||||
}
|
||||
|
||||
pc.oniceconnectionstatechange = () => {
|
||||
let el = document.createElement('p')
|
||||
el.appendChild(document.createTextNode(pc.iceConnectionState))
|
||||
|
||||
document.getElementById('iceConnectionStates').appendChild(el);
|
||||
}
|
||||
|
||||
pc.createOffer()
|
||||
.then(offer => {
|
||||
pc.setLocalDescription(offer)
|
||||
|
||||
return fetch(`/doSignaling`, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(offer)
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
pc.setRemoteDescription(res)
|
||||
})
|
||||
.catch(alert)
|
||||
</script>
|
||||
</html>
|
97
examples/ice-tcp/main.go
Normal file
97
examples/ice-tcp/main.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
var peerConnection *webrtc.PeerConnection //nolint
|
||||
|
||||
func doSignaling(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
|
||||
if peerConnection == nil {
|
||||
m := webrtc.MediaEngine{}
|
||||
m.RegisterDefaultCodecs()
|
||||
|
||||
settingEngine := webrtc.SettingEngine{}
|
||||
|
||||
// Enable support only for TCP ICE candidates.
|
||||
settingEngine.SetNetworkTypes([]webrtc.NetworkType{
|
||||
webrtc.NetworkTypeTCP4,
|
||||
webrtc.NetworkTypeTCP6,
|
||||
})
|
||||
settingEngine.SetICETCPPort(8443)
|
||||
|
||||
api := webrtc.NewAPI(
|
||||
webrtc.WithMediaEngine(m),
|
||||
webrtc.WithSettingEngine(settingEngine),
|
||||
)
|
||||
if peerConnection, err = api.NewPeerConnection(webrtc.Configuration{}); 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 main() {
|
||||
http.Handle("/", http.FileServer(http.Dir(".")))
|
||||
http.HandleFunc("/doSignaling", doSignaling)
|
||||
|
||||
fmt.Println("Open http://localhost:8080 to access this demo")
|
||||
panic(http.ListenAndServe(":8080", nil))
|
||||
}
|
2
go.mod
2
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/v2 v2.0.0-rc.5
|
||||
github.com/pion/ice/v2 v2.0.0-rc.6
|
||||
github.com/pion/logging v0.2.2
|
||||
github.com/pion/quic v0.1.1
|
||||
github.com/pion/randutil v0.1.0
|
||||
|
16
go.sum
16
go.sum
@@ -31,20 +31,22 @@ github.com/pion/datachannel v1.4.17 h1:8CChK5VrJoGrwKCysoTscoWvshCAFpUkgY11Tqgz5
|
||||
github.com/pion/datachannel v1.4.17/go.mod h1:+vPQfypU9vSsyPXogYj1hBThWQ6MNXEQoQAzxoPvjYM=
|
||||
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/v2 v2.0.0-rc.5 h1:HSSnTn3QEBgRgxwGj/kiI4iBgseNWpTQpb9GZZfLBcY=
|
||||
github.com/pion/ice/v2 v2.0.0-rc.5/go.mod h1:qfkp2BfgVTocUA3C9W559kFzW3IeCZxGplCIHAMyBZs=
|
||||
github.com/pion/ice/v2 v2.0.0-rc.3 h1:GvQ6nMGIGz7GltCUC9EU0m9JyQMan2vbifO4i8Y6T6A=
|
||||
github.com/pion/ice/v2 v2.0.0-rc.3/go.mod h1:5sP3yQ8Kd/azvPS4UrVTSgs/p5jfXMy3Ft2dQZBWyI8=
|
||||
github.com/pion/ice/v2 v2.0.0-rc.6 h1:Jz88W1iXzHBYJG6I5QbRTm+xuKD1vbgUW+NP1MtUPAg=
|
||||
github.com/pion/ice/v2 v2.0.0-rc.6/go.mod h1:VvpoDXwdierv9sPB8LAV3+T33ncCt0IG2NeI+CZYmTg=
|
||||
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=
|
||||
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
|
||||
github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA=
|
||||
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
|
||||
github.com/pion/randutil v0.0.0 h1:aLWLVhTG2jzoD25F0OlW6nXvXrjoGwiXq2Sz7j7NzL0=
|
||||
github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.3 h1:2wrhKnqgSz91Q5nzYTO07mQXztYPtxL8a0XOss4rJqA=
|
||||
github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
|
||||
github.com/pion/rtp v1.5.4/go.mod h1:bg60AL5GotNOlYZsqycbhDtEV3TkfbpXG0KBiUq29Mg=
|
||||
github.com/pion/rtp v1.5.5 h1:WTqWdmBuIj+luh8Wg6XVX+w7OytZHAIgtC7uSvgEl9Y=
|
||||
github.com/pion/rtp v1.5.5/go.mod h1:bg60AL5GotNOlYZsqycbhDtEV3TkfbpXG0KBiUq29Mg=
|
||||
github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk=
|
||||
@@ -53,10 +55,15 @@ github.com/pion/sctp v1.7.6 h1:8qZTdJtbKfAns/Hv5L0PAj8FyXcsKhMH1pKUCGisQg4=
|
||||
github.com/pion/sctp v1.7.6/go.mod h1:ichkYQ5tlgCQwEwvgfdcAolqx1nHbYCxo4D7zK/K0X8=
|
||||
github.com/pion/sctp v1.7.7 h1:6KVHBstRFV9+2si2B8H39CUpNn03oQ9yk/3dJ1TnkOs=
|
||||
github.com/pion/sctp v1.7.7/go.mod h1:E0K0acHLowZ2Ua21lHlQe4pHJoRzMU0HXqZVQEk061k=
|
||||
github.com/pion/sdp/v2 v2.3.9 h1:KQMzypCMOcbHnx20t2r/Kuh9rKqWBa7RVy2tZ8Zk2MA=
|
||||
github.com/pion/sdp/v2 v2.3.9/go.mod h1:sbxACjjlmwAgXMk0Qqw9uzFaazLIdPv4m0mIreLzPVk=
|
||||
github.com/pion/sdp/v2 v2.4.0 h1:luUtaETR5x2KNNpvEMv/r4Y+/kzImzbz4Lm1z8eQNQI=
|
||||
github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
|
||||
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/srtp v1.4.0 h1:Qg/RYeCOY59fpjaHgAaybj+Wdu7EBSmrqWqlb0hjrdE=
|
||||
github.com/pion/srtp v1.4.0/go.mod h1:LSHkbwXr484DujfzX9bY1PsoQbAqDO+WMKd1KBq5yW0=
|
||||
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=
|
||||
@@ -65,8 +72,11 @@ github.com/pion/transport v0.10.0 h1:9M12BSneJm6ggGhJyWpDveFOstJsTiQjkLf4M44rm80
|
||||
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
|
||||
github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM=
|
||||
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
|
||||
github.com/pion/turn/v2 v2.0.3 h1:SJUUIbcPoehlyZgMyIUbBBDhI03sBx32x3JuSIBKBWA=
|
||||
github.com/pion/turn/v2 v2.0.3/go.mod h1:kl1hmT3NxcLynpXVnwJgObL8C9NaCyPTeqI2DcCpSZs=
|
||||
github.com/pion/turn/v2 v2.0.4 h1:oDguhEv2L/4rxwbL9clGLgtzQPjtuZwCdoM7Te8vQVk=
|
||||
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
@@ -19,6 +19,7 @@ type ICECandidate struct {
|
||||
Component uint16 `json:"component"`
|
||||
RelatedAddress string `json:"relatedAddress"`
|
||||
RelatedPort uint16 `json:"relatedPort"`
|
||||
TCPType string `json:"tcpType"`
|
||||
}
|
||||
|
||||
// Conversion for package ice
|
||||
@@ -56,6 +57,7 @@ func newICECandidateFromICE(i ice.Candidate) (ICECandidate, error) {
|
||||
Port: uint16(i.Port()),
|
||||
Component: i.Component(),
|
||||
Typ: typ,
|
||||
TCPType: i.TCPType().String(),
|
||||
}
|
||||
|
||||
if i.RelatedAddress() != nil {
|
||||
@@ -76,6 +78,7 @@ func (c ICECandidate) toICE() (ice.Candidate, error) {
|
||||
Address: c.Address,
|
||||
Port: int(c.Port),
|
||||
Component: c.Component,
|
||||
TCPType: ice.NewTCPType(c.TCPType),
|
||||
}
|
||||
return ice.NewCandidateHost(&config)
|
||||
case ICECandidateTypeSrflx:
|
||||
@@ -140,16 +143,26 @@ func (c ICECandidate) String() string {
|
||||
}
|
||||
|
||||
func iceCandidateToSDP(c ICECandidate) sdp.ICECandidate {
|
||||
var extensions []sdp.ICECandidateAttribute
|
||||
|
||||
if c.Protocol == ICEProtocolTCP && c.TCPType != "" {
|
||||
extensions = append(extensions, sdp.ICECandidateAttribute{
|
||||
Key: "tcptype",
|
||||
Value: c.TCPType,
|
||||
})
|
||||
}
|
||||
|
||||
return sdp.ICECandidate{
|
||||
Foundation: c.Foundation,
|
||||
Priority: c.Priority,
|
||||
Address: c.Address,
|
||||
Protocol: c.Protocol.String(),
|
||||
Port: c.Port,
|
||||
Component: c.Component,
|
||||
Typ: c.Typ.String(),
|
||||
RelatedAddress: c.RelatedAddress,
|
||||
RelatedPort: c.RelatedPort,
|
||||
Foundation: c.Foundation,
|
||||
Priority: c.Priority,
|
||||
Address: c.Address,
|
||||
Protocol: c.Protocol.String(),
|
||||
Port: c.Port,
|
||||
Component: c.Component,
|
||||
Typ: c.Typ.String(),
|
||||
RelatedAddress: c.RelatedAddress,
|
||||
RelatedPort: c.RelatedPort,
|
||||
ExtensionAttributes: extensions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +175,17 @@ func newICECandidateFromSDP(c sdp.ICECandidate) (ICECandidate, error) {
|
||||
if err != nil {
|
||||
return ICECandidate{}, err
|
||||
}
|
||||
|
||||
var tcpType string
|
||||
if protocol == ICEProtocolTCP {
|
||||
for _, attr := range c.ExtensionAttributes {
|
||||
if attr.Key == "tcptype" {
|
||||
tcpType = attr.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ICECandidate{
|
||||
Foundation: c.Foundation,
|
||||
Priority: c.Priority,
|
||||
@@ -172,6 +196,7 @@ func newICECandidateFromSDP(c sdp.ICECandidate) (ICECandidate, error) {
|
||||
Typ: typ,
|
||||
RelatedAddress: c.RelatedAddress,
|
||||
RelatedPort: c.RelatedPort,
|
||||
TCPType: tcpType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/sdp/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestICECandidate_Convert(t *testing.T) {
|
||||
@@ -129,6 +131,47 @@ func TestICECandidate_Convert(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestICECandidate_ConvertTCP(t *testing.T) {
|
||||
candidate := ICECandidate{
|
||||
Foundation: "foundation",
|
||||
Priority: 128,
|
||||
Address: "1.0.0.1",
|
||||
Protocol: ICEProtocolTCP,
|
||||
Port: 1234,
|
||||
Typ: ICECandidateTypeHost,
|
||||
Component: 1,
|
||||
TCPType: "passive",
|
||||
}
|
||||
|
||||
got, err := candidate.toICE()
|
||||
require.NoError(t, err)
|
||||
|
||||
want, err := ice.NewCandidateHost(&ice.CandidateHostConfig{
|
||||
CandidateID: got.ID(),
|
||||
Address: "1.0.0.1",
|
||||
Component: 1,
|
||||
Network: "tcp",
|
||||
Port: 1234,
|
||||
TCPType: ice.TCPTypePassive,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, want, got)
|
||||
|
||||
sdpCandidate := iceCandidateToSDP(candidate)
|
||||
assert.Equal(t, []sdp.ICECandidateAttribute{
|
||||
{
|
||||
Key: "tcptype",
|
||||
Value: "passive",
|
||||
},
|
||||
}, sdpCandidate.ExtensionAttributes)
|
||||
|
||||
candidate2, err := newICECandidateFromSDP(sdpCandidate)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, candidate, candidate2)
|
||||
}
|
||||
|
||||
func TestConvertTypeFromICE(t *testing.T) {
|
||||
t.Run("host", func(t *testing.T) {
|
||||
ct, err := convertTypeFromICE(ice.CandidateTypeHost)
|
||||
|
@@ -109,6 +109,7 @@ func (g *ICEGatherer) createAgent() error {
|
||||
MulticastDNSHostName: g.api.settingEngine.candidates.MulticastDNSHostName,
|
||||
LocalUfrag: g.api.settingEngine.candidates.UsernameFragment,
|
||||
LocalPwd: g.api.settingEngine.candidates.Password,
|
||||
TCPListenPort: g.api.settingEngine.iceTCPPort,
|
||||
}
|
||||
|
||||
requestedNetworkTypes := g.api.settingEngine.candidates.ICENetworkTypes
|
||||
|
@@ -56,6 +56,7 @@ type SettingEngine struct {
|
||||
disableSRTCPReplayProtection bool
|
||||
vnet *vnet.Net
|
||||
LoggerFactory logging.LoggerFactory
|
||||
iceTCPPort int
|
||||
}
|
||||
|
||||
// DetachDataChannels enables detaching data channels. When enabled
|
||||
@@ -242,6 +243,12 @@ func (e *SettingEngine) SetSDPMediaLevelFingerprints(sdpMediaLevelFingerprints b
|
||||
e.sdpMediaLevelFingerprints = sdpMediaLevelFingerprints
|
||||
}
|
||||
|
||||
// SetICETCPPort to a non-zero value enables ICE-TCP listener. This API is experimental and
|
||||
// is likely to change in the future.
|
||||
func (e *SettingEngine) SetICETCPPort(port int) {
|
||||
e.iceTCPPort = port
|
||||
}
|
||||
|
||||
// AddSDPExtensions adds available and offered extensions for media type.
|
||||
//
|
||||
// Ext IDs are optional and generated if you do not provide them
|
||||
|
Reference in New Issue
Block a user