├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── README.md
├── dist
├── mrtc.js
├── mrtc.min.js
└── mrtc.min.js.gz
├── docs
└── img
│ ├── a-and-b-connected.gif
│ ├── a-and-b-connected.png
│ ├── a-and-b-signalling-0.png
│ ├── a-and-b-signalling-1.png
│ ├── a-and-b-signalling-exchange-0.png
│ ├── a-and-b-signalling-exchange-1.png
│ ├── a-and-b-signalling-exchange-10.png
│ ├── a-and-b-signalling-exchange-2.png
│ ├── a-and-b-signalling-exchange-3.png
│ ├── a-and-b-signalling-exchange-4.png
│ ├── a-and-b-signalling-exchange-5.png
│ ├── a-and-b-signalling-exchange-6.png
│ ├── a-and-b-signalling-exchange-7.png
│ ├── a-and-b-signalling-exchange-8.png
│ ├── a-and-b-signalling-exchange-9.png
│ ├── a-and-b-signalling-exchange.gif
│ ├── a-and-b-signals-0.png
│ ├── a-and-b-signals-1.png
│ ├── a-and-b-signals-2.png
│ ├── a-and-b-signals.gif
│ └── a-and-b.png
├── index.js
├── package.json
├── test
└── test.js
└── webpack.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "indent": [
4 | 2,
5 | 2,
6 | {"VariableDeclarator": { "var": 2, "let": 2, "const": 3}}
7 | ],
8 | "linebreak-style": [2, "unix"],
9 | "semi": [2, "always"],
10 | "comma-dangle": [2, "always-multiline"],
11 | "no-console": [0],
12 | "no-console-log/no-console-log": 2,
13 | },
14 | "env": {
15 | "es6": true,
16 | "browser": true
17 | },
18 | "extends": "eslint:recommended",
19 | "ecmaFeatures": {
20 | modules: true,
21 | "experimentalObjectRestSpread": true
22 | },
23 | "globals": {
24 | "require": true,
25 | "module": true,
26 | "Environment": true,
27 | "$": true,
28 | "__dirname": false,
29 | "process": false,
30 | "describe": false,
31 | "it": false,
32 | },
33 | "plugins": [
34 | "no-console-log",
35 | ],
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # vim temporary files
2 | *.sw*
3 |
4 | # Dependencies
5 | node_modules/
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | test/
2 | docs/
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Minimal RTC Wrapper
2 |
3 | [](https://david-dm.org/nihey/mrtc)
4 |
5 | RTCPeerConnection Wrapper for purists. No dependencies (just ~1.5KB gzipped).
6 | ```
7 | $ npm install --save mrtc
8 | ```
9 |
10 | # Why Should I Use It?
11 |
12 | There are a lot of RTCPeerConnection wrappers around there, but a lot of them
13 | miss an important point: **Sometimes you don't need a lot of dependencies and
14 | 200+ commit repos to do your duty**.
15 |
16 | RTCPeerConnection API is a bit complicated, but not that complicated, this
17 | module wraps just what you need to do your signalling, establish a connection,
18 | and send [MediaStream/DataChannel] data - it also exposes to you all the native
19 | events of the RTCPeerConnection API.
20 |
21 | # What is RTC/WebRTC?
22 |
23 | > WebRTC is a open source project aiming to enable the web with Real Time
24 | > Communication (RTC) capabilities
25 |
26 | -[webrtc.org](http://www.webrtc.org/)
27 |
28 | Basically it allow you to make RTC between two browser peers (or a NodeJS peer,
29 | if you're using [node-wrtc](https://www.npmjs.com/package/wrtc)). Data is
30 | streamed between two peers without the need of a central server gateway.
31 |
32 | # How does WebRTC work?
33 |
34 | In a nutshell, considering two peers, A and B:
35 |
36 |
37 |
38 |
39 |
40 | In order to connect to each other they need to exchange some `data`, this `data`
41 | is called `signals`. Peer B needs `signals` to peer A to establish a connection
42 | (peer A also needs `signals` from peer B).
43 |
44 |
45 |
46 |
47 |
48 | These `signals` have to be transported somehow from peer A to peer B, and for
49 | this you need a `signalling server`.
50 |
51 |
52 |
53 |
54 |
55 | A `signalling server` can transport `signals` between peers by a series of
56 | means, whether it will be `[polling](http://stackoverflow.com/a/6835879)`,
57 | `[long-polling](http://techoctave.com/c7/posts/60-simple-long-polling-example-with-javascript-and-jquery/)`
58 | or, my personal favorite, `[websocket](https://davidwalsh.name/websocket)`.
59 |
60 |
61 |
62 |
63 |
64 | Once you found your way to transport these `signals` between them, peer A and
65 | peer B will be connected. That means you no longer need the `signalling server`,
66 | as data will be transported between these two peers directly.
67 |
68 |
69 |
70 |
71 |
72 | # How does MRTC Work?
73 |
74 | First, you must define your peers:
75 | ```javascript
76 | var MRTC = require('mrtc');
77 |
78 | var peerA = new MRTC({dataChannel: true, offerer: true});
79 | var peerB = new MRTC({dataChannel: true});
80 | ```
81 |
82 | Then you must listen for signal events:
83 | ```javascript
84 | peerA.on('signal', function(signal) {
85 | // Send this `signal` somehow to peerB
86 | });
87 |
88 | peerB.on('signal', function(signal) {
89 | // Send this `signal` somehow to peerA
90 | });
91 | ```
92 |
93 | When you managed to find a way to send the signal between the peers:
94 | ```javascript
95 | // A adds a signal from B
96 | peerA.addSignal(signalB);
97 |
98 | // The same for peerB
99 | peerB.addSignal(signalA);
100 | ```
101 |
102 | After trading all the signals, the peer connection will be established:
103 | ```javascript
104 | peerA.on('channel-open', function() {
105 | // Connected to B
106 | peerA.channel.send('Hey!!')
107 | });
108 |
109 | peerB.on('channel-open', function() {
110 | // Connected to A
111 | peerB.channel.send('Hello there!');
112 | });
113 | ```
114 |
115 | Data can be received via the `channel-message` event, all `channel-*` events
116 | are received as the [data channel event handling
117 | api](https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel#Event_Handlers):
118 | ```javascript
119 | peerA.on('channel-message', function(event) {
120 | console.log(event.data); // -> Hello there!
121 | });
122 | ```
123 |
124 | Streams are available via the `add-stream` event (but you must have initialized
125 | MRTC with a stream)
126 | ```javascript
127 | // If you had initialized MRTC like:
128 | //
129 | // navigator.getUserMedia({audio: true, video: true}, function(stream) {
130 | // var peerA = new MRTC({stream: stream});
131 | // });
132 |
133 | peerB.on('add-stream', function(stream) {
134 | // Received the peerA's stream
135 | });
136 | ```
137 |
138 | To use MRTC in a Node environment, you should use `[wrtc](https://www.npmjs.com/package/wrtc)`:
139 | ```javascript
140 | var MRTC = require('mrtc');
141 |
142 | var peerA = new MRTC({wrtc: require('wrtc'), dataChannel: true, offerer: true});
143 | ...
144 | ```
145 |
146 | # API
147 |
148 | ```javascript
149 | /* Minimal RTC Wrapper
150 | *
151 | * @param {Object={}} options They can be:
152 | * {Object|Boolean} dataChannel Does this peer have a DataChannel? If so,
153 | * you can setup some custom config for it
154 | * {MediaStream} stream The MediaStream object to be send to the other peer
155 | * {Object={iceServers: []}} options RTCPeerConnection initialization options
156 | */
157 | constructor(options={})
158 |
159 | // new MRTC({dataChannel: {ordered: false}});
160 | ```
161 |
162 | ```javascript
163 | /* Add a signal into the peer connection
164 | *
165 | * @param {RTCSessionDescription|RTCIceCandidate} The signalling data
166 | */
167 | addSignal(signal)
168 |
169 | // peer.addSignal(signal)
170 | ```
171 | ```javascript
172 | /* Attach an event callback
173 | *
174 | * Event callbacks may be:
175 | *
176 | * signal -> A new signal is generated (may be either ice candidate or description)
177 | *
178 | * add-stream -> A new MediaSteam is received
179 | *
180 | * channel-open -> DataChannel connection is opened
181 | * channel-message -> DataChannel is received
182 | * channel-close -> DataChannel connection is closed
183 | * channel-error -> DataChannel error ocurred
184 | * channel-buffered-amount-low -> DataChannel bufferedAmount drops to less than
185 | * or equal to bufferedAmountLowThreshold
186 | *
187 | * Multiple callbacks may be attached to a single event
188 | *
189 | * @param {String} action Which action will have a callback attached
190 | * @param {Function} callback What will be executed when this event happen
191 | */
192 | on(action, callback)
193 |
194 | // peer.on('channel-message', function(event) {
195 | // // Received something
196 | // });
197 | ```
198 | ```javascript
199 | /* Detach an event callback
200 | *
201 | * @param {String} action Which action will have event(s) detached
202 | * @param {Function} callback Which function will be detached. If none is
203 | * provided all callbacks are detached
204 | */
205 | off(action, callback)
206 |
207 | // peer.off('channel-message');
208 | ```
209 | ```javascript
210 | /* Trigger an event
211 | *
212 | * @param {String} action Which event will be triggered
213 | * @param {Array} args Which arguments will be provided to the callbacks
214 | */
215 | trigger(action, args)
216 |
217 | // peer.trigger('channel-message', [{data: 'Hello there'}]);
218 | ```
219 |
220 | # License
221 |
222 | This code is released under
223 | [CC0](http://creativecommons.org/publicdomain/zero/1.0/) (Public Domain)
224 |
--------------------------------------------------------------------------------
/dist/mrtc.js:
--------------------------------------------------------------------------------
1 | (function webpackUniversalModuleDefinition(root, factory) {
2 | if(typeof exports === 'object' && typeof module === 'object')
3 | module.exports = factory();
4 | else if(typeof define === 'function' && define.amd)
5 | define([], factory);
6 | else if(typeof exports === 'object')
7 | exports["module"] = factory();
8 | else
9 | root["module"] = factory();
10 | })(this, function() {
11 | return /******/ (function(modules) { // webpackBootstrap
12 | /******/ // The module cache
13 | /******/ var installedModules = {};
14 |
15 | /******/ // The require function
16 | /******/ function __webpack_require__(moduleId) {
17 |
18 | /******/ // Check if module is in cache
19 | /******/ if(installedModules[moduleId])
20 | /******/ return installedModules[moduleId].exports;
21 |
22 | /******/ // Create a new module (and put it into the cache)
23 | /******/ var module = installedModules[moduleId] = {
24 | /******/ exports: {},
25 | /******/ id: moduleId,
26 | /******/ loaded: false
27 | /******/ };
28 |
29 | /******/ // Execute the module function
30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
31 |
32 | /******/ // Flag the module as loaded
33 | /******/ module.loaded = true;
34 |
35 | /******/ // Return the exports of the module
36 | /******/ return module.exports;
37 | /******/ }
38 |
39 |
40 | /******/ // expose the modules object (__webpack_modules__)
41 | /******/ __webpack_require__.m = modules;
42 |
43 | /******/ // expose the module cache
44 | /******/ __webpack_require__.c = installedModules;
45 |
46 | /******/ // __webpack_public_path__
47 | /******/ __webpack_require__.p = "";
48 |
49 | /******/ // Load entry module and return exports
50 | /******/ return __webpack_require__(0);
51 | /******/ })
52 | /************************************************************************/
53 | /******/ ([
54 | /* 0 */
55 | /***/ function(module, exports) {
56 |
57 | 'use strict';
58 |
59 | Object.defineProperty(exports, '__esModule', {
60 | value: true
61 | });
62 |
63 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
64 |
65 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
66 |
67 | var getRandomString = function getRandomString(length) {
68 | // Do not use Math.random().toString(32) for length control
69 | var universe = 'abcdefghijklmnopqrstuvwxyz';
70 | universe += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
71 | universe += '0123456789';
72 |
73 | var string = '';
74 | for (var i = 0; i < length; ++i) {
75 | string += universe[Math.floor((universe.length - 1) * Math.random())];
76 | }
77 | return string;
78 | };
79 |
80 | var getRTC = function getRTC() {
81 | return {
82 | RTCPeerConnection: window.RTCPeerConnection || window.msRTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection,
83 | RTCIceCandidate: window.RTCIceCandidate || window.msRTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate,
84 | RTCSessionDescription: window.RTCSessionDescription || window.msRTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription
85 | };
86 | };
87 |
88 | var MRTC = (function () {
89 | /* Minimal RTC Wrapper
90 | *
91 | * @param {Object={}} options They can be:
92 | * {Object|Boolean} channel Does this peer have a DataChannel? If so, you can
93 | * setup some custom config for it
94 | * {MediaStream} stream The MediaStream object to be send to the other peer
95 | * {Object={iceServers: []}} options RTCPeerConnection initialization options
96 | */
97 |
98 | function MRTC() {
99 | var _this = this;
100 |
101 | var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
102 |
103 | _classCallCheck(this, MRTC);
104 |
105 | options.options = options.options || { iceServers: [{
106 | url: 'stun:23.21.150.121', // Old WebRTC API (url)
107 | urls: [// New WebRTC API (urls)
108 | 'stun:23.21.150.121', 'stun:stun.l.google.com:19302', 'stun:stun.services.mozilla.com']
109 | }] };
110 |
111 | // Normalize dataChannel option into a object
112 | if (options.dataChannel && typeof options.dataChannel === 'boolean') {
113 | options.dataChannel = {};
114 | }
115 |
116 | this.stream = options.stream;
117 |
118 | // Event System
119 | this.events = {
120 | signal: []
121 | };
122 |
123 | // Has the remote offer/answer been set yet?
124 | this._remoteSet = false;
125 | // Ice candidates generated before remote description has been set
126 | this._ices = [];
127 |
128 | // Stream Events
129 | this.events['add-stream'] = [];
130 |
131 | // DataChannel Events
132 | this.events['channel-open'] = [];
133 | this.events['channel-message'] = [];
134 | this.events['channel-close'] = [];
135 | this.events['channel-error'] = [];
136 | this.events['channel-buffered-amount-low'] = [];
137 |
138 | // Holds signals if the user has not been hearing for the just yet
139 | this._signals = [];
140 |
141 | this.wrtc = options.wrtc || getRTC();
142 | if (!this.wrtc.RTCPeerConnection) {
143 | return console.error("No WebRTC support found");
144 | }
145 |
146 | this.peer = new this.wrtc.RTCPeerConnection(options.options);
147 | this.peer.onicecandidate = function (event) {
148 | // Nothing to do if no candidate is specified
149 | if (!event.candidate) {
150 | return;
151 | }
152 |
153 | return _this._onSignal(event.candidate);
154 | };
155 |
156 | this.peer.ondatachannel = function (event) {
157 | _this.channel = event.channel;
158 | _this._bindChannel();
159 | };
160 |
161 | this.peer.onaddstream = function (event) {
162 | _this.stream = event.stream;
163 | _this.trigger('add-stream', [_this.stream]);
164 | };
165 |
166 | if (this.stream) {
167 | this.peer.addStream(options.stream);
168 | }
169 |
170 | if (options.offerer) {
171 | if (options.dataChannel) {
172 | this.channel = this.peer.createDataChannel(getRandomString(128), options.dataChannel);
173 | this._bindChannel();
174 | }
175 |
176 | this.peer.createOffer(function (description) {
177 | _this.peer.setLocalDescription(description, function () {
178 | return _this._onSignal(description);
179 | }, _this.onError);
180 | }, this.onError);
181 | return;
182 | }
183 | }
184 |
185 | /*
186 | * Private
187 | */
188 |
189 | /* Emit Ice candidates that were waiting for a remote description to be set */
190 |
191 | _createClass(MRTC, [{
192 | key: '_flushIces',
193 | value: function _flushIces() {
194 | this._remoteSet = true;
195 | var ices = this._ices;
196 | this._ices = [];
197 |
198 | ices.forEach(function (ice) {
199 | this.addSignal(ice);
200 | }, this);
201 | }
202 |
203 | /* Bind all events related to dataChannel */
204 | }, {
205 | key: '_bindChannel',
206 | value: function _bindChannel() {
207 | ['open', 'close', 'message', 'error', 'buffered-amount-low'].forEach(function (action) {
208 | var _this2 = this;
209 |
210 | this.channel['on' + action.replace(/-/g, '')] = function () {
211 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
212 | args[_key] = arguments[_key];
213 | }
214 |
215 | _this2.trigger('channel-' + action, [].concat(args));
216 | };
217 | }, this);
218 | }
219 |
220 | /* Bubble signal events or accumulate then into an array */
221 | }, {
222 | key: '_onSignal',
223 | value: function _onSignal(signal) {
224 | // Capture signals if the user has not been hearing for the just yet
225 | if (this.events.signal.length === 0) {
226 | return this._signals.push(signal);
227 | }
228 |
229 | // in case the user is already hearing for signal events fire it
230 | this.trigger('signal', [signal]);
231 | }
232 |
233 | /*
234 | * Misc
235 | */
236 |
237 | /* Add a signal into the peer connection
238 | *
239 | * @param {RTCSessionDescription|RTCIceCandidate} The signalling data
240 | */
241 | }, {
242 | key: 'addSignal',
243 | value: function addSignal(signal) {
244 | var _this3 = this;
245 |
246 | if (signal.type === 'offer') {
247 | return this.peer.setRemoteDescription(new this.wrtc.RTCSessionDescription(signal), function () {
248 | _this3._flushIces();
249 | _this3.peer.createAnswer(function (description) {
250 | _this3.peer.setLocalDescription(description, function () {
251 | _this3._onSignal(description);
252 | }, _this3.onError);
253 | }, _this3.onError);
254 | }, this.onError);
255 | }
256 | if (signal.type === 'answer') {
257 | return this.peer.setRemoteDescription(new this.wrtc.RTCSessionDescription(signal), function () {
258 | _this3._flushIces();
259 | }, this.onError);
260 | }
261 | if (!this._remoteSet) {
262 | return this._ices.push(signal);
263 | }
264 |
265 | this.peer.addIceCandidate(new this.wrtc.RTCIceCandidate(signal), function () {}, this.onError);
266 | }
267 |
268 | /*
269 | * Event System
270 | */
271 |
272 | /* Attach an event callback
273 | *
274 | * Event callbacks may be:
275 | *
276 | * signal -> A new signal is generated (may be either ice candidate or description)
277 | *
278 | * add-stream -> A new MediaSteam is received
279 | *
280 | * channel-open -> DataChannel connection is opened
281 | * channel-message -> DataChannel is received
282 | * channel-close -> DataChannel connection is closed
283 | * channel-error -> DataChannel error ocurred
284 | * channel-buffered-amount-low -> DataChannel bufferedAmount drops to less than
285 | * or equal to bufferedAmountLowThreshold
286 | *
287 | * Multiple callbacks may be attached to a single event
288 | *
289 | * @param {String} action Which action will have a callback attached
290 | * @param {Function} callback What will be executed when this event happen
291 | */
292 | }, {
293 | key: 'on',
294 | value: function on(action, callback) {
295 | // Tell the user if the action he has input was invalid
296 | if (this.events[action] === undefined) {
297 | return console.error('MRTC: No such action \'' + action + '\'');
298 | }
299 |
300 | this.events[action].push(callback);
301 |
302 | // on Signal event is added, check the '_signals' array and flush it
303 | if (action === 'signal') {
304 | this._signals.forEach(function (signal) {
305 | this.trigger('signal', [signal]);
306 | }, this);
307 | }
308 | }
309 |
310 | /* Detach an event callback
311 | *
312 | * @param {String} action Which action will have event(s) detached
313 | * @param {Function} callback Which function will be detached. If none is
314 | * provided all callbacks are detached
315 | */
316 | }, {
317 | key: 'off',
318 | value: function off(action, callback) {
319 | if (callback) {
320 | // If a callback has been specified delete it specifically
321 | var index = this.events[action].indexOf(callback);
322 | index !== -1 && this.events[action].splice(index, 1);
323 | return index !== -1;
324 | }
325 |
326 | // Else just erase all callbacks
327 | this.events[action] = [];
328 | }
329 |
330 | /* Trigger an event
331 | *
332 | * @param {String} action Which event will be triggered
333 | * @param {Array} args Which arguments will be provided to the callbacks
334 | */
335 | }, {
336 | key: 'trigger',
337 | value: function trigger(action, args) {
338 | args = args || [];
339 | // Fire all events with the given callback
340 | this.events[action].forEach(function (callback) {
341 | callback.apply(null, args);
342 | });
343 | }
344 |
345 | /*
346 | * Logging
347 | */
348 |
349 | /* Log errors
350 | *
351 | * @param {Error} error Error to be logged
352 | */
353 | }, {
354 | key: 'onError',
355 | value: function onError(error) {
356 | console.error(error);
357 | }
358 | }]);
359 |
360 | return MRTC;
361 | })();
362 |
363 | exports['default'] = MRTC;
364 | module.exports = exports['default'];
365 |
366 | /***/ }
367 | /******/ ])
368 | });
369 | ;
--------------------------------------------------------------------------------
/dist/mrtc.min.js:
--------------------------------------------------------------------------------
1 | !function(e,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.module=n():e.module=n()}(this,function(){return function(e){function n(i){if(t[i])return t[i].exports;var o=t[i]={exports:{},id:i,loaded:!1};return e[i].call(o.exports,o,o.exports,n),o.loaded=!0,o.exports}var t={};return n.m=e,n.c=t,n.p="",n(0)}([function(e,n){"use strict";function t(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(n,"__esModule",{value:!0});var i=function(){function e(e,n){for(var t=0;ti;++i)t+=n[Math.floor((n.length-1)*Math.random())];return t},r=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}},s=function(){function e(){var n=this,i=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];return t(this,e),i.options=i.options||{iceServers:[{url:"stun:23.21.150.121",urls:["stun:23.21.150.121","stun:stun.l.google.com:19302","stun:stun.services.mozilla.com"]}]},i.dataChannel&&"boolean"==typeof i.dataChannel&&(i.dataChannel={}),this.stream=i.stream,this.events={signal:[]},this._remoteSet=!1,this._ices=[],this.events["add-stream"]=[],this.events["channel-open"]=[],this.events["channel-message"]=[],this.events["channel-close"]=[],this.events["channel-error"]=[],this.events["channel-buffered-amount-low"]=[],this._signals=[],this.wrtc=i.wrtc||r(),this.wrtc.RTCPeerConnection?(this.peer=new this.wrtc.RTCPeerConnection(i.options),this.peer.onicecandidate=function(e){return e.candidate?n._onSignal(e.candidate):void 0},this.peer.ondatachannel=function(e){n.channel=e.channel,n._bindChannel()},this.peer.onaddstream=function(e){n.stream=e.stream,n.trigger("add-stream",[n.stream])},this.stream&&this.peer.addStream(i.stream),i.offerer?(i.dataChannel&&(this.channel=this.peer.createDataChannel(o(128),i.dataChannel),this._bindChannel()),void this.peer.createOffer(function(e){n.peer.setLocalDescription(e,function(){return n._onSignal(e)},n.onError)},this.onError)):void 0):console.error("No WebRTC support found")}return i(e,[{key:"_flushIces",value:function(){this._remoteSet=!0;var e=this._ices;this._ices=[],e.forEach(function(e){this.addSignal(e)},this)}},{key:"_bindChannel",value:function(){["open","close","message","error","buffered-amount-low"].forEach(function(e){var n=this;this.channel["on"+e.replace(/-/g,"")]=function(){for(var t=arguments.length,i=Array(t),o=0;t>o;o++)i[o]=arguments[o];n.trigger("channel-"+e,[].concat(i))}},this)}},{key:"_onSignal",value:function(e){return 0===this.events.signal.length?this._signals.push(e):void this.trigger("signal",[e])}},{key:"addSignal",value:function(e){var n=this;return"offer"===e.type?this.peer.setRemoteDescription(new this.wrtc.RTCSessionDescription(e),function(){n._flushIces(),n.peer.createAnswer(function(e){n.peer.setLocalDescription(e,function(){n._onSignal(e)},n.onError)},n.onError)},this.onError):"answer"===e.type?this.peer.setRemoteDescription(new this.wrtc.RTCSessionDescription(e),function(){n._flushIces()},this.onError):this._remoteSet?void this.peer.addIceCandidate(new this.wrtc.RTCIceCandidate(e),function(){},this.onError):this._ices.push(e)}},{key:"on",value:function(e,n){return void 0===this.events[e]?console.error("MRTC: No such action '"+e+"'"):(this.events[e].push(n),void("signal"===e&&this._signals.forEach(function(e){this.trigger("signal",[e])},this)))}},{key:"off",value:function(e,n){if(n){var t=this.events[e].indexOf(n);return-1!==t&&this.events[e].splice(t,1),-1!==t}this.events[e]=[]}},{key:"trigger",value:function(e,n){n=n||[],this.events[e].forEach(function(e){e.apply(null,n)})}},{key:"onError",value:function(e){console.error(e)}}]),e}();n["default"]=s,e.exports=n["default"]}])});
--------------------------------------------------------------------------------
/dist/mrtc.min.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/dist/mrtc.min.js.gz
--------------------------------------------------------------------------------
/docs/img/a-and-b-connected.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-connected.gif
--------------------------------------------------------------------------------
/docs/img/a-and-b-connected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-connected.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-0.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-1.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-exchange-0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-exchange-0.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-exchange-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-exchange-1.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-exchange-10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-exchange-10.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-exchange-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-exchange-2.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-exchange-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-exchange-3.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-exchange-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-exchange-4.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-exchange-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-exchange-5.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-exchange-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-exchange-6.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-exchange-7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-exchange-7.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-exchange-8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-exchange-8.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-exchange-9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-exchange-9.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signalling-exchange.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signalling-exchange.gif
--------------------------------------------------------------------------------
/docs/img/a-and-b-signals-0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signals-0.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signals-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signals-1.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signals-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signals-2.png
--------------------------------------------------------------------------------
/docs/img/a-and-b-signals.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b-signals.gif
--------------------------------------------------------------------------------
/docs/img/a-and-b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nihey/mrtc/3f273909d5eb625424ffab27809f1e72ce7014f0/docs/img/a-and-b.png
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var getRandomString = function(length) {
2 | // Do not use Math.random().toString(32) for length control
3 | var universe = 'abcdefghijklmnopqrstuvwxyz';
4 | universe += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
5 | universe += '0123456789';
6 |
7 | var string = '';
8 | for (var i = 0; i < length; ++i) {
9 | string += universe[Math.floor((universe.length - 1) * Math.random())];
10 | }
11 | return string;
12 | };
13 |
14 | var getRTC = function() {
15 | return {
16 | RTCPeerConnection: (
17 | window.RTCPeerConnection ||
18 | window.msRTCPeerConnection ||
19 | window.mozRTCPeerConnection ||
20 | window.webkitRTCPeerConnection
21 | ),
22 | RTCIceCandidate: (
23 | window.RTCIceCandidate ||
24 | window.msRTCIceCandidate ||
25 | window.mozRTCIceCandidate ||
26 | window.webkitRTCIceCandidate
27 | ),
28 | RTCSessionDescription: (
29 | window.RTCSessionDescription ||
30 | window.msRTCSessionDescription ||
31 | window.mozRTCSessionDescription ||
32 | window.webkitRTCSessionDescription
33 | ),
34 | };
35 | };
36 |
37 | export default class MRTC {
38 | /* Minimal RTC Wrapper
39 | *
40 | * @param {Object={}} options They can be:
41 | * {Object|Boolean} channel Does this peer have a DataChannel? If so, you can
42 | * setup some custom config for it
43 | * {MediaStream} stream The MediaStream object to be send to the other peer
44 | * {Object={iceServers: []}} options RTCPeerConnection initialization options
45 | */
46 | constructor(options={}) {
47 | options.options = options.options || {iceServers: [
48 | {
49 | url: 'stun:23.21.150.121', // Old WebRTC API (url)
50 | urls: [ // New WebRTC API (urls)
51 | 'stun:23.21.150.121',
52 | 'stun:stun.l.google.com:19302',
53 | 'stun:stun.services.mozilla.com',
54 | ],
55 | },
56 | ]};
57 |
58 | // Normalize dataChannel option into a object
59 | if (options.dataChannel && typeof options.dataChannel === 'boolean') {
60 | options.dataChannel = {};
61 | }
62 |
63 | this.stream = options.stream;
64 |
65 | // Event System
66 | this.events = {
67 | signal: [],
68 | };
69 |
70 | // Has the remote offer/answer been set yet?
71 | this._remoteSet = false;
72 | // Ice candidates generated before remote description has been set
73 | this._ices = [];
74 |
75 | // Stream Events
76 | this.events['add-stream'] = [];
77 |
78 | // DataChannel Events
79 | this.events['channel-open'] = [];
80 | this.events['channel-message'] = [];
81 | this.events['channel-close'] = [];
82 | this.events['channel-error'] = [];
83 | this.events['channel-buffered-amount-low'] = [];
84 |
85 | // Holds signals if the user has not been hearing for the just yet
86 | this._signals = [];
87 |
88 | this.wrtc = options.wrtc || getRTC();
89 | if (!this.wrtc.RTCPeerConnection) {
90 | return console.error("No WebRTC support found");
91 | }
92 |
93 | this.peer = new this.wrtc.RTCPeerConnection(options.options);
94 | this.peer.onicecandidate = event => {
95 | // Nothing to do if no candidate is specified
96 | if (!event.candidate) {
97 | return;
98 | }
99 |
100 | return this._onSignal(event.candidate);
101 | };
102 |
103 | this.peer.ondatachannel = event => {
104 | this.channel = event.channel;
105 | this._bindChannel();
106 | };
107 |
108 | this.peer.onaddstream = event => {
109 | this.stream = event.stream;
110 | this.trigger('add-stream', [this.stream]);
111 | };
112 |
113 | if (this.stream) {
114 | this.peer.addStream(options.stream);
115 | }
116 |
117 | if (options.offerer) {
118 | if (options.dataChannel) {
119 | this.channel = this.peer.createDataChannel(getRandomString(128), options.dataChannel);
120 | this._bindChannel();
121 | }
122 |
123 | this.peer.createOffer(description => {
124 | this.peer.setLocalDescription(description, () => {
125 | return this._onSignal(description);
126 | }, this.onError);
127 | }, this.onError);
128 | return;
129 | }
130 | }
131 |
132 | /*
133 | * Private
134 | */
135 |
136 | /* Emit Ice candidates that were waiting for a remote description to be set */
137 | _flushIces() {
138 | this._remoteSet = true;
139 | let ices = this._ices;
140 | this._ices = [];
141 |
142 | ices.forEach(function(ice) {
143 | this.addSignal(ice);
144 | }, this);
145 | }
146 |
147 | /* Bind all events related to dataChannel */
148 | _bindChannel() {
149 | ['open', 'close', 'message', 'error', 'buffered-amount-low'].forEach(function(action) {
150 | this.channel['on' + action.replace(/-/g, '')] = (...args) => {
151 | this.trigger('channel-' + action, [...args]);
152 | };
153 | }, this);
154 | }
155 |
156 | /* Bubble signal events or accumulate then into an array */
157 | _onSignal(signal) {
158 | // Capture signals if the user has not been hearing for the just yet
159 | if (this.events.signal.length === 0) {
160 | return this._signals.push(signal);
161 | }
162 |
163 | // in case the user is already hearing for signal events fire it
164 | this.trigger('signal', [signal]);
165 | }
166 |
167 | /*
168 | * Misc
169 | */
170 |
171 | /* Add a signal into the peer connection
172 | *
173 | * @param {RTCSessionDescription|RTCIceCandidate} The signalling data
174 | */
175 | addSignal(signal) {
176 | if (signal.type === 'offer') {
177 | return this.peer.setRemoteDescription(new this.wrtc.RTCSessionDescription(signal), () => {
178 | this._flushIces();
179 | this.peer.createAnswer(description => {
180 | this.peer.setLocalDescription(description, () => {
181 | this._onSignal(description);
182 | }, this.onError);
183 | }, this.onError);
184 | }, this.onError);
185 | }
186 | if (signal.type === 'answer') {
187 | return this.peer.setRemoteDescription(new this.wrtc.RTCSessionDescription(signal), () => {
188 | this._flushIces();
189 | }, this.onError);
190 | }
191 | if (!this._remoteSet) {
192 | return this._ices.push(signal);
193 | }
194 |
195 | this.peer.addIceCandidate(new this.wrtc.RTCIceCandidate(signal), () => {}, this.onError);
196 | }
197 |
198 | /*
199 | * Event System
200 | */
201 |
202 | /* Attach an event callback
203 | *
204 | * Event callbacks may be:
205 | *
206 | * signal -> A new signal is generated (may be either ice candidate or description)
207 | *
208 | * add-stream -> A new MediaSteam is received
209 | *
210 | * channel-open -> DataChannel connection is opened
211 | * channel-message -> DataChannel is received
212 | * channel-close -> DataChannel connection is closed
213 | * channel-error -> DataChannel error ocurred
214 | * channel-buffered-amount-low -> DataChannel bufferedAmount drops to less than
215 | * or equal to bufferedAmountLowThreshold
216 | *
217 | * Multiple callbacks may be attached to a single event
218 | *
219 | * @param {String} action Which action will have a callback attached
220 | * @param {Function} callback What will be executed when this event happen
221 | */
222 | on(action, callback) {
223 | // Tell the user if the action he has input was invalid
224 | if (this.events[action] === undefined) {
225 | return console.error(`MRTC: No such action '${action}'`);
226 | }
227 |
228 | this.events[action].push(callback);
229 |
230 | // on Signal event is added, check the '_signals' array and flush it
231 | if (action === 'signal') {
232 | this._signals.forEach(function(signal) {
233 | this.trigger('signal', [signal]);
234 | }, this);
235 | }
236 | }
237 |
238 | /* Detach an event callback
239 | *
240 | * @param {String} action Which action will have event(s) detached
241 | * @param {Function} callback Which function will be detached. If none is
242 | * provided all callbacks are detached
243 | */
244 | off(action, callback) {
245 | if (callback) {
246 | // If a callback has been specified delete it specifically
247 | var index = this.events[action].indexOf(callback);
248 | (index !== -1) && this.events[action].splice(index, 1);
249 | return index !== -1;
250 | }
251 |
252 | // Else just erase all callbacks
253 | this.events[action] = [];
254 | }
255 |
256 | /* Trigger an event
257 | *
258 | * @param {String} action Which event will be triggered
259 | * @param {Array} args Which arguments will be provided to the callbacks
260 | */
261 | trigger(action, args) {
262 | args = args || [];
263 | // Fire all events with the given callback
264 | this.events[action].forEach(function(callback) {
265 | callback.apply(null, args);
266 | });
267 | }
268 |
269 | /*
270 | * Logging
271 | */
272 |
273 | /* Log errors
274 | *
275 | * @param {Error} error Error to be logged
276 | */
277 | onError(error) {
278 | console.error(error);
279 | }
280 | }
281 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mrtc",
3 | "version": "0.1.2",
4 | "description": "RTCPeerConnection Wrapper for purists. No dependencies (just ~1.5KB gzipped).",
5 | "main": "./dist/mrtc.min.js",
6 | "scripts": {
7 | "build": "webpack && webpack -p --output-filename mrtc.min.js && gzip -c ./dist/mrtc.min.js > ./dist/mrtc.min.js.gz",
8 | "watch": "webpack -d --watch",
9 | "test": "eslint . && mocha"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/nihey/mrtc.git"
14 | },
15 | "keywords": [
16 | "wrtc",
17 | "peer",
18 | "webrtc",
19 | "RTCPeerConnection"
20 | ],
21 | "author": "Nihey Takizawa ",
22 | "license": "CC0-1.0",
23 | "bugs": {
24 | "url": "https://github.com/nihey/mrtc/issues"
25 | },
26 | "homepage": "https://github.com/nihey/mrtc",
27 | "devDependencies": {
28 | "babel-core": "^5.8.23",
29 | "babel-eslint": "^4.1.3",
30 | "babel-loader": "^5.3.2",
31 | "eslint": "^1.6.0",
32 | "eslint-plugin-no-console-log": "^1.0.0",
33 | "mocha": "^2.3.4",
34 | "webpack": "^1.12.1",
35 | "wrtc": "0.0.59"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | var wrtc = require('wrtc'),
2 | MRTC = require('../dist/mrtc.js');
3 |
4 | var assert = require('assert');
5 |
6 | describe('Minimal RTC Wrapper', function() {
7 | var peerA = new MRTC({wrtc: wrtc, dataChannel: true, offerer: true});
8 | var peerB = new MRTC({wrtc: wrtc});
9 |
10 | it('Only PeerA should have a dataChannel', function() {
11 | assert.equal(typeof peerA.channel, "object");
12 | assert.equal(typeof peerB.channel, "undefined");
13 | });
14 |
15 | it('Should be able to connect', function(done) {
16 | var calls = 0;
17 | // Try calling the done callback
18 | var tryDone = function() {
19 | calls += 1;
20 | if (calls === 2) {
21 | done();
22 | }
23 | };
24 |
25 | peerA.on('signal', function(signal) {
26 | peerB.addSignal(signal);
27 | });
28 |
29 | peerB.on('signal', function(signal) {
30 | peerA.addSignal(signal);
31 | });
32 |
33 | peerA.on('channel-open', function() {
34 | tryDone();
35 | });
36 |
37 | peerB.on('channel-open', function() {
38 | tryDone();
39 | });
40 | });
41 |
42 | it('Should be able to trade messages', function(done) {
43 | var calls = 0;
44 | // Try calling the done callback
45 | var tryDone = function() {
46 | calls += 1;
47 | if (calls === 6) {
48 | done();
49 | }
50 | };
51 |
52 | // Send message via JSON
53 | var send = function(peer, data) {
54 | peer.channel.send(JSON.stringify(data));
55 | };
56 |
57 | peerA.on('channel-message', function(event) {
58 | var data = JSON.parse(event.data);
59 | if (data.type === 1) {
60 | assert.equal(data.message, "Here's to you, Hon");
61 | return tryDone();
62 | } else if (data.type === 2) {
63 | assert.equal(data.message, 'Beasts all over the shop');
64 | return tryDone();
65 | } else if (data.type === 3) {
66 | assert.equal(data.message, 'They played us like a damn fiddle');
67 | return tryDone();
68 | }
69 |
70 | assert.equal(true, false);
71 | });
72 |
73 | peerB.on('channel-message', function(event) {
74 | var data = JSON.parse(event.data);
75 | if (data.type === 1) {
76 | assert.equal(data.message, "It's ours, we built it dammit");
77 | return tryDone();
78 | } else if (data.type === 2) {
79 | assert.equal(data.message, 'There is no place for me here...');
80 | return tryDone();
81 | } else if (data.type === 3) {
82 | assert.equal(data.message, 'I will choose the truth I like');
83 | return tryDone();
84 | }
85 |
86 | assert.equal(true, false);
87 | });
88 |
89 | send(peerB, {
90 | type: 1,
91 | message: "Here's to you, Hon",
92 | });
93 | send(peerB, {
94 | type: 2,
95 | message: 'Beasts all over the shop',
96 | });
97 | send(peerB, {
98 | type: 3,
99 | message: 'They played us like a damn fiddle',
100 | });
101 |
102 | send(peerA, {
103 | type: 1,
104 | message: "It's ours, we built it dammit",
105 | });
106 | send(peerA, {
107 | type: 2,
108 | message: 'There is no place for me here...',
109 | });
110 | send(peerA, {
111 | type: 3,
112 | message: 'I will choose the truth I like',
113 | });
114 | });
115 |
116 | it('Should be calling channel close', function(done) {
117 | var calls = 0;
118 | // Try calling the done callback
119 | var tryDone = function() {
120 | calls += 1;
121 | if (calls === 2) {
122 | done();
123 | }
124 | };
125 |
126 | peerA.on('channel-close', function() {
127 | tryDone();
128 | });
129 |
130 | peerB.on('channel-close', function() {
131 | tryDone();
132 | });
133 |
134 | peerB.channel.close();
135 | });
136 | });
137 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | module.exports = {
4 | entry: path.resolve(path.join(__dirname, '.', 'index.js')),
5 | output: {
6 | path: path.resolve(path.join(__dirname, '.', 'dist')),
7 | library: 'module',
8 | libraryTarget: 'umd',
9 | filename: 'mrtc.js',
10 | },
11 | module: {
12 | loaders: [{test: /\.js?$/, exclude: /(node_modules|bower_components)/, loader: 'babel'}],
13 | },
14 | };
15 |
--------------------------------------------------------------------------------