├── .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 | [![Dependency Status](https://david-dm.org/nihey/mrtc.png)](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 | --------------------------------------------------------------------------------