Firstly: to create any data channel, the peers need to exchange an SDP offer/answer that negotiates the properties of the SCTP connection used by all data channels. This doesn't happen by default; you must call createDataChannel
before calling createOffer
for the offer to contain this SCTP information (an "m=application" section in the SDP).
If you don't do this, the data channel state will be stuck forever at connecting
.
With that out of the way, there are two ways to negotiate a data channel between two peers:
In-band negotiation
This is what occurs by default, if the negotiated
field is not set to true
. One peer calls createDataChannel
, and the other connects to the ondatachannel
EventHandler
. How this works:
- Peer A calls
createDataChannel
.
- Normal offer/answer exchange occurs.
- Once the SCTP connection is up, a message is sent in-band from Peer A to Peer B to tell it about the data channel's existence.
- On Peer B, the
ondatachannel
EventHandler
is invoked with a new data channel, created from the in-band message. It has the same properties as the data channel created by Peer A, and now these data channels can be used to send data bidirectionally.
The advantage of this approach is that data channels can be created dynamically at any time, without the application needing to do additional signaling.
Out-of-band negotiation
Data channels can also be negotiated out-of-band. With this approach, instead of calling createDataChannel
on one side and listening for ondatachannel
on the other side, the application just calls createDataChannel
on both sides.
- Peer A calls
createDataChannel({negotiated: true, id: 0})
- Peer B also calls
createDataChannel({negotiated: true, id: 0})
.
- Normal offer/answer exchange occurs.
- Once the SCTP connection is up, the channels will instantly be usable (
readyState
will change to open
). They're matched up by the ID, which is the underlying SCTP stream ID.
The advantage of this approach is that, since no message needs to be sent in-band to create the data channel on Peer B, the channel is usable sooner. This also makes the application code simpler, since you don't even need to bother with ondatachannel
.
So, for applications that only use a fixed number of data channels, this approach is recommended.
Note that the ID you choose is not just an arbitrary value. It represents an underlying 0-based SCTP stream ID. And these IDs can only go as high as the number of SCTP streams negotiated by the WebRTC implementations. So, if you use an ID that's too high, your data channel won't work.
What about native applications?
If you're using the native webrtc library instead of the JS API, it works the same way; things just have different names.
C++:
PeerConnectionObserver::OnDataChannel
DataChannelInit::negotiated
DataChannelInit::id
Java:
PeerConnection.Observer.onDataChannel
DataChannel.Init.negotiated
DataChannel.Init.id
Obj-C:
RTCPeerConnectionDelegate::didOpenDataChannel
RTCDataChannelConfiguration::isNegotiated
RTCDataChannelConfiguration::channelId