Repository navigation
Expand file tree
/
Copy pathdataChannel.js
More file actions
320 lines (295 loc) · 9.56 KB
/
Copy pathdataChannel.js
File metadata and controls
320 lines (295 loc) · 9.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
const debug = require('debug')('datachannel')
const io = require('socket.io-client')
const getBrowserRtc = require('get-browser-rtc')
const { poll, getStringByteLength, emitAsync } = require('./util')
const noop = () => {}
const MAX_MESSAGE_SIZE = 64 * 1024
const HIGH_WATERMARK = 1024 * 1024
module.exports = function dataChannel (channelId, config, onData = noop) {
if (!channelId) {
throw new Error('channelId required')
}
if (!/^[0-9A-z-]+$/.test(channelId)) {
throw new Error('channelId must be alphanumeric (incl. minus)')
}
config = {
rtcConfig: {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
},
wrtc: getBrowserRtc(),
...config
}
if (!config.wrtc) {
throw new Error('no browser, config.wrtc required')
}
if (!config.signalServerUrl) {
throw new Error('config.signalServerUrl required')
}
const ID = channelId
const messageQueue = [] // of type {data, lengthInBytes, resolve, reject}
let channel, remoteMaxMessageSize
const connection = new config.wrtc.RTCPeerConnection(config.rtcConfig)
const socket = io(config.signalServerUrl)
return new Promise(async (resolve, reject) => {
// signal server connection
await new Promise((resolve, reject) => {
socket.on('connect', () => resolve())
socket.on('error', e => reject(new Error(e)))
})
const localId = socket.id
// one of the two peers gets this event in order to initiate setup
socket.on('initiate', async channelId => {
debug(channelId, localId, 'got initiate')
if (channelId !== ID) return
try {
channel = connection.createDataChannel(channelId, { ordered: true })
setupChannel()
} catch (e) {
debug(channelId, localId, 'creating channel failed', e)
return reject(e)
}
let offer
try {
offer = await connection.createOffer()
debug(channelId, localId, 'got offer')
} catch (e) {
debug(channelId, localId, 'creating offer failed', e)
return reject(e)
}
try {
connection.setLocalDescription(offer)
} catch (e) {
debug(channelId, localId, 'setting local description failed', e)
return reject(e)
}
socket.emit('signal', { channelId: ID, data: { offer } })
})
// listen to signal events
socket.on('signal', async ({ channelId, data }) => {
if (channelId !== ID) return
if (data.offer) {
debug(channelId, localId, 'got offer signal')
connection.setRemoteDescription(
new config.wrtc.RTCSessionDescription(data.offer)
)
extractRemoteMaxMessageSize(data.offer)
let answer
try {
answer = await connection.createAnswer()
} catch (e) {
debug(channelId, localId, 'creating answer failed', e)
return reject(e)
}
try {
connection.setLocalDescription(answer)
} catch (e) {
debug(channelId, localId, 'failed to set local description', e)
return reject(e)
}
try {
socket.emit('signal', { channelId: ID, data: { answer } })
} catch (e) {
debug(channelId, localId, 'failed to send answer', e)
return reject(e)
}
} else if (data.answer) {
debug(channelId, localId, 'got answer signal')
try {
connection.setRemoteDescription(
new config.wrtc.RTCSessionDescription(data.answer)
)
extractRemoteMaxMessageSize(data.answer)
} catch (e) {
debug(channelId, localId, 'failed to set remote description', e)
return reject(e)
}
} else if (data.candidate) {
debug(channelId, localId, 'got candidate signal')
try {
await connection.addIceCandidate(
new config.wrtc.RTCIceCandidate(data.candidate)
)
} catch (e) {
debug(channelId, localId, 'failed to add ICE candidate', e)
return reject(e)
}
} else {
debug(channelId, localId, 'got unknown signal', data)
}
})
connection.addEventListener('icecandidate', event => {
const candidate = event.candidate
debug(channelId, localId, 'got ICE candidate:', candidate)
if (candidate === null) {
return
}
socket.emit('signal', { channelId: ID, data: { candidate } })
})
connection.addEventListener('datachannel', event => {
debug(channelId, localId, 'received channel callback')
channel = event.channel
setupChannel()
})
try {
await emitAsync(socket, 'channel', ID)
} catch (e) {
debug(channelId, localId, 'joining datachannel failed', e)
reject(e)
}
try {
await poll(() => channel && channel.readyState === 'open', 10000)
} catch (e) {
debug(channelId, localId, 'no remote peer connected', e)
reject(new Error('connection timeout'))
}
resolve({
send (message) {
try {
debug(
channelId,
localId,
'bufferedAmount before send:',
channel.bufferedAmount
)
channel.send(message)
setTimeout(
// give chance to update bufferedAmount async
() =>
debug(
channelId,
localId,
'bufferedAmount after send:',
channel.bufferedAmount
),
0
)
return true
} catch (e) {
debug(channelId, localId, 'sending message failed', e)
debugState()
return false
}
},
sendAsync (message) {
const lengthInBytes = validateMessage(message)
return new Promise((resolve, reject) => {
messageQueue.push({ data: message, lengthInBytes, resolve, reject })
return sendAsyncInternal()
})
},
debugState () {
debugState()
}
})
})
async function sendAsyncInternal () {
const message = messageQueue.shift()
if (!message) {
debug('message queue is empty')
return
}
const { data, lengthInBytes, resolve, reject } = message
// TODO check connection state and maybe signaling state too
if (channel.readyState !== 'open') {
await poll(() => channel.readyState === 'open')
}
if (channel.readyState !== 'open') {
// TODO try to create a new datachannel
reject(new Error('datachannel is not open'))
}
// see sendData() in https://github.com/webrtc/samples/blob/gh-pages/src/content/datachannel/datatransfer/js/main.js
let bufferedAmount = channel.bufferedAmount
bufferedAmount += lengthInBytes
if (bufferedAmount >= HIGH_WATERMARK) {
// since event onbufferedamountlow is not fired, we retry after some delay
// 100 ms worked well during tests
setTimeout(() => sendAsyncInternal(), 100)
debug(
`did custom delay, bufferedAmount: ${bufferedAmount} (announced: ${
channel.bufferedAmount
})`
)
messageQueue.unshift(message) // back into the queue
return
}
try {
channel.send(data)
resolve()
} catch (e) {
reject(e)
}
}
function setupChannel () {
channel.binaryType = 'arraybuffer'
channel.bufferedAmountLowThreshold = MAX_MESSAGE_SIZE
channel.addEventListener('bufferedamountlow', e => {
// didn't see this fired, but leave it here, doesn't hurt
debug('bufferedamountlow event', e)
sendAsyncInternal()
})
// TODO set bufferedAmountLowThreshold and bufferedAmountLow callback in `open` handler (see sample)
channel.addEventListener('open', debugState)
channel.addEventListener('close', debugState)
channel.addEventListener('error', e => {
debug('channel error:', e)
debugState()
})
channel.addEventListener('message', message => {
onData(message.data)
})
}
function debugState () {
debug('state', {
channelId: channel.id,
// localDescription: connection.currentLocalDescription,
// remoteDescription: connection.currentRemoteDescription,
iceConnectionState: connection.iceConnectionState,
connectionState: connection.connectionState,
signalingState: connection.signalingState,
channelState: channel.readyState,
channelBufferedAmount: channel.bufferedAmount,
sctpMaxMessageSize: connection.sctp && connection.sctp.maxMessageSize,
remoteMaxMessageSize
})
}
// from: https://blog.mozilla.org/webrtc/large-data-channel-messages/
function extractRemoteMaxMessageSize (description) {
remoteMaxMessageSize = 65535
try {
const match = description.sdp.match(/a=max-message-size:\s*(\d+)/)
if (match !== null && match.length >= 2) {
remoteMaxMessageSize = parseInt(match[1])
debug('got remoteMaxMessageSize from sdp prop')
}
} catch (e) {
debug(
'failed to extract remoteMaxMessageSize from',
description,
'error:',
e
)
}
}
function validateMessage (msg) {
if (!msg) return
let msgLength
if (typeof msg === 'string') {
msgLength = getStringByteLength(msg)
} else if (
// checks for Buffer, ArrayBuffer and typed arrays
msg instanceof Buffer ||
msg instanceof ArrayBuffer ||
msg.buffer instanceof ArrayBuffer
) {
msgLength = msg.byteLength
} else {
throw new Error('unknown type of message')
}
if (msgLength > MAX_MESSAGE_SIZE) {
throw new Error(
`message too big, allowed are ${MAX_MESSAGE_SIZE} bytes, but message has ${msgLength} bytes`
)
}
return msgLength
}
}