Skip to content

Commit

Permalink
Detach: WASM Shim
Browse files Browse the repository at this point in the history
JS/WASM shim for Detach API
  • Loading branch information
backkem committed Apr 1, 2019
1 parent 57a3296 commit d906c2b
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 2 deletions.
32 changes: 30 additions & 2 deletions datachannel_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package webrtc
import (
"fmt"
"syscall/js"

"github.com/pions/datachannel"
)

const dataChannelBufferSize = 16384 // Lowest common denominator among browsers
Expand Down Expand Up @@ -64,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
Expand Down Expand Up @@ -96,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) {
Expand Down Expand Up @@ -247,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

Expand Down
71 changes: 71 additions & 0 deletions datachannel_js_detach.go
Original file line number Diff line number Diff line change
@@ -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()
}
4 changes: 4 additions & 0 deletions examples/data-channels-detach/jsfiddle/demo.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
textarea {
width: 500px;
min-height: 75px;
}
16 changes: 16 additions & 0 deletions examples/data-channels-detach/jsfiddle/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Browser base64 Session Description<br />
<textarea id="localSessionDescription" readonly="true"></textarea> <br />

Golang base64 Session Description<br />
<textarea id="remoteSessionDescription"></textarea><br/>
<button onclick="window.startSession()">Start Session</button><br />

<br />

<!--Message<br />
<textarea id="message">This is my DataChannel message!</textarea> <br/>
<button onclick="window.sendMessage()">Send Message</button> <br />-->

<br />
Logs<br />
<div id="logs"></div>
176 changes: 176 additions & 0 deletions examples/data-channels-detach/jsfiddle/main.go
Original file line number Diff line number Diff line change
@@ -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+"<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)
}
6 changes: 6 additions & 0 deletions examples/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit d906c2b

Please sign in to comment.