Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WASM Detach #565

Merged
merged 2 commits into from
Apr 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions api_js.go
Original file line number Diff line number Diff line change
@@ -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
}
}
36 changes: 34 additions & 2 deletions datachannel_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
package webrtc

import (
"fmt"
"syscall/js"

"github.com/pions/datachannel"
)

const dataChannelBufferSize = 16384 // Lowest common denominator among browsers
Expand All @@ -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
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Nice catch!

f(msg)
}()
return js.Undefined()
})
d.onMessageHandler = &onMessageHandler
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -243,6 +273,8 @@ func valueToDataChannelMessage(val js.Value) DataChannelMessage {
return js.Undefined()
}))

reader.Call("readAsArrayBuffer", val)
albrow marked this conversation as resolved.
Show resolved Hide resolved

// 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
Loading