Add JavaScript/WASM bindings

Resolves #478. Adds minimal JavaScript/WASM bindings. This makes it
possible to compile core parts of pions/webrtc to WASM and run it in the
browser. Only data channels are supported for now and there is
limited/no support for certificates and credentials.
This commit is contained in:
Alex Browne
2019-03-07 15:15:15 -08:00
committed by Michiel De Backker
parent 6a68ca04a8
commit 0f1ddf0825
18 changed files with 1099 additions and 18 deletions

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ vendor/
tags
cover.out
*.sw[poe]
*.wasm

View File

@@ -89,6 +89,7 @@ Check out the **[contributing wiki](https://github.com/pions/webrtc/wiki/Contrib
* [Antoine Baché](https://github.com/Antonito) - *OGG Opus export*
* [frank](https://github.com/feixiao) - *Building examples on OSX*
* [mxmCherry](https://github.com/mxmCherry)
* [Alex Browne](https://github.com/albrow) - *JavaScript/WASM bindings*
### License
MIT License - see [LICENSE](LICENSE) for full text

10
constants.go Normal file
View File

@@ -0,0 +1,10 @@
package webrtc
const (
// Unknown defines default public constant to use for "enum" like struct
// comparisons when no value was defined.
Unknown = iota
unknownStr = "unknown"
receiveMTU = 8192
)

View File

@@ -1,3 +1,5 @@
// +build !js
package webrtc
import (
@@ -261,15 +263,6 @@ func (d *DataChannel) onClose() (done chan struct{}) {
return
}
// DataChannelMessage represents a message received from the
// data channel. IsString will be set to true if the incoming
// message is of the string type. Otherwise the message is of
// a binary type.
type DataChannelMessage struct {
IsString bool
Data []byte
}
// OnMessage sets an event handler which is invoked on a binary
// message arrival over the sctp transport from a remote peer.
// OnMessage can currently receive messages up to 16384 bytes

256
datachannel_js.go Normal file
View File

@@ -0,0 +1,256 @@
// +build js
package webrtc
import (
"syscall/js"
)
const dataChannelBufferSize = 16384 // Lowest common denominator among browsers
// DataChannel represents a WebRTC DataChannel
// The DataChannel interface represents a network channel
// which can be used for bidirectional peer-to-peer transfers of arbitrary data
type DataChannel struct {
// Pointer to the underlying JavaScript RTCPeerConnection object.
underlying js.Value
// Keep track of handlers/callbacks so we can call Release as required by the
// syscall/js API. Initially nil.
onOpenHandler *js.Func
onCloseHandler *js.Func
onMessageHandler *js.Func
}
// OnOpen sets an event handler which is invoked when
// the underlying data transport has been established (or re-established).
func (d *DataChannel) OnOpen(f func()) {
if d.onOpenHandler != nil {
oldHandler := d.onOpenHandler
defer oldHandler.Release()
}
onOpenHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go f()
return js.Undefined()
})
d.onOpenHandler = &onOpenHandler
d.underlying.Set("onopen", onOpenHandler)
}
// OnClose sets an event handler which is invoked when
// the underlying data transport has been closed.
func (d *DataChannel) OnClose(f func()) {
if d.onCloseHandler != nil {
oldHandler := d.onCloseHandler
defer oldHandler.Release()
}
onCloseHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go f()
return js.Undefined()
})
d.onCloseHandler = &onCloseHandler
d.underlying.Set("onclose", onCloseHandler)
}
// OnMessage sets an event handler which is invoked on a binary message arrival
// from a remote peer. Note that browsers may place limitations on message size.
func (d *DataChannel) OnMessage(f func(msg DataChannelMessage)) {
if d.onMessageHandler != nil {
oldHandler := d.onMessageHandler
defer oldHandler.Release()
}
onMessageHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
msg := valueToDataChannelMessage(args[0].Get("data"))
go f(msg)
return js.Undefined()
})
d.onMessageHandler = &onMessageHandler
d.underlying.Set("onmessage", onMessageHandler)
}
// Send sends the binary message to the DataChannel peer
func (d *DataChannel) Send(data []byte) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
array := js.TypedArrayOf([]uint8(data))
defer array.Release()
d.underlying.Call("send", array.Value)
return nil
}
// SendText sends the text message to the DataChannel peer
func (d *DataChannel) SendText(s string) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
d.underlying.Call("send", s)
return nil
}
// Close Closes the DataChannel. It may be called regardless of whether
// the DataChannel object was created by this peer or the remote peer.
func (d *DataChannel) Close() (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
d.underlying.Call("close")
// Release any handlers as required by the syscall/js API.
if d.onOpenHandler != nil {
d.onOpenHandler.Release()
}
if d.onCloseHandler != nil {
d.onCloseHandler.Release()
}
if d.onMessageHandler != nil {
d.onMessageHandler.Release()
}
return nil
}
// Label represents a label that can be used to distinguish this
// DataChannel object from other DataChannel objects. Scripts are
// allowed to create multiple DataChannel objects with the same label.
func (d *DataChannel) Label() string {
return d.underlying.Get("label").String()
}
// Ordered represents if the DataChannel is ordered, and false if
// out-of-order delivery is allowed.
func (d *DataChannel) Ordered() bool {
return d.underlying.Get("ordered").Bool()
}
// MaxPacketLifeTime represents the length of the time window (msec) during
// which transmissions and retransmissions may occur in unreliable mode.
func (d *DataChannel) MaxPacketLifeTime() *uint16 {
return valueToUint16Pointer(d.underlying.Get("maxPacketLifeTime"))
}
// MaxRetransmits represents the maximum number of retransmissions that are
// attempted in unreliable mode.
func (d *DataChannel) MaxRetransmits() *uint16 {
return valueToUint16Pointer(d.underlying.Get("maxRetransmits"))
}
// Protocol represents the name of the sub-protocol used with this
// DataChannel.
func (d *DataChannel) Protocol() string {
return d.underlying.Get("protocol").String()
}
// Negotiated represents whether this DataChannel was negotiated by the
// application (true), or not (false).
func (d *DataChannel) Negotiated() bool {
return d.underlying.Get("negotiated").Bool()
}
// ID represents the ID for this DataChannel. The value is initially
// null, which is what will be returned if the ID was not provided at
// channel creation time. Otherwise, it will return the ID that was either
// selected by the script or generated. After the ID is set to a non-null
// value, it will not change.
func (d *DataChannel) ID() *uint16 {
return valueToUint16Pointer(d.underlying.Get("id"))
}
// NOTE(albrow): Apparently not part of the JavaScript WebRTC API
// // Priority represents the priority for this DataChannel. The priority is
// // assigned at channel creation time.
// Priority PriorityType
// ReadyState represents the state of the DataChannel object.
func (d *DataChannel) ReadyState() DataChannelState {
return newDataChannelState(d.underlying.Get("readyState").String())
}
// BufferedAmount represents the number of bytes of application data
// (UTF-8 text and binary data) that have been queued using send(). Even
// though the data transmission can occur in parallel, the returned value
// MUST NOT be decreased before the current task yielded back to the event
// loop to prevent race conditions. The value does not include framing
// overhead incurred by the protocol, or buffering done by the operating
// system or network hardware. The value of BufferedAmount slot will only
// increase with each call to the send() method as long as the ReadyState is
// open; however, BufferedAmount does not reset to zero once the channel
// closes.
func (d *DataChannel) BufferedAmount() uint64 {
return uint64(d.underlying.Get("bufferedAmount").Int())
}
// BufferedAmountLowThreshold represents the threshold at which the
// bufferedAmount is considered to be low. When the bufferedAmount decreases
// from above this threshold to equal or below it, the bufferedamountlow
// event fires. BufferedAmountLowThreshold is initially zero on each new
// DataChannel, but the application may change its value at any time.
func (d *DataChannel) BufferedAmountLowThreshold() uint64 {
return uint64(d.underlying.Get("bufferedAmountLowThreshold").Int())
}
// valueToDataChannelMessage converts the given value to a DataChannelMessage.
// val should be obtained from MessageEvent.data where MessageEvent is received
// via the RTCDataChannel.onmessage callback.
func valueToDataChannelMessage(val js.Value) DataChannelMessage {
// If val is of type string, the conversion is straightforward.
if val.Type() == js.TypeString {
return DataChannelMessage{
IsString: true,
Data: []byte(val.String()),
}
}
// For other types, we need to first determine val.constructor.name.
constructorName := val.Get("constructor").Get("name").String()
var data []byte
switch constructorName {
case "Uint8Array":
// We can easily convert Uint8Array to []byte
data = uint8ArrayValueToBytes(val)
case "Blob":
// Convert the Blob to an ArrayBuffer and then convert the ArrayBuffer
// to a Uint8Array.
// See: https://developer.mozilla.org/en-US/docs/Web/API/Blob
// The JavaScript API for reading from the Blob is asynchronous. We use a
// channel to signal when reading is done.
reader := js.Global().Get("FileReader").New()
doneChan := make(chan struct{})
reader.Call("addEventListener", "loadend", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go func() {
// Signal that the FileReader is done reading/loading by sending through
// the doneChan.
doneChan <- struct{}{}
}()
return js.Undefined()
}))
// Wait for the FileReader to finish reading/loading.
<-doneChan
// At this point buffer.result is a typed array, which we know how to
// handle.
buffer := reader.Get("result")
uint8Array := js.Global().Get("Uint8Array").New(buffer)
data = uint8ArrayValueToBytes(uint8Array)
default:
// Assume we have an ArrayBufferView type which we can convert to a
// Uint8Array in JavaScript.
// See: https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView
uint8Array := js.Global().Get("Uint8Array").New(val)
data = uint8ArrayValueToBytes(uint8Array)
}
return DataChannelMessage{
IsString: false,
Data: data,
}
}

10
datachannelmessage.go Normal file
View File

@@ -0,0 +1,10 @@
package webrtc
// DataChannelMessage represents a message received from the
// data channel. IsString will be set to true if the incoming
// message is of the string type. Otherwise the message is of
// a binary type.
type DataChannelMessage struct {
IsString bool
Data []byte
}

View File

@@ -1,3 +1,5 @@
// +build !js
package webrtc
import (

View File

@@ -0,0 +1,110 @@
// +build js
package main
import (
"fmt"
"syscall/js"
"github.com/pions/webrtc"
"github.com/pions/webrtc/examples/internal/signal"
)
func main() {
// Configure and create a new PeerConnection.
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
pc, err := webrtc.NewPeerConnection(config)
if err != nil {
handleError(err)
}
// Create DataChannel.
sendChannel, err := pc.CreateDataChannel("foo", nil)
if err != nil {
handleError(err)
}
sendChannel.OnClose(func() {
fmt.Println("sendChannel has closed")
})
sendChannel.OnOpen(func() {
fmt.Println("sendChannel has opened")
})
sendChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
log(fmt.Sprintf("Message from DataChannel %s payload %s", sendChannel.Label(), string(msg.Data)))
})
// Add handlers for setting up the connection.
pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
log(fmt.Sprint(state))
})
pc.OnICECandidate(func(candidate *string) {
if candidate != nil {
encodedDescr := signal.Encode(pc.LocalDescription())
el := getElementByID("localSessionDescription")
el.Set("value", encodedDescr)
}
})
pc.OnNegotiationNeeded(func() {
offer, err := pc.CreateOffer(nil)
if err != nil {
handleError(err)
}
pc.SetLocalDescription(offer)
})
// Set up global callbacks which will be triggered on button clicks.
js.Global().Set("sendMessage", js.FuncOf(func(_ js.Value, _ []js.Value) interface{} {
go func() {
el := getElementByID("message")
message := el.Get("value").String()
if message == "" {
js.Global().Call("alert", "Message must not be empty")
return
}
if err := sendChannel.SendText(message); err != nil {
handleError(err)
}
}()
return js.Undefined()
}))
js.Global().Set("startSession", js.FuncOf(func(_ js.Value, _ []js.Value) interface{} {
go func() {
el := getElementByID("remoteSessionDescription")
sd := el.Get("value").String()
if sd == "" {
js.Global().Call("alert", "Session Description must not be empty")
return
}
descr := webrtc.SessionDescription{}
signal.Decode(sd, &descr)
if err := pc.SetRemoteDescription(descr); err != nil {
handleError(err)
}
}()
return js.Undefined()
}))
// Stay alive
select {}
}
func log(msg string) {
el := getElementByID("logs")
el.Set("innerHTML", el.Get("innerHTML").String()+msg+"<br>")
}
func handleError(err error) {
log("Unexpected error. Check console.")
panic(err)
}
func getElementByID(id string) js.Value {
return js.Global().Get("document").Call("getElementById", id)
}

View File

@@ -1,3 +1,5 @@
// +build !js
package webrtc
import (

155
js_utils.go Normal file
View File

@@ -0,0 +1,155 @@
// +build js
package webrtc
import (
"fmt"
"syscall/js"
)
// awaitPromise accepts a js.Value representing a Promise. If the promise
// resolves, it returns (result, nil). If the promise rejects, it returns
// (js.Undefined, error). awaitPromise has a synchronous-like API but does not
// block the JavaScript event loop.
func awaitPromise(promise js.Value) (js.Value, error) {
resultsChan := make(chan js.Value)
errChan := make(chan js.Error)
thenFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go func() {
resultsChan <- args[0]
}()
return js.Undefined()
})
defer thenFunc.Release()
promise.Call("then", thenFunc)
catchFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go func() {
errChan <- js.Error{args[0]}
}()
return js.Undefined()
})
defer catchFunc.Release()
promise.Call("catch", catchFunc)
select {
case result := <-resultsChan:
return result, nil
case err := <-errChan:
return js.Undefined(), err
}
}
func valueToUint16Pointer(val js.Value) *uint16 {
if val == js.Null() || val == js.Undefined() {
return nil
}
convertedVal := uint16(val.Int())
return &convertedVal
}
func valueToStringPointer(val js.Value) *string {
if val == js.Null() || val == js.Undefined() {
return nil
}
stringVal := val.String()
return &stringVal
}
func stringToValueOrUndefined(val string) js.Value {
if val == "" {
return js.Undefined()
}
return js.ValueOf(val)
}
func uint8ToValueOrUndefined(val uint8) js.Value {
if val == 0 {
return js.Undefined()
}
return js.ValueOf(val)
}
func interfaceToValueOrUndefined(val interface{}) js.Value {
if val == nil {
return js.Undefined()
}
return js.ValueOf(val)
}
func valueToStringOrZero(val js.Value) string {
if val == js.Undefined() || val == js.Null() {
return ""
}
return val.String()
}
func valueToUint8OrZero(val js.Value) uint8 {
if val == js.Undefined() || val == js.Null() {
return 0
}
return uint8(val.Int())
}
func valueToStrings(val js.Value) []string {
result := make([]string, val.Length())
for i := 0; i < val.Length(); i++ {
result[i] = val.Index(i).String()
}
return result
}
func stringPointerToValue(val *string) js.Value {
if val == nil {
return js.Undefined()
}
return js.ValueOf(*val)
}
func uint16PointerToValue(val *uint16) js.Value {
if val == nil {
return js.Undefined()
}
return js.ValueOf(*val)
}
func boolPointerToValue(val *bool) js.Value {
if val == nil {
return js.Undefined()
}
return js.ValueOf(*val)
}
func stringsToValue(strings []string) js.Value {
val := make([]interface{}, len(strings))
for i, s := range strings {
val[i] = s
}
return js.ValueOf(val)
}
func stringEnumToValueOrUndefined(s string) js.Value {
if s == "unknown" {
return js.Undefined()
}
return js.ValueOf(s)
}
// Converts the return value of recover() to an error.
func recoveryToError(e interface{}) error {
switch e := e.(type) {
case error:
return e
default:
return fmt.Errorf("recovered with non-error value: (%T) %s", e, e)
}
}
func uint8ArrayValueToBytes(val js.Value) []byte {
result := make([]byte, val.Length())
for i := 0; i < val.Length(); i++ {
result[i] = byte(val.Index(i).Int())
}
return result
}

View File

@@ -1,3 +1,5 @@
// +build !js
// Package webrtc implements the WebRTC 1.0 as defined in W3C WebRTC specification document.
package webrtc
@@ -22,15 +24,6 @@ import (
var pcLog = logging.NewScopedLogger("pc")
const (
// Unknown defines default public constant to use for "enum" like struct
// comparisons when no value was defined.
Unknown = iota
unknownStr = "unknown"
receiveMTU = 8192
)
// PeerConnection represents a WebRTC connection that establishes a
// peer-to-peer communications with another PeerConnection instance in a
// browser, or to another endpoint implementing the required protocols.

536
peerconnection_js.go Normal file
View File

@@ -0,0 +1,536 @@
// +build js
// Package webrtc implements the WebRTC 1.0 as defined in W3C WebRTC specification document.
package webrtc
import (
"syscall/js"
)
// PeerConnection represents a WebRTC connection that establishes a
// peer-to-peer communications with another PeerConnection instance in a
// browser, or to another endpoint implementing the required protocols.
type PeerConnection struct {
// Pointer to the underlying JavaScript RTCPeerConnection object.
underlying js.Value
// Keep track of handlers/callbacks so we can call Release as required by the
// syscall/js API. Initially nil.
onSignalingStateChangeHandler *js.Func
onDataChannelHandler *js.Func
onICEConectionStateChangeHandler *js.Func
onICECandidateHandler *js.Func
onNegotiationNeededHandler *js.Func
}
// NewPeerConnection creates a peerconnection with the default
// codecs.
func NewPeerConnection(configuration Configuration) (_ *PeerConnection, err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
configMap := configurationToValue(configuration)
underlying := js.Global().Get("window").Get("RTCPeerConnection").New(configMap)
return &PeerConnection{
underlying: underlying,
}, nil
}
// OnSignalingStateChange sets an event handler which is invoked when the
// peer connection's signaling state changes
func (pc *PeerConnection) OnSignalingStateChange(f func(SignalingState)) {
if pc.onSignalingStateChangeHandler != nil {
oldHandler := pc.onSignalingStateChangeHandler
defer oldHandler.Release()
}
onSignalingStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// TODO(albrow): Protect args access; recover from panics.
state := newSignalingState(args[0].String())
go f(state)
return js.Undefined()
})
pc.onSignalingStateChangeHandler = &onSignalingStateChangeHandler
pc.underlying.Set("onsignalingstatechange", onSignalingStateChangeHandler)
}
// OnDataChannel sets an event handler which is invoked when a data
// channel message arrives from a remote peer.
func (pc *PeerConnection) OnDataChannel(f func(*DataChannel)) {
if pc.onDataChannelHandler != nil {
oldHandler := pc.onDataChannelHandler
defer oldHandler.Release()
}
onDataChannelHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// BUG(albrow): This reference to the underlying DataChannel doesn't know
// about any other references to the same DataChannel. This might result in
// memory leaks where we don't clean up handler functions. Could possibly
// fix by keeping a mutex-protected list of all DataChannel references as a
// property of this PeerConnection, but at the cost of additional overhead.
dataChannel := &DataChannel{
underlying: args[0],
}
go f(dataChannel)
return js.Undefined()
})
pc.onDataChannelHandler = &onDataChannelHandler
pc.underlying.Set("onsignalingstatechange", onDataChannelHandler)
}
// OnTrack sets an event handler which is called when remote track
// arrives from a remote peer.
// func (pc *PeerConnection) OnTrack(f func(*Track, *RTPReceiver)) {
// }
// OnICEConnectionStateChange sets an event handler which is called
// when an ICE connection state is changed.
func (pc *PeerConnection) OnICEConnectionStateChange(f func(ICEConnectionState)) {
if pc.onICEConectionStateChangeHandler != nil {
oldHandler := pc.onICEConectionStateChangeHandler
defer oldHandler.Release()
}
onICEConectionStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
connectionState := newICEConnectionState(pc.underlying.Get("iceConnectionState").String())
go f(connectionState)
return js.Undefined()
})
pc.onICEConectionStateChangeHandler = &onICEConectionStateChangeHandler
pc.underlying.Set("oniceconnectionstatechange", onICEConectionStateChangeHandler)
}
// SetConfiguration updates the configuration of this PeerConnection object.
func (pc *PeerConnection) SetConfiguration(configuration Configuration) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
configMap := configurationToValue(configuration)
pc.underlying.Call("setConfiguration", configMap)
return nil
}
// GetConfiguration returns a Configuration object representing the current
// configuration of this PeerConnection object. The returned object is a
// copy and direct mutation on it will not take affect until SetConfiguration
// has been called with Configuration passed as its only argument.
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-getconfiguration
func (pc *PeerConnection) GetConfiguration() Configuration {
return valueToConfiguration(pc.underlying.Call("getConfiguration"))
}
// CreateOffer starts the PeerConnection and generates the localDescription
func (pc *PeerConnection) CreateOffer(options *OfferOptions) (_ SessionDescription, err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
promise := pc.underlying.Call("createOffer", offerOptionsToValue(options))
desc, err := awaitPromise(promise)
if err != nil {
return SessionDescription{}, err
}
return *valueToSessionDescription(desc), nil
}
// CreateAnswer starts the PeerConnection and generates the localDescription
func (pc *PeerConnection) CreateAnswer(options *AnswerOptions) (_ SessionDescription, err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
promise := pc.underlying.Call("createAnswer", answerOptionsToValue(options))
desc, err := awaitPromise(promise)
if err != nil {
return SessionDescription{}, err
}
return *valueToSessionDescription(desc), nil
}
// SetLocalDescription sets the SessionDescription of the local peer
func (pc *PeerConnection) SetLocalDescription(desc SessionDescription) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
promise := pc.underlying.Call("setLocalDescription", sessionDescriptionToValue(&desc))
_, err = awaitPromise(promise)
return err
}
// LocalDescription returns PendingLocalDescription if it is not null and
// otherwise it returns CurrentLocalDescription. This property is used to
// determine if setLocalDescription has already been called.
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-localdescription
func (pc *PeerConnection) LocalDescription() *SessionDescription {
return valueToSessionDescription(pc.underlying.Get("localDescription"))
}
// SetRemoteDescription sets the SessionDescription of the remote peer
func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
promise := pc.underlying.Call("setRemoteDescription", sessionDescriptionToValue(&desc))
_, err = awaitPromise(promise)
return err
}
// RemoteDescription returns PendingRemoteDescription if it is not null and
// otherwise it returns CurrentRemoteDescription. This property is used to
// determine if setRemoteDescription has already been called.
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-remotedescription
func (pc *PeerConnection) RemoteDescription() *SessionDescription {
return valueToSessionDescription(pc.underlying.Get("remoteDescription"))
}
// AddICECandidate accepts an ICE candidate string and adds it
// to the existing set of candidates
func (pc *PeerConnection) AddICECandidate(candidate ICECandidateInit) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
promise := pc.underlying.Call("addIceCandidate", iceCandidateInitToValue(candidate))
_, err = awaitPromise(promise)
return err
}
// ICEConnectionState returns the ICE connection state of the
// PeerConnection instance.
func (pc *PeerConnection) ICEConnectionState() ICEConnectionState {
return newICEConnectionState(pc.underlying.Get("iceConnectionState").String())
}
// TODO(albrow): This function doesn't exist in the Go implementation.
// TODO(albrow): Follow the spec more closely. Handler should accept
// RTCPeerConnectionIceEvent instead of *string.
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onicecandidate
func (pc *PeerConnection) OnICECandidate(f func(candidate *string)) {
if pc.onICECandidateHandler != nil {
oldHandler := pc.onICECandidateHandler
defer oldHandler.Release()
}
onICECandidateHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// TODO(albrow): Protect args access; recover from panics.
candidate := valueToStringPointer(args[0].Get("candidate"))
go f(candidate)
return js.Undefined()
})
pc.onICECandidateHandler = &onICECandidateHandler
pc.underlying.Set("onicecandidate", onICECandidateHandler)
}
// TODO(albrow): This function doesn't exist in the Go implementation.
func (pc *PeerConnection) OnNegotiationNeeded(f func()) {
if pc.onNegotiationNeededHandler != nil {
oldHandler := pc.onNegotiationNeededHandler
defer oldHandler.Release()
}
onNegotiationNeededHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go f()
return js.Undefined()
})
pc.onNegotiationNeededHandler = &onNegotiationNeededHandler
pc.underlying.Set("onnegotiationneeded", onNegotiationNeededHandler)
}
// // GetSenders returns the RTPSender that are currently attached to this PeerConnection
// func (pc *PeerConnection) GetSenders() []*RTPSender {
// }
// // GetReceivers returns the RTPReceivers that are currently attached to this RTCPeerConnection
// func (pc *PeerConnection) GetReceivers() []*RTPReceiver {
// }
// // GetTransceivers returns the RTCRtpTransceiver that are currently attached to this RTCPeerConnection
// func (pc *PeerConnection) GetTransceivers() []*RTPTransceiver {
// }
// // AddTrack adds a Track to the PeerConnection
// func (pc *PeerConnection) AddTrack(track *Track) (*RTPSender, error) {
// }
// func (pc *PeerConnection) RemoveTrack() {
// }
// func (pc *PeerConnection) AddTransceiver() RTPTransceiver {
// }
// CreateDataChannel creates a new DataChannel object with the given label
// and optional DataChannelInit used to configure properties of the
// underlying channel such as data reliability.
func (pc *PeerConnection) CreateDataChannel(label string, options *DataChannelInit) (_ *DataChannel, err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
channel := pc.underlying.Call("createDataChannel", label, dataChannelInitToValue(options))
return &DataChannel{
underlying: channel,
}, nil
}
// SetIdentityProvider is used to configure an identity provider to generate identity assertions
func (pc *PeerConnection) SetIdentityProvider(provider string) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
pc.underlying.Call("setIdentityProvider", provider)
return nil
}
// Note(albrow) SendRTCP is not included in MDN WebRTC documentation.
// SendRTCP sends a user provided RTCP packet to the connected peer
// If no peer is connected the packet is discarded
// func (pc *PeerConnection) SendRTCP(pkt rtcp.Packet) error {
// return errors.New("Not yet implemented")
// }
// Close ends the PeerConnection
func (pc *PeerConnection) Close() (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
pc.underlying.Call("close")
// Release any handlers as required by the syscall/js API.
if pc.onSignalingStateChangeHandler != nil {
pc.onSignalingStateChangeHandler.Release()
}
if pc.onDataChannelHandler != nil {
pc.onDataChannelHandler.Release()
}
if pc.onICEConectionStateChangeHandler != nil {
pc.onICEConectionStateChangeHandler.Release()
}
if pc.onICECandidateHandler != nil {
pc.onICECandidateHandler.Release()
}
if pc.onNegotiationNeededHandler != nil {
pc.onNegotiationNeededHandler.Release()
}
return nil
}
// NewTrack Creates a new Track
// func (pc *PeerConnection) NewTrack(payloadType uint8, ssrc uint32, id, label string) (*Track, error) {
// return nil, errors.New("Not yet implemented")
// }
// CurrentLocalDescription represents the local description that was
// successfully negotiated the last time the PeerConnection transitioned
// into the stable state plus any local candidates that have been generated
// by the ICEAgent since the offer or answer was created.
func (pc *PeerConnection) CurrentLocalDescription() *SessionDescription {
desc := pc.underlying.Get("currentLocalDescription")
return valueToSessionDescription(desc)
}
// PendingLocalDescription represents a local description that is in the
// process of being negotiated plus any local candidates that have been
// generated by the ICEAgent since the offer or answer was created. If the
// PeerConnection is in the stable state, the value is null.
func (pc *PeerConnection) PendingLocalDescription() *SessionDescription {
desc := pc.underlying.Get("pendingLocalDescription")
return valueToSessionDescription(desc)
}
// CurrentRemoteDescription represents the last remote description that was
// successfully negotiated the last time the PeerConnection transitioned
// into the stable state plus any remote candidates that have been supplied
// via AddICECandidate() since the offer or answer was created.
func (pc *PeerConnection) CurrentRemoteDescription() *SessionDescription {
desc := pc.underlying.Get("currentRemoteDescription")
return valueToSessionDescription(desc)
}
// PendingRemoteDescription represents a remote description that is in the
// process of being negotiated, complete with any remote candidates that
// have been supplied via AddICECandidate() since the offer or answer was
// created. If the PeerConnection is in the stable state, the value is
// null.
func (pc *PeerConnection) PendingRemoteDescription() *SessionDescription {
desc := pc.underlying.Get("pendingRemoteDescription")
return valueToSessionDescription(desc)
}
// SignalingState returns the signaling state of the PeerConnection instance.
func (pc *PeerConnection) SignalingState() SignalingState {
rawState := pc.underlying.Get("signalingState").String()
return newSignalingState(rawState)
}
// ICEGatheringState attribute the ICE gathering state of the PeerConnection
// instance.
func (pc *PeerConnection) ICEGatheringState() ICEGatheringState {
rawState := pc.underlying.Get("iceGatheringState").String()
return newICEGatheringState(rawState)
}
// ConnectionState attribute the connection state of the PeerConnection
// instance.
func (pc *PeerConnection) ConnectionState() PeerConnectionState {
rawState := pc.underlying.Get("connectionState").String()
return newPeerConnectionState(rawState)
}
// 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.
func configurationToValue(configuration Configuration) js.Value {
return js.ValueOf(map[string]interface{}{
"iceServers": iceServersToValue(configuration.ICEServers),
"iceTransportPolicy": stringEnumToValueOrUndefined(configuration.ICETransportPolicy.String()),
"bundlePolicy": stringEnumToValueOrUndefined(configuration.BundlePolicy.String()),
"rtcpMuxPolicy": stringEnumToValueOrUndefined(configuration.RTCPMuxPolicy.String()),
"peerIdentity": stringToValueOrUndefined(configuration.PeerIdentity),
"iceCandidatePoolSize": uint8ToValueOrUndefined(configuration.ICECandidatePoolSize),
// TODO(albrow): Docs for RTCCertificate are underspecified.
// https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate How
// should we handle this?
// "certificates": configuration.Certificates,
})
}
func iceServersToValue(iceServers []ICEServer) js.Value {
if len(iceServers) == 0 {
return js.Undefined()
}
maps := make([]interface{}, len(iceServers))
for i, server := range iceServers {
maps[i] = iceServerToValue(server)
}
return js.ValueOf(maps)
}
func iceServerToValue(server ICEServer) js.Value {
return js.ValueOf(map[string]interface{}{
"urls": stringsToValue(server.URLs), // required
"username": stringToValueOrUndefined(server.Username),
"credential": interfaceToValueOrUndefined(server.Credential),
"credentialType": stringEnumToValueOrUndefined(server.CredentialType.String()),
})
}
func valueToConfiguration(configValue js.Value) Configuration {
if configValue == js.Null() || configValue == js.Undefined() {
return Configuration{}
}
return Configuration{
ICEServers: valueToICEServers(configValue.Get("iceServers")),
ICETransportPolicy: newICETransportPolicy(valueToStringOrZero(configValue.Get("iceTransportPolicy"))),
BundlePolicy: newBundlePolicy(valueToStringOrZero(configValue.Get("bundlePolicy"))),
RTCPMuxPolicy: newRTCPMuxPolicy(valueToStringOrZero(configValue.Get("bundlePolicy"))),
PeerIdentity: valueToStringOrZero(configValue.Get("peerIdentity")),
ICECandidatePoolSize: valueToUint8OrZero(configValue.Get("iceCandidatePoolSize")),
// TODO(albrow): Docs for RTCCertificate are underspecified.
// https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate How
// should we handle this?
// Certificates []Certificate
}
}
func valueToICEServers(iceServersValue js.Value) []ICEServer {
if iceServersValue == js.Null() || iceServersValue == js.Undefined() {
return nil
}
iceServers := make([]ICEServer, iceServersValue.Length())
for i := 0; i < iceServersValue.Length(); i++ {
iceServers[i] = valueToICEServer(iceServersValue.Index(i))
}
return iceServers
}
func valueToICEServer(iceServerValue js.Value) ICEServer {
return ICEServer{
URLs: valueToStrings(iceServerValue.Get("urls")), // required
Username: valueToStringOrZero(iceServerValue.Get("username")),
// TODO(albrow): Handle Credential which might have different types.
// Credential: iceServerValue.Get("credential"),
CredentialType: newICECredentialType(valueToStringOrZero(iceServerValue.Get("credentialType"))),
}
}
func sessionDescriptionToValue(desc *SessionDescription) js.Value {
if desc == nil {
return js.Undefined()
}
return js.ValueOf(map[string]interface{}{
"type": desc.Type.String(),
"sdp": desc.SDP,
})
}
func valueToSessionDescription(descValue js.Value) *SessionDescription {
if descValue == js.Null() || descValue == js.Undefined() {
return nil
}
return &SessionDescription{
Type: newSDPType(descValue.Get("type").String()),
SDP: descValue.Get("sdp").String(),
}
}
func offerOptionsToValue(offerOptions *OfferOptions) js.Value {
if offerOptions == nil {
return js.Undefined()
}
return js.ValueOf(map[string]interface{}{
"iceRestart": offerOptions.ICERestart,
"voiceActivityDetection": offerOptions.VoiceActivityDetection,
})
}
func answerOptionsToValue(answerOptions *AnswerOptions) js.Value {
if answerOptions == nil {
return js.Undefined()
}
return js.ValueOf(map[string]interface{}{
"voiceActivityDetection": answerOptions.VoiceActivityDetection,
})
}
func iceCandidateInitToValue(candidate ICECandidateInit) js.Value {
return js.ValueOf(map[string]interface{}{
"candidate": candidate.Candidate,
"sdpMid": stringPointerToValue(candidate.SDPMid),
"sdpMLineIndex": uint16PointerToValue(candidate.SDPMLineIndex),
"usernameFragment": candidate.UsernameFragment,
})
}
func dataChannelInitToValue(options *DataChannelInit) js.Value {
if options == nil {
return js.Undefined()
}
return js.ValueOf(map[string]interface{}{
"ordered": boolPointerToValue(options.Ordered),
"maxPacketLifeTime": uint16PointerToValue(options.MaxPacketLifeTime),
"maxRetransmits": uint16PointerToValue(options.MaxRetransmits),
"protocol": stringPointerToValue(options.Protocol),
"negotiated": boolPointerToValue(options.Negotiated),
"id": uint16PointerToValue(options.ID),
// Note(albrow) Priority is not included in MDN WebRTC documentation. Should
// we include it here?
// "priority": options.Priority
})
}

View File

@@ -1,3 +1,5 @@
// +build !js
package webrtc
import (

View File

@@ -1,3 +1,5 @@
// +build !js
package webrtc
import (

View File

@@ -1,3 +1,5 @@
// +build !js
package webrtc
import (

View File

@@ -1,3 +1,5 @@
// +build !js
package webrtc
import "fmt"

View File

@@ -1,3 +1,5 @@
// +build !js
package webrtc
import (

View File

@@ -1,3 +1,5 @@
// +build !js
package webrtc
import (