ORTC: Add basic data channel constructors

Resolves #273
This commit is contained in:
backkem
2018-12-28 11:05:08 +01:00
committed by Sean DuBois
parent 69571ee135
commit e203a0537c
29 changed files with 910 additions and 50 deletions

View File

@@ -69,7 +69,8 @@ func main() {
fmt.Println(util.Encode(offer))
// Wait for the answer to be pasted
answer := util.Decode(util.MustReadStdin())
answer := webrtc.RTCSessionDescription{}
util.Decode(util.MustReadStdin(), &answer)
// Apply the answer as the remote description
err = peerConnection.SetRemoteDescription(answer)

View File

@@ -66,7 +66,8 @@ func main() {
fmt.Println(util.Encode(offer))
// Wait for the answer to be pasted
answer := util.Decode(util.MustReadStdin())
answer := webrtc.RTCSessionDescription{}
util.Decode(util.MustReadStdin(), answer)
// Apply the answer as the remote description
err = peerConnection.SetRemoteDescription(answer)

View File

@@ -60,7 +60,8 @@ func main() {
})
// Wait for the offer to be pasted
offer := util.Decode(util.MustReadStdin())
offer := webrtc.RTCSessionDescription{}
util.Decode(util.MustReadStdin(), offer)
// Set the remote SessionDescription
err = peerConnection.SetRemoteDescription(offer)

View File

@@ -63,7 +63,8 @@ func main() {
})
// Wait for the offer to be pasted
offer := util.Decode(util.MustReadStdin())
offer := webrtc.RTCSessionDescription{}
util.Decode(util.MustReadStdin(), &offer)
// Set the remote SessionDescription
err = peerConnection.SetRemoteDescription(offer)

View File

@@ -6,7 +6,7 @@ import (
"github.com/pions/webrtc"
"github.com/pions/webrtc/examples/util"
"github.com/pions/webrtc/examples/util/gstreamer-sink"
gst "github.com/pions/webrtc/examples/util/gstreamer-sink"
"github.com/pions/webrtc/pkg/ice"
"github.com/pions/webrtc/pkg/rtcp"
)
@@ -63,7 +63,8 @@ func main() {
})
// Wait for the offer to be pasted
offer := util.Decode(util.MustReadStdin())
offer := webrtc.RTCSessionDescription{}
util.Decode(util.MustReadStdin(), &offer)
// Set the remote SessionDescription
err = peerConnection.SetRemoteDescription(offer)

View File

@@ -5,7 +5,7 @@ import (
"github.com/pions/webrtc"
"github.com/pions/webrtc/examples/util"
"github.com/pions/webrtc/examples/util/gstreamer-src"
gst "github.com/pions/webrtc/examples/util/gstreamer-src"
"github.com/pions/webrtc/pkg/ice"
)
@@ -55,7 +55,8 @@ func main() {
fmt.Println(util.Encode(offer))
// Wait for the answer to be pasted
answer := util.Decode(util.MustReadStdin())
answer := webrtc.RTCSessionDescription{}
util.Decode(util.MustReadStdin(), &answer)
// Set the remote SessionDescription
err = peerConnection.SetRemoteDescription(answer)

View File

@@ -6,7 +6,7 @@ import (
"github.com/pions/webrtc"
"github.com/pions/webrtc/examples/util"
"github.com/pions/webrtc/examples/util/gstreamer-src"
gst "github.com/pions/webrtc/examples/util/gstreamer-src"
"github.com/pions/webrtc/pkg/ice"
)
@@ -53,7 +53,8 @@ func main() {
util.Check(err)
// Wait for the offer to be pasted
offer := util.Decode(util.MustReadStdin())
offer := webrtc.RTCSessionDescription{}
util.Decode(util.MustReadStdin(), &offer)
// Set the remote SessionDescription
err = peerConnection.SetRemoteDescription(offer)

149
examples/ortc/main.go Normal file
View File

@@ -0,0 +1,149 @@
package main
import (
"flag"
"fmt"
"time"
"github.com/pions/webrtc"
"github.com/pions/webrtc/examples/util"
"github.com/pions/webrtc/pkg/datachannel"
)
func main() {
isOffer := flag.Bool("offer", false, "Act as the offerer if set")
flag.Parse()
// Everything below is the pion-WebRTC (ORTC) API! Thanks for using it ❤️.
// Prepare ICE gathering options
iceOptions := webrtc.RTCIceGatherOptions{
ICEServers: []webrtc.RTCIceServer{
{URLs: []string{"stun:stun.l.google.com:19302"}},
},
}
// Create the ICE gatherer
gatherer, err := webrtc.NewRTCIceGatherer(iceOptions)
util.Check(err)
// Construct the ICE transport
ice := webrtc.NewRTCIceTransport(gatherer)
// Construct the DTLS transport
dtls, err := webrtc.NewRTCDtlsTransport(ice, nil)
util.Check(err)
// Construct the SCTP transport
sctp := webrtc.NewRTCSctpTransport(dtls)
// Handle incoming data channels
sctp.OnDataChannel(func(channel *webrtc.RTCDataChannel) {
fmt.Printf("New DataChannel %s %d\n", channel.Label, channel.ID)
// Register the handlers
channel.OnOpen(handleOnOpen(channel))
channel.OnMessage(handleMessage(channel))
})
// Gather candidates
err = gatherer.Gather()
util.Check(err)
iceCandidates, err := gatherer.GetLocalCandidates()
util.Check(err)
iceParams, err := gatherer.GetLocalParameters()
util.Check(err)
dtlsParams := dtls.GetLocalParameters()
sctpCapabilities := sctp.GetCapabilities()
signal := Signal{
ICECandidates: iceCandidates,
ICEParameters: iceParams,
DtlsParameters: dtlsParams,
SCTPCapabilities: sctpCapabilities,
}
// Exchange the information
fmt.Println(util.Encode(signal))
remoteSignal := Signal{}
util.Decode(util.MustReadStdin(), &remoteSignal)
iceRole := webrtc.RTCIceRoleControlled
if *isOffer {
iceRole = webrtc.RTCIceRoleControlling
}
err = ice.SetRemoteCandidates(remoteSignal.ICECandidates)
util.Check(err)
// Start the ICE transport
err = ice.Start(nil, remoteSignal.ICEParameters, &iceRole)
util.Check(err)
// Start the DTLS transport
err = dtls.Start(remoteSignal.DtlsParameters)
util.Check(err)
// Start the SCTP transport
err = sctp.Start(remoteSignal.SCTPCapabilities)
util.Check(err)
// Construct the data channel as the offerer
if *isOffer {
dcParams := &webrtc.RTCDataChannelParameters{
Label: "Foo",
ID: 1,
}
var channel *webrtc.RTCDataChannel
channel, err = webrtc.NewRTCDataChannel(sctp, dcParams)
util.Check(err)
// Register the handlers
// channel.OnOpen(handleOnOpen(channel)) // TODO: OnOpen on handle ChannelAck
go handleOnOpen(channel)() // Temporary alternative
channel.OnMessage(handleMessage(channel))
}
select {}
}
// Signal is used to exchange signaling info.
// This is not part of the ORTC spec. You are free
// to exchange this information any way you want.
type Signal struct {
ICECandidates []webrtc.RTCIceCandidate `json:"iceCandidates"`
ICEParameters webrtc.RTCIceParameters `json:"iceParameters"`
DtlsParameters webrtc.RTCDtlsParameters `json:"dtlsParameters"`
SCTPCapabilities webrtc.RTCSctpCapabilities `json:"sctpCapabilities"`
}
func handleOnOpen(channel *webrtc.RTCDataChannel) func() {
return func() {
fmt.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds\n", channel.Label, channel.ID)
for range time.NewTicker(5 * time.Second).C {
message := util.RandSeq(15)
fmt.Printf("Sending %s \n", message)
err := channel.Send(datachannel.PayloadString{Data: []byte(message)})
util.Check(err)
}
}
}
func handleMessage(channel *webrtc.RTCDataChannel) func(datachannel.Payload) {
return func(payload datachannel.Payload) {
switch p := payload.(type) {
case *datachannel.PayloadString:
fmt.Printf("Message '%s' from DataChannel '%s' payload '%s'\n", p.PayloadType().String(), channel.Label, string(p.Data))
case *datachannel.PayloadBinary:
fmt.Printf("Message '%s' from DataChannel '%s' payload '% 02x'\n", p.PayloadType().String(), channel.Label, p.Data)
default:
fmt.Printf("Message '%s' from DataChannel '%s' no payload \n", p.PayloadType().String(), channel.Label)
}
}
}

View File

@@ -66,7 +66,8 @@ func main() {
})
// Wait for the offer to be pasted
offer := util.Decode(util.MustReadStdin())
offer := webrtc.RTCSessionDescription{}
util.Decode(util.MustReadStdin(), &offer)
// Set the remote SessionDescription
err = peerConnection.SetRemoteDescription(offer)

View File

@@ -48,7 +48,9 @@ const (
func main() {
reader := bufio.NewReader(os.Stdin)
offer := util.Decode(mustReadStdin(reader))
offer := webrtc.RTCSessionDescription{}
util.Decode(mustReadStdin(reader), &offer)
fmt.Println("")
/* Everything below is the pion-WebRTC API, thanks for using it! */
@@ -103,7 +105,9 @@ func main() {
for {
fmt.Println("")
fmt.Println("Paste an SDP to start sendonly peer connection")
recvOnlyOffer := util.Decode(mustReadStdin(reader))
recvOnlyOffer := webrtc.RTCSessionDescription{}
util.Decode(mustReadStdin(reader), &recvOnlyOffer)
// Create a new RTCPeerConnection
peerConnection, err := webrtc.New(peerConnectionConfig)

View File

@@ -13,8 +13,6 @@ import (
"os"
"strings"
"time"
"github.com/pions/webrtc"
)
// Allows compressing offer/answer to bypass terminal input limits.
@@ -52,8 +50,8 @@ func MustReadStdin() string {
// Encode encodes the input in base64
// It can optionally zip the input before encoding
func Encode(sdp webrtc.RTCSessionDescription) string {
b, err := json.Marshal(sdp)
func Encode(obj interface{}) string {
b, err := json.Marshal(obj)
Check(err)
if compress {
@@ -65,7 +63,7 @@ func Encode(sdp webrtc.RTCSessionDescription) string {
// Decode decodes the input from base64
// It can optionally unzip the input after decoding
func Decode(in string) webrtc.RTCSessionDescription {
func Decode(in string, obj interface{}) {
b, err := base64.StdEncoding.DecodeString(in)
Check(err)
@@ -73,11 +71,8 @@ func Decode(in string) webrtc.RTCSessionDescription {
b = unzip(b)
}
var sdp webrtc.RTCSessionDescription
err = json.Unmarshal(b, &sdp)
err = json.Unmarshal(b, obj)
Check(err)
return sdp
}
// RandSeq generates a random string to serve as dummy data

View File

@@ -102,6 +102,45 @@ type RTCDataChannel struct {
dataChannel *datachannel.DataChannel
}
// NewRTCDataChannel creates a new RTCDataChannel.
// This constructor is part of the ORTC API. It is not
// meant to be used together with the basic WebRTC API.
func NewRTCDataChannel(transport *RTCSctpTransport, params *RTCDataChannelParameters) (*RTCDataChannel, error) {
c := &RTCDataChannel{
Transport: transport,
Label: params.Label,
ID: &params.ID,
}
if err := c.ensureSCTP(); err != nil {
return nil, err
}
cfg := &datachannel.Config{
ChannelType: datachannel.ChannelTypeReliable, // TODO: Wiring
Priority: datachannel.ChannelPriorityNormal, // TODO: Wiring
ReliabilityParameter: 0, // TODO: Wiring
Label: c.Label,
}
dc, err := datachannel.Dial(c.Transport.association, *c.ID, cfg)
if err != nil {
return nil, err
}
c.handleOpen(dc)
return c, nil
}
func (d *RTCDataChannel) ensureSCTP() error {
if d.Transport == nil ||
d.Transport.association == nil {
return errors.New("SCTP not establisched")
}
return nil
}
// OnOpen sets an event handler which is invoked when
// the underlying data transport has been established (or re-established).
func (d *RTCDataChannel) OnOpen(f func()) {

View File

@@ -17,16 +17,16 @@ func TestGenerateDataChannelID(t *testing.T) {
c *RTCPeerConnection
result uint16
}{
{true, &RTCPeerConnection{sctpTransport: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{}}, 0},
{true, &RTCPeerConnection{sctpTransport: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{1: nil}}, 0},
{true, &RTCPeerConnection{sctpTransport: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{0: nil}}, 2},
{true, &RTCPeerConnection{sctpTransport: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{0: nil, 2: nil}}, 4},
{true, &RTCPeerConnection{sctpTransport: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{0: nil, 4: nil}}, 2},
{false, &RTCPeerConnection{sctpTransport: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{}}, 1},
{false, &RTCPeerConnection{sctpTransport: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{0: nil}}, 1},
{false, &RTCPeerConnection{sctpTransport: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{1: nil}}, 3},
{false, &RTCPeerConnection{sctpTransport: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{1: nil, 3: nil}}, 5},
{false, &RTCPeerConnection{sctpTransport: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{1: nil, 5: nil}}, 3},
{true, &RTCPeerConnection{sctpTransport: NewRTCSctpTransport(nil), dataChannels: map[uint16]*RTCDataChannel{}}, 0},
{true, &RTCPeerConnection{sctpTransport: NewRTCSctpTransport(nil), dataChannels: map[uint16]*RTCDataChannel{1: nil}}, 0},
{true, &RTCPeerConnection{sctpTransport: NewRTCSctpTransport(nil), dataChannels: map[uint16]*RTCDataChannel{0: nil}}, 2},
{true, &RTCPeerConnection{sctpTransport: NewRTCSctpTransport(nil), dataChannels: map[uint16]*RTCDataChannel{0: nil, 2: nil}}, 4},
{true, &RTCPeerConnection{sctpTransport: NewRTCSctpTransport(nil), dataChannels: map[uint16]*RTCDataChannel{0: nil, 4: nil}}, 2},
{false, &RTCPeerConnection{sctpTransport: NewRTCSctpTransport(nil), dataChannels: map[uint16]*RTCDataChannel{}}, 1},
{false, &RTCPeerConnection{sctpTransport: NewRTCSctpTransport(nil), dataChannels: map[uint16]*RTCDataChannel{0: nil}}, 1},
{false, &RTCPeerConnection{sctpTransport: NewRTCSctpTransport(nil), dataChannels: map[uint16]*RTCDataChannel{1: nil}}, 3},
{false, &RTCPeerConnection{sctpTransport: NewRTCSctpTransport(nil), dataChannels: map[uint16]*RTCDataChannel{1: nil, 3: nil}}, 5},
{false, &RTCPeerConnection{sctpTransport: NewRTCSctpTransport(nil), dataChannels: map[uint16]*RTCDataChannel{1: nil, 5: nil}}, 3},
}
for _, testCase := range testCases {

View File

@@ -0,0 +1,7 @@
package webrtc
// RTCDataChannelParameters describes the configuration of the RTCDataChannel.
type RTCDataChannelParameters struct {
Label string `json:"label"`
ID uint16 `json:"id"`
}

View File

@@ -5,10 +5,10 @@ package webrtc
type RTCDtlsFingerprint struct {
// Algorithm specifies one of the the hash function algorithms defined in
// the 'Hash function Textual Names' registry.
Algorithm string
Algorithm string `json:"algorithm"`
// Value specifies the value of the certificate fingerprint in lowercase
// hex string as expressed utilizing the syntax of 'fingerprint' in
// https://tools.ietf.org/html/rfc4572#section-5.
Value string
Value string `json:"value"`
}

7
rtcdtlsparameters.go Normal file
View File

@@ -0,0 +1,7 @@
package webrtc
// RTCDtlsParameters holds information relating to DTLS configuration.
type RTCDtlsParameters struct {
Role RTCDtlsRole `json:"role"`
Fingerprints []RTCDtlsFingerprint `json:"fingerprints"`
}

30
rtcdtlsrole.go Normal file
View File

@@ -0,0 +1,30 @@
package webrtc
// RTCDtlsRole indicates the role of the DTLS transport.
type RTCDtlsRole byte
const (
// RTCDtlsRoleAuto defines the DLTS role is determined based on
// the resolved ICE role: the ICE controlled role acts as the DTLS
// client and the ICE controlling role acts as the DTLS server.
RTCDtlsRoleAuto RTCDtlsRole = iota + 1
// RTCDtlsRoleClient defines the DTLS client role.
RTCDtlsRoleClient
// RTCDtlsRoleServer defines the DTLS server role.
RTCDtlsRoleServer
)
func (r RTCDtlsRole) String() string {
switch r {
case RTCDtlsRoleAuto:
return "auto"
case RTCDtlsRoleClient:
return "client"
case RTCDtlsRoleServer:
return "server"
default:
return "Unknown DTLS role"
}
}

View File

@@ -1,13 +1,164 @@
package webrtc
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/pions/dtls/pkg/dtls"
"github.com/pions/webrtc/pkg/rtcerr"
)
// RTCDtlsTransport allows an application access to information about the DTLS
// transport over which RTP and RTCP packets are sent and received by
// RTCRtpSender and RTCRtpReceiver, as well other data such as SCTP packets sent
// and received by data channels.
type RTCDtlsTransport struct {
// Transport RTCIceTransport
lock sync.RWMutex
iceTransport *RTCIceTransport
certificates []RTCCertificate
// State RTCDtlsTransportState
// OnStateChange func()
// OnError func()
conn *dtls.Conn
}
// NewRTCDtlsTransport creates a new RTCDtlsTransport.
// This constructor is part of the ORTC API. It is not
// meant to be used together with the basic WebRTC API.
func NewRTCDtlsTransport(transport *RTCIceTransport, certificates []RTCCertificate) (*RTCDtlsTransport, error) {
t := &RTCDtlsTransport{iceTransport: transport}
if len(certificates) > 0 {
now := time.Now()
for _, x509Cert := range certificates {
if !x509Cert.Expires().IsZero() && now.After(x509Cert.Expires()) {
return nil, &rtcerr.InvalidAccessError{Err: ErrCertificateExpired}
}
t.certificates = append(t.certificates, x509Cert)
}
} else {
sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, &rtcerr.UnknownError{Err: err}
}
certificate, err := GenerateCertificate(sk)
if err != nil {
return nil, err
}
t.certificates = []RTCCertificate{*certificate}
}
return t, nil
}
// GetLocalParameters returns the DTLS parameters of the local RTCDtlsTransport upon construction.
func (t *RTCDtlsTransport) GetLocalParameters() RTCDtlsParameters {
fingerprints := []RTCDtlsFingerprint{}
for _, c := range t.certificates {
prints := c.GetFingerprints() // TODO: Should be only one?
fingerprints = append(fingerprints, prints...)
}
return RTCDtlsParameters{
Role: RTCDtlsRoleAuto, // always returns the default role
Fingerprints: fingerprints,
}
}
// Start DTLS transport negotiation with the parameters of the remote DTLS transport
func (t *RTCDtlsTransport) Start(remoteParameters RTCDtlsParameters) error {
t.lock.Lock()
defer t.lock.Unlock()
if err := t.ensureICEConn(); err != nil {
return err
}
// TODO: Mux
dtlsEndpoint := t.iceTransport.conn
// TODO: handle multiple certs
cert := t.certificates[0]
isClient := true
switch remoteParameters.Role {
case RTCDtlsRoleClient:
isClient = true
case RTCDtlsRoleServer:
isClient = false
default:
if t.iceTransport.Role() == RTCIceRoleControlling {
isClient = false
}
}
dtlsCofig := &dtls.Config{Certificate: cert.x509Cert, PrivateKey: cert.privateKey}
if isClient {
// Assumes the peer offered to be passive and we accepted.
dtlsConn, err := dtls.Client(dtlsEndpoint, dtlsCofig)
if err != nil {
return err
}
t.conn = dtlsConn
} else {
// Assumes we offer to be passive and this is accepted.
dtlsConn, err := dtls.Server(dtlsEndpoint, dtlsCofig)
if err != nil {
return err
}
t.conn = dtlsConn
}
// Check the fingerprint if a certificate was exchanged
remoteCert := t.conn.RemoteCertificate()
if remoteCert != nil {
err := t.validateFingerPrint(remoteParameters, remoteCert)
if err != nil {
return err
}
} else {
fmt.Println("Warning: Certificate not checked")
}
return nil
}
func (t *RTCDtlsTransport) validateFingerPrint(remoteParameters RTCDtlsParameters, remoteCert *x509.Certificate) error {
for _, fp := range remoteParameters.Fingerprints {
hashAlgo, err := dtls.HashAlgorithmString(fp.Algorithm)
if err != nil {
return err
}
remoteValue, err := dtls.Fingerprint(remoteCert, hashAlgo)
if err != nil {
return err
}
if strings.ToLower(remoteValue) == strings.ToLower(fp.Value) {
return nil
}
}
return errors.New("No matching fingerprint")
}
func (t *RTCDtlsTransport) ensureICEConn() error {
if t.iceTransport == nil ||
t.iceTransport.conn == nil {
return errors.New("ICE connection not started")
}
return nil
}

86
rtcicecandidate.go Normal file
View File

@@ -0,0 +1,86 @@
package webrtc
import (
"errors"
"fmt"
"net"
"github.com/pions/webrtc/pkg/ice"
)
// RTCIceCandidate represents a ice candidate
type RTCIceCandidate struct {
Foundation string `json:"foundation"`
Priority uint32 `json:"priority"`
IP string `json:"ip"`
Protocol RTCIceProtocol `json:"protocol"`
Port uint16 `json:"port"`
Typ RTCIceCandidateType `json:"type"`
RelatedAddress string `json:"relatedAddress"`
RelatedPort uint16 `json:"relatedPort"`
}
func newRTCIceCandidatesFromICE(iceCandidates []*ice.Candidate) ([]RTCIceCandidate, error) {
candidates := []RTCIceCandidate{}
for _, i := range iceCandidates {
c, err := newRTCIceCandidateFromICE(i)
if err != nil {
return nil, err
}
candidates = append(candidates, c)
}
return candidates, nil
}
func newRTCIceCandidateFromICE(i *ice.Candidate) (RTCIceCandidate, error) {
typ, err := convertTypeFromICE(i.Type)
if err != nil {
return RTCIceCandidate{}, err
}
return RTCIceCandidate{
Foundation: "foundation",
Priority: uint32(i.Priority(i.Type.Preference(), uint16(1))), // TODO: component support
IP: i.IP.String(),
Protocol: newRTCIceProtocol(i.NetworkType.NetworkShort()),
Port: uint16(i.Port), // TODO store differently in ICE package
Typ: typ,
RelatedAddress: "", // TODO
RelatedPort: 0, // TODO (parse & store port correctly in ICE package)
}, nil
}
func (c RTCIceCandidate) toICE() (*ice.Candidate, error) {
ip := net.ParseIP(c.IP)
if ip == nil {
return nil, errors.New("Failed to parse IP address")
}
switch c.Typ {
case RTCIceCandidateTypeHost:
return ice.NewCandidateHost(c.Protocol.String(), ip, int(c.Port))
case RTCIceCandidateTypeSrflx:
return ice.NewCandidateServerReflexive(c.Protocol.String(), ip, int(c.Port),
c.RelatedAddress, int(c.RelatedPort))
default:
return nil, fmt.Errorf("Unknown candidate type: %s", c.Typ)
}
}
func convertTypeFromICE(t ice.CandidateType) (RTCIceCandidateType, error) {
switch t {
case ice.CandidateTypeHost:
return RTCIceCandidateTypeHost, nil
case ice.CandidateTypeServerReflexive:
return RTCIceCandidateTypeSrflx, nil
// case ice.CandidateTypePeerReflexive:
// return RTCIceCandidateTypePrflx, nil
// case ice.CandidateTypeRelay:
// return RTCIceCandidateTypeRelay, nil
default:
return RTCIceCandidateType(t), fmt.Errorf("Unknown ICE candidate type: %s", t)
}
}

105
rtcicegatherer.go Normal file
View File

@@ -0,0 +1,105 @@
package webrtc
import (
"errors"
"sync"
"github.com/pions/webrtc/pkg/ice"
)
// The RTCIceGatherer gathers local host, server reflexive and relay
// candidates, as well as enabling the retrieval of local Interactive
// Connectivity Establishment (ICE) parameters which can be
// exchanged in signaling.
type RTCIceGatherer struct {
lock sync.RWMutex
state RTCIceGathererState
validatedServers []*ice.URL
agent *ice.Agent
}
// NewRTCIceGatherer creates a new NewRTCIceGatherer.
// This constructor is part of the ORTC API. It is not
// meant to be used together with the basic WebRTC API.
func NewRTCIceGatherer(opts RTCIceGatherOptions) (*RTCIceGatherer, error) {
validatedServers := []*ice.URL{}
if len(opts.ICEServers) > 0 {
for _, server := range opts.ICEServers {
url, err := server.validate()
if err != nil {
return nil, err
}
validatedServers = append(validatedServers, url...)
}
}
return &RTCIceGatherer{
state: RTCIceGathererStateNew,
validatedServers: validatedServers,
}, nil
}
// State indicates the current state of the ICE gatherer.
func (g *RTCIceGatherer) State() RTCIceGathererState {
g.lock.RLock()
defer g.lock.RUnlock()
return g.state
}
// Gather ICE candidates.
func (g *RTCIceGatherer) Gather() error {
g.lock.Lock()
defer g.lock.Unlock()
config := &ice.AgentConfig{
Urls: g.validatedServers,
Notifier: nil, // TODO
PortMin: defaultSettingEngine.EphemeralUDP.PortMin,
PortMax: defaultSettingEngine.EphemeralUDP.PortMax,
}
agent, err := ice.NewAgent(config)
if err != nil {
return err
}
g.agent = agent
g.state = RTCIceGathererStateComplete
return nil
}
// GetLocalParameters returns the ICE parameters of the RTCIceGatherer.
func (g *RTCIceGatherer) GetLocalParameters() (RTCIceParameters, error) {
g.lock.RLock()
defer g.lock.RUnlock()
if g.agent == nil {
return RTCIceParameters{}, errors.New("Gatherer not started")
}
frag, pwd := g.agent.GetLocalUserCredentials()
return RTCIceParameters{
UsernameFragment: frag,
Password: pwd,
IceLite: false,
}, nil
}
// GetLocalCandidates returns the sequence of valid local candidates associated with the RTCIceGatherer.
func (g *RTCIceGatherer) GetLocalCandidates() ([]RTCIceCandidate, error) {
g.lock.RLock()
defer g.lock.RUnlock()
if g.agent == nil {
return nil, errors.New("Gatherer not started")
}
iceCandidates, err := g.agent.GetLocalCandidates()
if err != nil {
return nil, err
}
return newRTCIceCandidatesFromICE(iceCandidates)
}

36
rtcicegathererstate.go Normal file
View File

@@ -0,0 +1,36 @@
package webrtc
// RTCIceGathererState represents the current state of the ICE gatherer.
type RTCIceGathererState byte
const (
// RTCIceGathererStateNew indicates object has been created but
// gather() has not been called.
RTCIceGathererStateNew RTCIceGathererState = iota + 1
// RTCIceGathererStateGathering indicates gather() has been called,
// and the RTCIceGatherer is in the process of gathering candidates.
RTCIceGathererStateGathering
// RTCIceGathererStateComplete indicates the RTCIceGatherer has completed gathering.
RTCIceGathererStateComplete
// RTCIceGathererStateClosed indicates the closed state can only be entered
// when the RTCIceGatherer has been closed intentionally by calling close().
RTCIceGathererStateClosed
)
func (s RTCIceGathererState) String() string {
switch s {
case RTCIceGathererStateNew:
return "new"
case RTCIceGathererStateGathering:
return "gathering"
case RTCIceGathererStateComplete:
return "complete"
case RTCIceGathererStateClosed:
return "closed"
default:
return "Unknown state"
}
}

6
rtcicegatheroptions.go Normal file
View File

@@ -0,0 +1,6 @@
package webrtc
// RTCIceGatherOptions provides options relating to the gathering of ICE candidates.
type RTCIceGatherOptions struct {
ICEServers []RTCIceServer
}

9
rtciceparameters.go Normal file
View File

@@ -0,0 +1,9 @@
package webrtc
// RTCIceParameters includes the ICE username fragment
// and password and other ICE-related parameters.
type RTCIceParameters struct {
UsernameFragment string `json:"usernameFragment"`
Password string `json:"password"`
IceLite bool `json:"iceLite"`
}

View File

@@ -18,35 +18,41 @@ func (s RTCIceServer) parseURL(i int) (*ice.URL, error) {
return ice.ParseURL(s.URLs[i])
}
func (s RTCIceServer) validate() error {
func (s RTCIceServer) validate() ([]*ice.URL, error) {
urls := []*ice.URL{}
for i := range s.URLs {
url, err := s.parseURL(i)
if err != nil {
return err
return nil, err
}
if url.Scheme == ice.SchemeTypeTURN || url.Scheme == ice.SchemeTypeTURNS {
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3.2)
if s.Username == "" || s.Credential == nil {
return &rtcerr.InvalidAccessError{Err: ErrNoTurnCredencials}
return nil, &rtcerr.InvalidAccessError{Err: ErrNoTurnCredencials}
}
switch s.CredentialType {
case RTCIceCredentialTypePassword:
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3.3)
if _, ok := s.Credential.(string); !ok {
return &rtcerr.InvalidAccessError{Err: ErrTurnCredencials}
return nil, &rtcerr.InvalidAccessError{Err: ErrTurnCredencials}
}
case RTCIceCredentialTypeOauth:
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3.4)
if _, ok := s.Credential.(RTCOAuthCredential); !ok {
return &rtcerr.InvalidAccessError{Err: ErrTurnCredencials}
return nil, &rtcerr.InvalidAccessError{Err: ErrTurnCredencials}
}
default:
return &rtcerr.InvalidAccessError{Err: ErrTurnCredencials}
return nil, &rtcerr.InvalidAccessError{Err: ErrTurnCredencials}
}
}
urls = append(urls, url)
}
return nil
return urls, nil
}

View File

@@ -32,7 +32,8 @@ func TestRTCIceServer_validate(t *testing.T) {
}
for i, testCase := range testCases {
assert.Nil(t, testCase.iceServer.validate(), "testCase: %d %v", i, testCase)
_, err := testCase.iceServer.validate()
assert.Nil(t, err, "testCase: %d %v", i, testCase)
}
})
t.Run("Failure", func(t *testing.T) {
@@ -70,8 +71,9 @@ func TestRTCIceServer_validate(t *testing.T) {
}
for i, testCase := range testCases {
_, err := testCase.iceServer.validate()
assert.EqualError(t,
testCase.iceServer.validate(),
err,
testCase.expectedErr.Error(),
"testCase: %d %v", i, testCase,
)

View File

@@ -1,12 +1,25 @@
package webrtc
import (
"context"
"errors"
"sync"
"github.com/pions/webrtc/pkg/ice"
)
// RTCIceTransport allows an application access to information about the ICE
// transport over which packets are sent and received.
type RTCIceTransport struct {
// Role RTCIceRole
lock sync.RWMutex
role RTCIceRole
// Component RTCIceComponent
// State RTCIceTransportState
// gatheringState RTCIceGathererState
gatherer *RTCIceGatherer
conn *ice.Conn
}
// func (t *RTCIceTransport) GetLocalCandidates() []RTCIceCandidate {
@@ -28,3 +41,95 @@ type RTCIceTransport struct {
// func (t *RTCIceTransport) GetRemoteParameters() RTCIceParameters {
//
// }
// NewRTCIceTransport creates a new NewRTCIceTransport.
// This constructor is part of the ORTC API. It is not
// meant to be used together with the basic WebRTC API.
func NewRTCIceTransport(gatherer *RTCIceGatherer) *RTCIceTransport {
return &RTCIceTransport{gatherer: gatherer}
}
// Start incoming connectivity checks based on its configured role.
func (t *RTCIceTransport) Start(gatherer *RTCIceGatherer, params RTCIceParameters, role *RTCIceRole) error {
t.lock.Lock()
defer t.lock.Unlock()
if gatherer != nil {
t.gatherer = gatherer
}
if err := t.ensureGatherer(); err != nil {
return err
}
if role == nil {
controlled := RTCIceRoleControlled
role = &controlled
}
t.role = *role
switch t.role {
case RTCIceRoleControlling:
iceConn, err := t.gatherer.agent.Dial(context.TODO(),
params.UsernameFragment,
params.Password)
if err != nil {
return err
}
t.conn = iceConn
case RTCIceRoleControlled:
iceConn, err := t.gatherer.agent.Accept(context.TODO(),
params.UsernameFragment,
params.Password)
if err != nil {
return err
}
t.conn = iceConn
default:
return errors.New("Unknown ICE Role")
}
return nil
}
// Role indicates the current role of the ICE transport.
func (t *RTCIceTransport) Role() RTCIceRole {
t.lock.RLock()
defer t.lock.RUnlock()
return t.role
}
// SetRemoteCandidates sets the sequence of candidates associated with the remote RTCIceTransport.
func (t *RTCIceTransport) SetRemoteCandidates(remoteCandidates []RTCIceCandidate) error {
t.lock.RLock()
defer t.lock.RUnlock()
if err := t.ensureGatherer(); err != nil {
return err
}
for _, c := range remoteCandidates {
i, err := c.toICE()
if err != nil {
return err
}
err = t.gatherer.agent.AddRemoteCandidate(i)
if err != nil {
return err
}
}
return nil
}
func (t *RTCIceTransport) ensureGatherer() error {
if t.gatherer == nil ||
t.gatherer.agent == nil {
return errors.New("Gatherer not started")
}
return nil
}

View File

@@ -138,7 +138,7 @@ func New(configuration RTCConfiguration) (*RTCPeerConnection, error) {
IceGatheringState: RTCIceGatheringStateNew,
ConnectionState: RTCPeerConnectionStateNew,
mediaEngine: DefaultMediaEngine,
sctpTransport: newRTCSctpTransport(),
sctpTransport: NewRTCSctpTransport(nil),
dataChannels: make(map[uint16]*RTCDataChannel),
}
@@ -222,7 +222,7 @@ func (pc *RTCPeerConnection) initConfiguration(configuration RTCConfiguration) e
if len(configuration.IceServers) > 0 {
for _, server := range configuration.IceServers {
if err := server.validate(); err != nil {
if _, err := server.validate(); err != nil {
return err
}
}
@@ -408,7 +408,7 @@ func (pc *RTCPeerConnection) SetConfiguration(configuration RTCConfiguration) er
if len(configuration.IceServers) > 0 {
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3)
for _, server := range configuration.IceServers {
if err := server.validate(); err != nil {
if _, err := server.validate(); err != nil {
return err
}
}

6
rtcsctpcapabilities.go Normal file
View File

@@ -0,0 +1,6 @@
package webrtc
// RTCSctpCapabilities indicates the capabilities of the RTCSctpTransport.
type RTCSctpCapabilities struct {
MaxMessageSize uint32 `json:"maxMessageSize"`
}

View File

@@ -1,11 +1,19 @@
package webrtc
import (
"errors"
"fmt"
"math"
"sync"
"github.com/pions/datachannel"
"github.com/pions/sctp"
)
// RTCSctpTransport provides details about the SCTP transport.
type RTCSctpTransport struct {
lock sync.RWMutex
// Transport represents the transport over which all SCTP packets for data
// channels will be sent and received.
Transport *RTCDtlsTransport
@@ -13,6 +21,8 @@ type RTCSctpTransport struct {
// State represents the current state of the SCTP transport.
State RTCSctpTransportState
port uint16
// MaxMessageSize represents the maximum size of data that can be passed to
// RTCDataChannel's send() method.
MaxMessageSize float64
@@ -25,11 +35,19 @@ type RTCSctpTransport struct {
// dataChannels
// dataChannels map[uint16]*RTCDataChannel
association *sctp.Association
onDataChannelHandler func(*RTCDataChannel)
}
func newRTCSctpTransport() *RTCSctpTransport {
// NewRTCSctpTransport creates a new RTCSctpTransport.
// This constructor is part of the ORTC API. It is not
// meant to be used together with the basic WebRTC API.
func NewRTCSctpTransport(transport *RTCDtlsTransport) *RTCSctpTransport {
res := &RTCSctpTransport{
Transport: transport,
State: RTCSctpTransportStateConnecting,
port: 5000, // TODO
}
res.updateMessageSize()
@@ -38,6 +56,97 @@ func newRTCSctpTransport() *RTCSctpTransport {
return res
}
// GetCapabilities returns the RTCSctpCapabilities of the RTCSctpTransport.
func (r *RTCSctpTransport) GetCapabilities() RTCSctpCapabilities {
return RTCSctpCapabilities{
MaxMessageSize: 0,
}
}
// Start the RTCSctpTransport. Since both local and remote parties must mutually
// create an RTCSctpTransport, SCTP SO (Simultaneous Open) is used to establish
// a connection over SCTP.
func (r *RTCSctpTransport) Start(remoteCaps RTCSctpCapabilities) error {
r.lock.Lock()
defer r.lock.Unlock()
// TODO: port
_ = r.MaxMessageSize // TODO
if err := r.ensureDTLS(); err != nil {
return err
}
sctpAssociation, err := sctp.Client(r.Transport.conn)
if err != nil {
return err
}
r.association = sctpAssociation
go r.acceptDataChannels()
return nil
}
func (r *RTCSctpTransport) ensureDTLS() error {
if r.Transport == nil ||
r.Transport.conn == nil {
return errors.New("DTLS not establisched")
}
return nil
}
func (r *RTCSctpTransport) acceptDataChannels() {
for {
dc, err := datachannel.Accept(r.association)
if err != nil {
fmt.Println("Failed to accept data channel:", err)
// TODO: Kill DataChannel/PeerConnection?
return
}
sid := dc.StreamIdentifier()
rtcDC := &RTCDataChannel{
ID: &sid,
Label: dc.Config.Label,
ReadyState: RTCDataChannelStateOpen,
}
<-r.onDataChannel(rtcDC)
rtcDC.handleOpen(dc)
}
}
// OnDataChannel sets an event handler which is invoked when a data
// channel message arrives from a remote peer.
func (r *RTCSctpTransport) OnDataChannel(f func(*RTCDataChannel)) {
r.lock.Lock()
defer r.lock.Unlock()
r.onDataChannelHandler = f
}
func (r *RTCSctpTransport) onDataChannel(dc *RTCDataChannel) (done chan struct{}) {
r.lock.Lock()
hdlr := r.onDataChannelHandler
r.lock.Unlock()
done = make(chan struct{})
if hdlr == nil || dc == nil {
close(done)
return
}
// Run this synchronously to allow setup done in onDataChannelFn()
// to complete before datachannel event handlers might be called.
go func() {
hdlr(dc)
close(done)
}()
return
}
func (r *RTCSctpTransport) updateMessageSize() {
var remoteMaxMessageSize float64 = 65536 // TODO: get from SDP
var canSendSize float64 = 65536 // TODO: Get from SCTP implementation