datachannel: send OpenChannel message

This commit is contained in:
backkem
2018-08-11 15:27:55 +02:00
committed by Sean DuBois
parent b2467593d0
commit 8d6e30ec87
12 changed files with 336 additions and 17 deletions

View File

@@ -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

View File

@@ -0,0 +1 @@

View File

@@ -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

View File

@@ -0,0 +1,9 @@
Browser base64 Session Description <textarea id="localSessionDescription" readonly="true"></textarea> <br />
Golang base64 Session Description: <textarea id="remoteSessionDescription"></textarea> <br/>
<button onclick="window.startSession()"> Start Session </button> <br />
<br />
Message: <textarea id="message">This is my DataChannel message!</textarea> <br/>
<button onclick="window.sendMessage()"> Send Message </button> <br />
<div id="logs"></div>

View File

@@ -0,0 +1,42 @@
/* eslint-env browser */
let pc = new RTCPeerConnection()
let log = msg => {
document.getElementById('logs').innerHTML += msg + '<br>'
}
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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io"
"math/rand" "math/rand"
"os" "os"
"sync" "sync"
@@ -27,7 +28,7 @@ func randSeq(n int) string {
func main() { func main() {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
rawSd, err := reader.ReadString('\n') rawSd, err := reader.ReadString('\n')
if err != nil { if err != nil && err != io.EOF {
panic(err) panic(err)
} }

View File

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

View File

@@ -28,7 +28,7 @@ ChannelOpen represents a DATA_CHANNEL_OPEN Message
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/ */
type ChannelOpen struct { type ChannelOpen struct {
ChannelType byte ChannelType ChannelType
Priority uint16 Priority uint16
ReliabilityParameter uint32 ReliabilityParameter uint32
@@ -40,6 +40,43 @@ const (
channelOpenHeaderLength = 12 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 // Marshal returns raw bytes for the given message
func (c *ChannelOpen) Marshal() ([]byte, error) { func (c *ChannelOpen) Marshal() ([]byte, error) {
labelLength := len(c.Label) labelLength := len(c.Label)

View File

@@ -8,7 +8,7 @@ import (
func TestChannelOpenMarshal(t *testing.T) { func TestChannelOpenMarshal(t *testing.T) {
msg := ChannelOpen{ msg := ChannelOpen{
ChannelType: 0, ChannelType: ChannelTypeReliable,
Priority: 0, Priority: 0,
ReliabilityParameter: 0, ReliabilityParameter: 0,
@@ -18,13 +18,15 @@ func TestChannelOpenMarshal(t *testing.T) {
rawMsg, err := msg.Marshal() rawMsg, err := msg.Marshal()
if err != nil { 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} 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) { if len(rawMsg) != len(result) {
t.Fatalf("%q != %q", rawMsg, result) t.Errorf("%q != %q", rawMsg, result)
return
} }
for i, v := range rawMsg { for i, v := range rawMsg {
@@ -39,12 +41,14 @@ func TestChannelAckMarshal(t *testing.T) {
msg := ChannelAck{} msg := ChannelAck{}
rawMsg, err := msg.Marshal() rawMsg, err := msg.Marshal()
if err != nil { if err != nil {
t.Fatalf("Failed to marshal: %v", err) t.Errorf("Failed to marshal: %v", err)
return
} }
result := []byte{0x02, 0x00, 0x00, 0x00} result := []byte{0x02, 0x00, 0x00, 0x00}
if len(rawMsg) != len(result) { if len(rawMsg) != len(result) {
t.Fatalf("%q != %q", rawMsg, result) t.Errorf("%q != %q", rawMsg, result)
return
} }
for i, v := range rawMsg { for i, v := range rawMsg {
@@ -66,7 +70,7 @@ func TestChannelOpenUnmarshal(t *testing.T) {
if err != nil { if err != nil {
t.Error(errors.Wrap(err, "Unmarshal failed, ChannelOpen")) 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")) t.Error(errors.Errorf("ChannelType should be 0"))
} else if msg.Priority != 0 { } else if msg.Priority != 0 {
t.Error(errors.Errorf("Priority should be 0")) t.Error(errors.Errorf("Priority should be 0"))
@@ -83,7 +87,8 @@ func TestChannelAckUnmarshal(t *testing.T) {
rawMsg := []byte{0x02} rawMsg := []byte{0x02}
msgUncast, err := Parse(rawMsg) msgUncast, err := Parse(rawMsg)
if err != nil { if err != nil {
t.Fatalf("Failed to parse: %v", err) t.Errorf("Failed to parse: %v", err)
return
} }
_, ok := msgUncast.(*ChannelAck) _, ok := msgUncast.(*ChannelAck)

View File

@@ -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 { type RTCDataChannelInit struct {
Ordered bool Ordered bool
MaxPacketLifeTime *uint16 MaxPacketLifeTime *uint16
MaxRetransmits *uint16 MaxRetransmits *uint16
Protocol string Protocol string
Negotiated bool Negotiated bool
Id uint16 ID uint16
Priority RTCPriorityType Priority RTCPriorityType
} }
@@ -82,11 +83,15 @@ func (r *RTCPeerConnection) CreateDataChannel(label string, options *RTCDataChan
negotiated = options.Negotiated negotiated = options.Negotiated
} }
var id uint16 = 0 var id uint16
if negotiated { if negotiated {
id = options.Id id = options.ID
} else { } 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 { if id > 65534 {
@@ -98,20 +103,38 @@ func (r *RTCPeerConnection) CreateDataChannel(label string, options *RTCDataChan
return nil, &OperationError{Err: ErrMaxDataChannels} return nil, &OperationError{Err: ErrMaxDataChannels}
} }
// TODO: Actually allocate datachannel _ = ordered // TODO
_ = priority // TODO
res := &RTCDataChannel{ res := &RTCDataChannel{
Label: label, Label: label,
ID: id, ID: id,
rtcPeerConnection: r, rtcPeerConnection: r,
} }
// TODO handle settings: // Remember datachannel
_ = ordered r.dataChannels[id] = res
_ = priority
// Send opening message
r.networkManager.SendOpenChannelMessage(id, label)
return res, nil 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 // Send sends the passed message to the DataChannel peer
func (r *RTCDataChannel) Send(p datachannel.Payload) error { func (r *RTCDataChannel) Send(p datachannel.Payload) error {
if err := r.rtcPeerConnection.networkManager.SendDataChannelMessage(p, r.ID); err != nil { if err := r.rtcPeerConnection.networkManager.SendDataChannelMessage(p, r.ID); err != nil {

35
rtcdatachannel_test.go Normal file
View File

@@ -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)
}
}
}