-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.js
280 lines (244 loc) · 7.78 KB
/
index.js
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
var getRandomString = function(length) {
// Do not use Math.random().toString(32) for length control
var universe = 'abcdefghijklmnopqrstuvwxyz';
universe += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
universe += '0123456789';
var string = '';
for (var i = 0; i < length; ++i) {
string += universe[Math.floor((universe.length - 1) * Math.random())];
}
return string;
};
var getRTC = function() {
return {
RTCPeerConnection: (
window.RTCPeerConnection ||
window.msRTCPeerConnection ||
window.mozRTCPeerConnection ||
window.webkitRTCPeerConnection
),
RTCIceCandidate: (
window.RTCIceCandidate ||
window.msRTCIceCandidate ||
window.mozRTCIceCandidate ||
window.webkitRTCIceCandidate
),
RTCSessionDescription: (
window.RTCSessionDescription ||
window.msRTCSessionDescription ||
window.mozRTCSessionDescription ||
window.webkitRTCSessionDescription
),
};
};
export default class MRTC {
/* Minimal RTC Wrapper
*
* @param {Object={}} options They can be:
* {Object|Boolean} channel Does this peer have a DataChannel? If so, you can
* setup some custom config for it
* {MediaStream} stream The MediaStream object to be send to the other peer
* {Object={iceServers: []}} options RTCPeerConnection initialization options
*/
constructor(options={}) {
options.options = options.options || {iceServers: [
{
url: 'stun:23.21.150.121', // Old WebRTC API (url)
urls: [ // New WebRTC API (urls)
'stun:23.21.150.121',
'stun:stun.l.google.com:19302',
'stun:stun.services.mozilla.com',
],
},
]};
// Normalize dataChannel option into a object
if (options.dataChannel && typeof options.dataChannel === 'boolean') {
options.dataChannel = {};
}
this.stream = options.stream;
// Event System
this.events = {
signal: [],
};
// Has the remote offer/answer been set yet?
this._remoteSet = false;
// Ice candidates generated before remote description has been set
this._ices = [];
// Stream Events
this.events['add-stream'] = [];
// DataChannel Events
this.events['channel-open'] = [];
this.events['channel-message'] = [];
this.events['channel-close'] = [];
this.events['channel-error'] = [];
this.events['channel-buffered-amount-low'] = [];
// Holds signals if the user has not been hearing for the just yet
this._signals = [];
this.wrtc = options.wrtc || getRTC();
if (!this.wrtc.RTCPeerConnection) {
return console.error("No WebRTC support found");
}
this.peer = new this.wrtc.RTCPeerConnection(options.options);
this.peer.onicecandidate = event => {
// Nothing to do if no candidate is specified
if (!event.candidate) {
return;
}
return this._onSignal(event.candidate);
};
this.peer.ondatachannel = event => {
this.channel = event.channel;
this._bindChannel();
};
this.peer.onaddstream = event => {
this.stream = event.stream;
this.trigger('add-stream', [this.stream]);
};
if (this.stream) {
this.peer.addStream(options.stream);
}
if (options.offerer) {
if (options.dataChannel) {
this.channel = this.peer.createDataChannel(getRandomString(128), options.dataChannel);
this._bindChannel();
}
this.peer.createOffer(description => {
this.peer.setLocalDescription(description, () => {
return this._onSignal(description);
}, this.onError);
}, this.onError);
return;
}
}
/*
* Private
*/
/* Emit Ice candidates that were waiting for a remote description to be set */
_flushIces() {
this._remoteSet = true;
let ices = this._ices;
this._ices = [];
ices.forEach(function(ice) {
this.addSignal(ice);
}, this);
}
/* Bind all events related to dataChannel */
_bindChannel() {
['open', 'close', 'message', 'error', 'buffered-amount-low'].forEach(function(action) {
this.channel['on' + action.replace(/-/g, '')] = (...args) => {
this.trigger('channel-' + action, [...args]);
};
}, this);
}
/* Bubble signal events or accumulate then into an array */
_onSignal(signal) {
// Capture signals if the user has not been hearing for the just yet
if (this.events.signal.length === 0) {
return this._signals.push(signal);
}
// in case the user is already hearing for signal events fire it
this.trigger('signal', [signal]);
}
/*
* Misc
*/
/* Add a signal into the peer connection
*
* @param {RTCSessionDescription|RTCIceCandidate} The signalling data
*/
addSignal(signal) {
if (signal.type === 'offer') {
return this.peer.setRemoteDescription(new this.wrtc.RTCSessionDescription(signal), () => {
this._flushIces();
this.peer.createAnswer(description => {
this.peer.setLocalDescription(description, () => {
this._onSignal(description);
}, this.onError);
}, this.onError);
}, this.onError);
}
if (signal.type === 'answer') {
return this.peer.setRemoteDescription(new this.wrtc.RTCSessionDescription(signal), () => {
this._flushIces();
}, this.onError);
}
if (!this._remoteSet) {
return this._ices.push(signal);
}
this.peer.addIceCandidate(new this.wrtc.RTCIceCandidate(signal), () => {}, this.onError);
}
/*
* Event System
*/
/* Attach an event callback
*
* Event callbacks may be:
*
* signal -> A new signal is generated (may be either ice candidate or description)
*
* add-stream -> A new MediaSteam is received
*
* channel-open -> DataChannel connection is opened
* channel-message -> DataChannel is received
* channel-close -> DataChannel connection is closed
* channel-error -> DataChannel error ocurred
* channel-buffered-amount-low -> DataChannel bufferedAmount drops to less than
* or equal to bufferedAmountLowThreshold
*
* Multiple callbacks may be attached to a single event
*
* @param {String} action Which action will have a callback attached
* @param {Function} callback What will be executed when this event happen
*/
on(action, callback) {
// Tell the user if the action he has input was invalid
if (this.events[action] === undefined) {
return console.error(`MRTC: No such action '${action}'`);
}
this.events[action].push(callback);
// on Signal event is added, check the '_signals' array and flush it
if (action === 'signal') {
this._signals.forEach(function(signal) {
this.trigger('signal', [signal]);
}, this);
}
}
/* Detach an event callback
*
* @param {String} action Which action will have event(s) detached
* @param {Function} callback Which function will be detached. If none is
* provided all callbacks are detached
*/
off(action, callback) {
if (callback) {
// If a callback has been specified delete it specifically
var index = this.events[action].indexOf(callback);
(index !== -1) && this.events[action].splice(index, 1);
return index !== -1;
}
// Else just erase all callbacks
this.events[action] = [];
}
/* Trigger an event
*
* @param {String} action Which event will be triggered
* @param {Array} args Which arguments will be provided to the callbacks
*/
trigger(action, args) {
args = args || [];
// Fire all events with the given callback
this.events[action].forEach(function(callback) {
callback.apply(null, args);
});
}
/*
* Logging
*/
/* Log errors
*
* @param {Error} error Error to be logged
*/
onError(error) {
console.error(error);
}
}