diff --git a/examples/data-channels-create/README.md b/examples/data-channels-create/README.md
new file mode 100644
index 00000000..85c3e018
--- /dev/null
+++ b/examples/data-channels-create/README.md
@@ -0,0 +1,31 @@
+# data-channels
+data-channels is a pion-WebRTC application that shows how you can send/recv DataChannel messages from a web browser
+
+## Instructions
+### Download data-channels
+```
+go get github.com/pions/webrtc/examples/data-channels
+```
+
+### Open data-channels example page
+[jsfiddle.net](https://jsfiddle.net/gh/get/library/pure/pions/webrtc/tree/master/examples/data-channels/jsfiddle)
+
+### Run data-channels, with your browsers SessionDescription as stdin
+In the jsfiddle the top textarea is your browser's session description, copy that and:
+#### Linux/macOS
+Run `echo $BROWSER_SDP | data-channels`
+#### Windows
+1. Paste the SessionDescription into a file.
+1. Run `data-channels < my_file`
+
+### Input data-channels's SessionDescription into your browser
+Copy the text that `data-channels` just emitted and copy into second text area
+
+### Hit 'Start Session' in jsfiddle
+Under Start Session you should see 'Checking' as it starts connecting. If everything worked you should see `New DataChannel foo 1`
+
+Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your browser!
+
+You can also type in your terminal, and when you hit enter it will appear in your web browser.
+
+Congrats, you have used pion-WebRTC! Now start building something cool
diff --git a/examples/data-channels-create/jsfiddle/demo.css b/examples/data-channels-create/jsfiddle/demo.css
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/examples/data-channels-create/jsfiddle/demo.css
@@ -0,0 +1 @@
+
diff --git a/examples/data-channels-create/jsfiddle/demo.details b/examples/data-channels-create/jsfiddle/demo.details
new file mode 100644
index 00000000..ab58100d
--- /dev/null
+++ b/examples/data-channels-create/jsfiddle/demo.details
@@ -0,0 +1,5 @@
+---
+ name: data-channels
+ description: Example of using pion-WebRTC to communicate with a web browser using bi-direction DataChannels
+ authors:
+ - Sean DuBois
diff --git a/examples/data-channels-create/jsfiddle/demo.html b/examples/data-channels-create/jsfiddle/demo.html
new file mode 100644
index 00000000..1cc6e85d
--- /dev/null
+++ b/examples/data-channels-create/jsfiddle/demo.html
@@ -0,0 +1,9 @@
+Browser base64 Session Description
+Golang base64 Session Description:
+
+
+
+Message:
+
+
+
diff --git a/examples/data-channels-create/jsfiddle/demo.js b/examples/data-channels-create/jsfiddle/demo.js
new file mode 100644
index 00000000..d45fc1c5
--- /dev/null
+++ b/examples/data-channels-create/jsfiddle/demo.js
@@ -0,0 +1,42 @@
+/* eslint-env browser */
+
+let pc = new RTCPeerConnection()
+let log = msg => {
+ document.getElementById('logs').innerHTML += msg + ' '
+}
+
+let sendChannel = pc.createDataChannel()
+console.log(sendChannel.id)
+sendChannel.onclose = () => console.log('sendChannel has closed')
+sendChannel.onopen = () => console.log('sendChannel has opened')
+sendChannel.onmessage = e => log(`sendChannel got '${e.data}'`)
+
+pc.oniceconnectionstatechange = e => log(pc.iceConnectionState)
+
+pc.onnegotiationneeded = e =>
+ pc.createOffer({ }).then(d => {
+ document.getElementById('localSessionDescription').value = btoa(d.sdp)
+ return pc.setLocalDescription(d)
+ }).catch(log)
+
+window.sendMessage = () => {
+ let message = document.getElementById('message').value
+ if (message === '') {
+ return alert('Message must not be empty')
+ }
+
+ sendChannel.send(message)
+}
+
+window.startSession = () => {
+ let sd = document.getElementById('remoteSessionDescription').value
+ if (sd === '') {
+ return alert('Session Description must not be empty')
+ }
+
+ try {
+ pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: atob(sd)}))
+ } catch (e) {
+ alert(e)
+ }
+}
diff --git a/examples/data-channels-create/main.go b/examples/data-channels-create/main.go
new file mode 100644
index 00000000..7c05b10f
--- /dev/null
+++ b/examples/data-channels-create/main.go
@@ -0,0 +1,110 @@
+package main
+
+import (
+ "bufio"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "math/rand"
+ "os"
+ "time"
+
+ "github.com/pions/webrtc"
+ "github.com/pions/webrtc/pkg/datachannel"
+ "github.com/pions/webrtc/pkg/ice"
+)
+
+func randSeq(n int) string {
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+ b := make([]rune, n)
+ for i := range b {
+ b[i] = letters[r.Intn(len(letters))]
+ }
+ return string(b)
+}
+
+func main() {
+ reader := bufio.NewReader(os.Stdin)
+ rawSd, err := reader.ReadString('\n')
+ if err != nil && err != io.EOF {
+ panic(err)
+ }
+
+ fmt.Println("")
+ sd, err := base64.StdEncoding.DecodeString(rawSd)
+ if err != nil {
+ panic(err)
+ }
+
+ /* Everything below is the pion-WebRTC API, thanks for using it! */
+
+ // Create a new RTCPeerConnection
+ peerConnection, err := webrtc.New(webrtc.RTCConfiguration{
+ ICEServers: []webrtc.RTCICEServer{
+ {
+ URLs: []string{"stun:stun.l.google.com:19302"},
+ },
+ },
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ // Set the handler for ICE connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnICEConnectionStateChange = func(connectionState ice.ConnectionState) {
+ fmt.Printf("Connection State has changed %s \n", connectionState.String())
+ }
+
+ // TODO: We have to send the offer otherwise it's empty.
+
+ d, err := peerConnection.CreateDataChannel("data", nil)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("New DataChannel %s %d\n", d.Label, d.ID)
+
+ d.Lock()
+ d.Onmessage = func(payload datachannel.Payload) {
+ switch p := payload.(type) {
+ case *datachannel.PayloadString:
+ fmt.Printf("Message '%s' from DataChannel '%s' payload '%s'\n", p.PayloadType().String(), d.Label, string(p.Data))
+ case *datachannel.PayloadBinary:
+ fmt.Printf("Message '%s' from DataChannel '%s' payload '% 02x'\n", p.PayloadType().String(), d.Label, p.Data)
+ default:
+ fmt.Printf("Message '%s' from DataChannel '%s' no payload \n", p.PayloadType().String(), d.Label)
+ }
+ }
+ d.Unlock()
+
+ // Set the remote SessionDescription
+ offer := webrtc.RTCSessionDescription{
+ Type: webrtc.RTCSdpTypeOffer,
+ Sdp: string(sd),
+ }
+ if err := peerConnection.SetRemoteDescription(offer); err != nil {
+ panic(err)
+ }
+
+ // Sets the LocalDescription, and starts our UDP listeners
+ answer, err := peerConnection.CreateAnswer(nil)
+ if err != nil {
+ panic(err)
+ }
+
+ // Get the LocalDescription and take it to base64 so we can paste in browser
+ fmt.Println(base64.StdEncoding.EncodeToString([]byte(answer.Sdp)))
+ fmt.Println("Random messages will now be sent to any connected DataChannels every 5 seconds")
+ for {
+ time.Sleep(5 * time.Second)
+ message := randSeq(15)
+ fmt.Printf("Sending %s \n", message)
+
+ err := d.Send(datachannel.PayloadString{Data: []byte(message)})
+ if err != nil {
+ panic(err)
+ }
+ }
+}
diff --git a/examples/data-channels/main.go b/examples/data-channels/main.go
index 08cec1f9..75103455 100644
--- a/examples/data-channels/main.go
+++ b/examples/data-channels/main.go
@@ -4,6 +4,7 @@ import (
"bufio"
"encoding/base64"
"fmt"
+ "io"
"math/rand"
"os"
"sync"
@@ -27,7 +28,7 @@ func randSeq(n int) string {
func main() {
reader := bufio.NewReader(os.Stdin)
rawSd, err := reader.ReadString('\n')
- if err != nil {
+ if err != nil && err != io.EOF {
panic(err)
}
diff --git a/internal/network/manager.go b/internal/network/manager.go
index b5eb217c..d81d0c88 100644
--- a/internal/network/manager.go
+++ b/internal/network/manager.go
@@ -270,3 +270,23 @@ func (m *Manager) iceOutboundHandler(raw []byte, local *stun.TransportAddr, remo
}
}
}
+
+func (m *Manager) SendOpenChannelMessage(streamIdentifier uint16, label string) error {
+ msg := &datachannel.ChannelOpen{
+ ChannelType: datachannel.ChannelTypeReliable,
+ Priority: datachannel.PriorityNormal,
+ ReliabilityParameter: 0,
+
+ Label: []byte(label),
+ Protocol: []byte(""),
+ }
+
+ rawMsg, err := msg.Marshal()
+ if err != nil {
+ return fmt.Errorf("Error Marshaling ChannelOpen %v", err)
+ }
+ if err = m.sctpAssociation.HandleOutbound(rawMsg, streamIdentifier, sctp.PayloadTypeWebRTCDCEP); err != nil {
+ return fmt.Errorf("Error sending ChannelOpen %v", err)
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/pkg/datachannel/message_channel_open.go b/pkg/datachannel/message_channel_open.go
index 9db609b7..1fb1903e 100644
--- a/pkg/datachannel/message_channel_open.go
+++ b/pkg/datachannel/message_channel_open.go
@@ -28,7 +28,7 @@ ChannelOpen represents a DATA_CHANNEL_OPEN Message
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
type ChannelOpen struct {
- ChannelType byte
+ ChannelType ChannelType
Priority uint16
ReliabilityParameter uint32
@@ -40,6 +40,43 @@ const (
channelOpenHeaderLength = 12
)
+type ChannelType byte
+
+const (
+ // ChannelTypeReliable determines the Data Channel provides a
+ // reliable in-order bi-directional communication.
+ ChannelTypeReliable = 0x00
+ // ChannelTypeReliableUnordered determines the Data Channel
+ // provides a reliable unordered bi-directional communication.
+ ChannelTypeReliableUnordered = 0x80
+ // ChannelTypePartialReliableRexmit determines the Data Channel
+ // provides a partially-reliable in-order bi-directional communication.
+ // User messages will not be retransmitted more times than specified in the Reliability Parameter.
+ ChannelTypePartialReliableRexmit = 0x01
+ // ChannelTypePartialReliableRexmitUnordered determines
+ // the Data Channel provides a partial reliable unordered bi-directional communication.
+ // User messages will not be retransmitted more times than specified in the Reliability Parameter.
+ ChannelTypePartialReliableRexmitUnordered = 0x81
+ // ChannelTypePartialReliableTimed determines the Data Channel
+ // provides a partial reliable in-order bi-directional communication.
+ // User messages might not be transmitted or retransmitted after
+ // a specified life-time given in milli- seconds in the Reliability Parameter.
+ // This life-time starts when providing the user message to the protocol stack.
+ ChannelTypePartialReliableTimed = 0x02
+ // The Data Channel provides a partial reliable unordered bi-directional
+ // communication. User messages might not be transmitted or retransmitted
+ // after a specified life-time given in milli- seconds in the Reliability Parameter.
+ // This life-time starts when providing the user message to the protocol stack.
+ ChannelTypePartialReliableTimedUnordered = 0x82
+)
+
+const (
+ PriorityBelowNormal uint16 = 128
+ PriorityNormal uint16 = 256
+ PriorityHigh uint16 = 512
+ PriorityExtraHigh uint16 = 1024
+)
+
// Marshal returns raw bytes for the given message
func (c *ChannelOpen) Marshal() ([]byte, error) {
labelLength := len(c.Label)
diff --git a/pkg/datachannel/message_test.go b/pkg/datachannel/message_test.go
index c7268691..786bd680 100644
--- a/pkg/datachannel/message_test.go
+++ b/pkg/datachannel/message_test.go
@@ -8,7 +8,7 @@ import (
func TestChannelOpenMarshal(t *testing.T) {
msg := ChannelOpen{
- ChannelType: 0,
+ ChannelType: ChannelTypeReliable,
Priority: 0,
ReliabilityParameter: 0,
@@ -18,13 +18,15 @@ func TestChannelOpenMarshal(t *testing.T) {
rawMsg, err := msg.Marshal()
if err != nil {
- t.Fatalf("Failed to marshal: %v", err)
+ t.Errorf("Failed to marshal: %v", err)
+ return
}
result := []byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72}
if len(rawMsg) != len(result) {
- t.Fatalf("%q != %q", rawMsg, result)
+ t.Errorf("%q != %q", rawMsg, result)
+ return
}
for i, v := range rawMsg {
@@ -39,12 +41,14 @@ func TestChannelAckMarshal(t *testing.T) {
msg := ChannelAck{}
rawMsg, err := msg.Marshal()
if err != nil {
- t.Fatalf("Failed to marshal: %v", err)
+ t.Errorf("Failed to marshal: %v", err)
+ return
}
result := []byte{0x02, 0x00, 0x00, 0x00}
if len(rawMsg) != len(result) {
- t.Fatalf("%q != %q", rawMsg, result)
+ t.Errorf("%q != %q", rawMsg, result)
+ return
}
for i, v := range rawMsg {
@@ -66,7 +70,7 @@ func TestChannelOpenUnmarshal(t *testing.T) {
if err != nil {
t.Error(errors.Wrap(err, "Unmarshal failed, ChannelOpen"))
- } else if msg.ChannelType != 0 {
+ } else if msg.ChannelType != ChannelTypeReliable {
t.Error(errors.Errorf("ChannelType should be 0"))
} else if msg.Priority != 0 {
t.Error(errors.Errorf("Priority should be 0"))
@@ -83,7 +87,8 @@ func TestChannelAckUnmarshal(t *testing.T) {
rawMsg := []byte{0x02}
msgUncast, err := Parse(rawMsg)
if err != nil {
- t.Fatalf("Failed to parse: %v", err)
+ t.Errorf("Failed to parse: %v", err)
+ return
}
_, ok := msgUncast.(*ChannelAck)
diff --git a/rtcdatachannel.go b/rtcdatachannel.go
index 3c138ebd..40df1d5a 100644
--- a/rtcdatachannel.go
+++ b/rtcdatachannel.go
@@ -51,13 +51,14 @@ func (p RTCPriorityType) String() string {
}
}
+// RTCDataChannelInit can be used to configure properties of the underlying channel such as data reliability.
type RTCDataChannelInit struct {
Ordered bool
MaxPacketLifeTime *uint16
MaxRetransmits *uint16
Protocol string
Negotiated bool
- Id uint16
+ ID uint16
Priority RTCPriorityType
}
@@ -82,11 +83,15 @@ func (r *RTCPeerConnection) CreateDataChannel(label string, options *RTCDataChan
negotiated = options.Negotiated
}
- var id uint16 = 0
+ var id uint16
if negotiated {
- id = options.Id
+ id = options.ID
} else {
- // TODO: generate id
+ var err error
+ id, err = r.generateDataChannelID(true) // TODO: base on DTLS role
+ if err != nil {
+ return nil, err
+ }
}
if id > 65534 {
@@ -98,20 +103,38 @@ func (r *RTCPeerConnection) CreateDataChannel(label string, options *RTCDataChan
return nil, &OperationError{Err: ErrMaxDataChannels}
}
- // TODO: Actually allocate datachannel
+ _ = ordered // TODO
+ _ = priority // TODO
res := &RTCDataChannel{
Label: label,
ID: id,
rtcPeerConnection: r,
}
- // TODO handle settings:
- _ = ordered
- _ = priority
+ // Remember datachannel
+ r.dataChannels[id] = res
+
+ // Send opening message
+ r.networkManager.SendOpenChannelMessage(id, label)
return res, nil
}
+func (r *RTCPeerConnection) generateDataChannelID(client bool) (uint16, error) {
+ var id uint16
+ if !client {
+ id++
+ }
+
+ for ; id < r.sctp.MaxChannels-1; id += 2 {
+ _, ok := r.dataChannels[id]
+ if !ok {
+ return id, nil
+ }
+ }
+ return 0, &OperationError{Err: ErrMaxDataChannels}
+}
+
// Send sends the passed message to the DataChannel peer
func (r *RTCDataChannel) Send(p datachannel.Payload) error {
if err := r.rtcPeerConnection.networkManager.SendDataChannelMessage(p, r.ID); err != nil {
diff --git a/rtcdatachannel_test.go b/rtcdatachannel_test.go
new file mode 100644
index 00000000..fc842545
--- /dev/null
+++ b/rtcdatachannel_test.go
@@ -0,0 +1,35 @@
+package webrtc
+
+import (
+ "testing"
+)
+
+func TestGenerateDataChannelID(t *testing.T) {
+ testCases := []struct {
+ client bool
+ c *RTCPeerConnection
+ result uint16
+ }{
+ {true, &RTCPeerConnection{sctp: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{}}, 0},
+ {true, &RTCPeerConnection{sctp: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{1: nil}}, 0},
+ {true, &RTCPeerConnection{sctp: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{0: nil}}, 2},
+ {true, &RTCPeerConnection{sctp: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{0: nil, 2: nil}}, 4},
+ {true, &RTCPeerConnection{sctp: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{0: nil, 4: nil}}, 2},
+ {false, &RTCPeerConnection{sctp: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{}}, 1},
+ {false, &RTCPeerConnection{sctp: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{0: nil}}, 1},
+ {false, &RTCPeerConnection{sctp: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{1: nil}}, 3},
+ {false, &RTCPeerConnection{sctp: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{1: nil, 3: nil}}, 5},
+ {false, &RTCPeerConnection{sctp: newRTCSctpTransport(), dataChannels: map[uint16]*RTCDataChannel{1: nil, 5: nil}}, 3},
+ }
+
+ for _, testCase := range testCases {
+ id, err := testCase.c.generateDataChannelID(testCase.client)
+ if err != nil {
+ t.Errorf("failed to generate id: %v", err)
+ return
+ }
+ if id != testCase.result {
+ t.Errorf("Wrong id: %d expected %d", id, testCase.result)
+ }
+ }
+}