diff --git a/api_js.go b/api_js.go
new file mode 100644
index 0000000000..964b7b05e0
--- /dev/null
+++ b/api_js.go
@@ -0,0 +1,31 @@
+// +build js,wasm
+
+package webrtc
+
+// API bundles the global funcions of the WebRTC and ORTC API.
+type API struct {
+ settingEngine *SettingEngine
+}
+
+// NewAPI Creates a new API object for keeping semi-global settings to WebRTC objects
+func NewAPI(options ...func(*API)) *API {
+ a := &API{}
+
+ for _, o := range options {
+ o(a)
+ }
+
+ if a.settingEngine == nil {
+ a.settingEngine = &SettingEngine{}
+ }
+
+ return a
+}
+
+// WithSettingEngine allows providing a SettingEngine to the API.
+// Settings should not be changed after passing the engine to an API.
+func WithSettingEngine(s SettingEngine) func(a *API) {
+ return func(a *API) {
+ a.settingEngine = &s
+ }
+}
diff --git a/datachannel_js.go b/datachannel_js.go
index 8646ab7516..2faf72f9fc 100644
--- a/datachannel_js.go
+++ b/datachannel_js.go
@@ -3,7 +3,10 @@
package webrtc
import (
+ "fmt"
"syscall/js"
+
+ "github.com/pions/datachannel"
)
const dataChannelBufferSize = 16384 // Lowest common denominator among browsers
@@ -20,6 +23,9 @@ type DataChannel struct {
onOpenHandler *js.Func
onCloseHandler *js.Func
onMessageHandler *js.Func
+
+ // A reference to the associated api object used by this datachannel
+ api *API
}
// OnOpen sets an event handler which is invoked when
@@ -60,8 +66,15 @@ func (d *DataChannel) OnMessage(f func(msg DataChannelMessage)) {
defer oldHandler.Release()
}
onMessageHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
- msg := valueToDataChannelMessage(args[0].Get("data"))
- go f(msg)
+ // TODO: Ensure message order?
+ data := args[0].Get("data")
+ go func() {
+ // valueToDataChannelMessage may block when handling 'Blob' data
+ // so we need to call it from a new routine. See:
+ // https://godoc.org/syscall/js#FuncOf
+ msg := valueToDataChannelMessage(data)
+ f(msg)
+ }()
return js.Undefined()
})
d.onMessageHandler = &onMessageHandler
@@ -92,6 +105,23 @@ func (d *DataChannel) SendText(s string) (err error) {
return nil
}
+// Detach allows you to detach the underlying datachannel. This provides
+// an idiomatic API to work with, however it disables the OnMessage callback.
+// Before calling Detach you have to enable this behavior by calling
+// webrtc.DetachDataChannels(). Combining detached and normal data channels
+// is not supported.
+// Please reffer to the data-channels-detach example and the
+// pions/datachannel documentation for the correct way to handle the
+// resulting DataChannel object.
+func (d *DataChannel) Detach() (datachannel.ReadWriteCloser, error) {
+ if !d.api.settingEngine.detach.DataChannels {
+ return nil, fmt.Errorf("enable detaching by calling webrtc.DetachDataChannels()")
+ }
+
+ detached := newDetachedDataChannel(d)
+ return detached, 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) {
@@ -243,6 +273,8 @@ func valueToDataChannelMessage(val js.Value) DataChannelMessage {
return js.Undefined()
}))
+ reader.Call("readAsArrayBuffer", val)
+
// Wait for the FileReader to finish reading/loading.
<-doneChan
diff --git a/datachannel_js_detach.go b/datachannel_js_detach.go
new file mode 100644
index 0000000000..7a84876d36
--- /dev/null
+++ b/datachannel_js_detach.go
@@ -0,0 +1,71 @@
+// +build js,wasm
+
+package webrtc
+
+import (
+ "errors"
+)
+
+type detachedDataChannel struct {
+ dc *DataChannel
+
+ read chan DataChannelMessage
+ done chan struct{}
+}
+
+func newDetachedDataChannel(dc *DataChannel) *detachedDataChannel {
+ read := make(chan DataChannelMessage)
+ done := make(chan struct{})
+
+ // Wire up callbacks
+ dc.OnMessage(func(msg DataChannelMessage) {
+ read <- msg // TODO: Potential leak?
+ })
+
+ // TODO: OnClose?
+
+ return &detachedDataChannel{
+ dc: dc,
+ read: read,
+ done: done,
+ }
+}
+
+func (c *detachedDataChannel) Read(p []byte) (int, error) {
+ n, _, err := c.ReadDataChannel(p)
+ return n, err
+}
+
+func (c *detachedDataChannel) ReadDataChannel(p []byte) (int, bool, error) {
+ select {
+ case <-c.done:
+ return 0, false, errors.New("Reader closed")
+ case msg := <-c.read:
+ n := copy(p, msg.Data)
+ if n < len(msg.Data) {
+ return n, msg.IsString, errors.New("Read buffer to small")
+ }
+ return n, msg.IsString, nil
+ }
+}
+
+func (c *detachedDataChannel) Write(p []byte) (n int, err error) {
+ return c.WriteDataChannel(p, false)
+}
+
+func (c *detachedDataChannel) WriteDataChannel(p []byte, isString bool) (n int, err error) {
+ if isString {
+ err = c.dc.SendText(string(p))
+ return len(p), err
+ }
+
+ err = c.dc.Send(p)
+
+ return len(p), err
+}
+
+func (c *detachedDataChannel) Close() error {
+ close(c.done)
+
+ return c.dc.Close()
+}
diff --git a/examples/data-channels-detach/jsfiddle/demo.css b/examples/data-channels-detach/jsfiddle/demo.css
new file mode 100644
index 0000000000..9e43d34075
--- /dev/null
+++ b/examples/data-channels-detach/jsfiddle/demo.css
@@ -0,0 +1,4 @@
+textarea {
+ width: 500px;
+ min-height: 75px;
+}
\ No newline at end of file
diff --git a/examples/data-channels-detach/jsfiddle/demo.html b/examples/data-channels-detach/jsfiddle/demo.html
new file mode 100644
index 0000000000..6afd5f4bfe
--- /dev/null
+++ b/examples/data-channels-detach/jsfiddle/demo.html
@@ -0,0 +1,16 @@
+Browser base64 Session Description
+
+
+Golang base64 Session Description
+
+
+
+
+
+
+
+
+Logs
+
\ No newline at end of file
diff --git a/examples/data-channels-detach/jsfiddle/main.go b/examples/data-channels-detach/jsfiddle/main.go
new file mode 100644
index 0000000000..fcabc4ff46
--- /dev/null
+++ b/examples/data-channels-detach/jsfiddle/main.go
@@ -0,0 +1,176 @@
+// +build js,wasm
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "syscall/js"
+ "time"
+
+ "github.com/pions/webrtc"
+
+ "github.com/pions/webrtc/examples/internal/signal"
+)
+
+const messageSize = 15
+
+func main() {
+ // Since this behavior diverges from the WebRTC API it has to be
+ // enabled using a settings engine. Mixing both detached and the
+ // OnMessage DataChannel API is not supported.
+
+ // Create a SettingEngine and enable Detach
+ s := webrtc.SettingEngine{}
+ s.DetachDataChannels()
+
+ // Create an API object with the engine
+ api := webrtc.NewAPI(webrtc.WithSettingEngine(s))
+
+ // 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 RTCPeerConnection using the API object
+ peerConnection, err := api.NewPeerConnection(config)
+ if err != nil {
+ handleError(err)
+ }
+
+ // Create a datachannel with label 'data'
+ dataChannel, err := peerConnection.CreateDataChannel("data", nil)
+ if err != nil {
+ handleError(err)
+ }
+
+ // Set the handler for ICE connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
+ log(fmt.Sprintf("ICE Connection State has changed: %s\n", connectionState.String()))
+ })
+
+ // Register channel opening handling
+ dataChannel.OnOpen(func() {
+ log(fmt.Sprintf("Data channel '%s'-'%d' open.\n", dataChannel.Label(), dataChannel.ID()))
+
+ // Detach the data channel
+ raw, dErr := dataChannel.Detach()
+ if dErr != nil {
+ handleError(dErr)
+ }
+
+ // Handle reading from the data channel
+ go ReadLoop(raw)
+
+ // Handle writing to the data channel
+ go WriteLoop(raw)
+ })
+
+ // Create an offer to send to the browser
+ offer, err := peerConnection.CreateOffer(nil)
+ if err != nil {
+ handleError(err)
+ }
+
+ // Sets the LocalDescription, and starts our UDP listeners
+ err = peerConnection.SetLocalDescription(offer)
+ if err != nil {
+ handleError(err)
+ }
+
+ // Add handlers for setting up the connection.
+ peerConnection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
+ log(fmt.Sprint(state))
+ })
+ peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
+ if candidate != nil {
+ encodedDescr := signal.Encode(peerConnection.LocalDescription())
+ el := getElementByID("localSessionDescription")
+ el.Set("value", encodedDescr)
+ }
+ })
+
+ // 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 := peerConnection.SetRemoteDescription(descr); err != nil {
+ handleError(err)
+ }
+ }()
+ return js.Undefined()
+ }))
+
+ // Block forever
+ select {}
+}
+
+// ReadLoop shows how to read from the datachannel directly
+func ReadLoop(d io.Reader) {
+ for {
+ buffer := make([]byte, messageSize)
+ n, err := d.Read(buffer)
+ if err != nil {
+ log(fmt.Sprintf("Datachannel closed; Exit the readloop: %v", err))
+ return
+ }
+
+ log(fmt.Sprintf("Message from DataChannel: %s\n", string(buffer[:n])))
+ }
+}
+
+// WriteLoop shows how to write to the datachannel directly
+func WriteLoop(d io.Writer) {
+ for range time.NewTicker(5 * time.Second).C {
+ message := signal.RandSeq(messageSize)
+ log(fmt.Sprintf("Sending %s \n", message))
+
+ _, err := d.Write([]byte(message))
+ if err != nil {
+ handleError(err)
+ }
+ }
+}
+
+func log(msg string) {
+ el := getElementByID("logs")
+ el.Set("innerHTML", el.Get("innerHTML").String()+msg+"
")
+}
+
+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)
+}
diff --git a/examples/examples.json b/examples/examples.json
index 7a5d79b673..97e5381492 100644
--- a/examples/examples.json
+++ b/examples/examples.json
@@ -17,6 +17,12 @@
"description": "Example data-channels-close is a variant of data-channels that allow playing with the life cycle of data channels.",
"type": "browser"
},
+ {
+ "title": "Data Channels Detach",
+ "link": "data-channels-detach",
+ "description": "The data-channels-detach is an example that shows how you can detach a data channel.",
+ "type": "browser"
+ },
{
"title": "Gstreamer Receive",
"link": "gstreamer-receive",
diff --git a/peerconnection_js.go b/peerconnection_js.go
index 2e17c7d7c4..bcff2fc834 100644
--- a/peerconnection_js.go
+++ b/peerconnection_js.go
@@ -23,11 +23,19 @@ type PeerConnection struct {
onICEConectionStateChangeHandler *js.Func
onICECandidateHandler *js.Func
onICEGatheringStateChangeHandler *js.Func
+
+ // A reference to the associated API state used by this connection
+ api *API
+}
+
+// NewPeerConnection creates a peerconnection.
+func NewPeerConnection(configuration Configuration) (*PeerConnection, error) {
+ api := NewAPI()
+ return api.NewPeerConnection(configuration)
}
-// NewPeerConnection creates a peerconnection with the default
-// codecs.
-func NewPeerConnection(configuration Configuration) (_ *PeerConnection, err error) {
+// NewPeerConnection creates a new PeerConnection with the provided configuration against the received API object
+func (api *API) NewPeerConnection(configuration Configuration) (_ *PeerConnection, err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
@@ -37,6 +45,7 @@ func NewPeerConnection(configuration Configuration) (_ *PeerConnection, err erro
underlying := js.Global().Get("window").Get("RTCPeerConnection").New(configMap)
return &PeerConnection{
underlying: underlying,
+ api: api,
}, nil
}
@@ -71,6 +80,7 @@ func (pc *PeerConnection) OnDataChannel(f func(*DataChannel)) {
// property of this PeerConnection, but at the cost of additional overhead.
dataChannel := &DataChannel{
underlying: args[0].Get("channel"),
+ api: pc.api,
}
go f(dataChannel)
return js.Undefined()
@@ -341,6 +351,7 @@ func (pc *PeerConnection) CreateDataChannel(label string, options *DataChannelIn
channel := pc.underlying.Call("createDataChannel", label, dataChannelInitToValue(options))
return &DataChannel{
underlying: channel,
+ api: pc.api,
}, nil
}
diff --git a/settingengine_js.go b/settingengine_js.go
new file mode 100644
index 0000000000..5b77d66023
--- /dev/null
+++ b/settingengine_js.go
@@ -0,0 +1,19 @@
+// +build js,wasm
+
+package webrtc
+
+// SettingEngine allows influencing behavior in ways that are not
+// supported by the WebRTC API. This allows us to support additional
+// use-cases without deviating from the WebRTC API elsewhere.
+type SettingEngine struct {
+ detach struct {
+ DataChannels bool
+ }
+}
+
+// DetachDataChannels enables detaching data channels. When enabled
+// data channels have to be detached in the OnOpen callback using the
+// DataChannel.Detach method.
+func (e *SettingEngine) DetachDataChannels() {
+ e.detach.DataChannels = true
+}