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