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 +}