├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── DataChannel.js ├── DataChannel.min.js ├── Gruntfile.js ├── README.md ├── auto-session-establishment.html ├── bower.json ├── dev ├── DataChannel.js ├── DataConnector.js ├── FileConverter.js ├── FileReceiver.js ├── FileSaver.js ├── FileSender.js ├── IceServersHandler.js ├── RTCPeerConnection.js ├── SocketConnector.js ├── TextReceiver.js ├── TextSender.js ├── externalIceServers.js ├── globals.js ├── head.js └── tail.js ├── index.html ├── package.json ├── server.js └── simple.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | 4 | # bower 5 | bower_components 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "devel": true, 6 | "eqeqeq": true, 7 | "forin": false, 8 | "globalstrict": true, 9 | "quotmark": "single", 10 | "undef": true, 11 | "globals": { 12 | "DataChannel": true, 13 | "io": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib-cov 3 | npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | install: npm install 5 | before_script: 6 | - npm install grunt-cli 7 | - npm install grunt 8 | - grunt 9 | matrix: 10 | fast_finish: true 11 | -------------------------------------------------------------------------------- /DataChannel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Last time updated: 2017-07-29 4:31:53 PM UTC 4 | 5 | // __________________ 6 | // DataChannel v1.0.0 7 | 8 | // Open-Sourced: https://github.com/muaz-khan/DataChannel 9 | 10 | // -------------------------------------------------- 11 | // Muaz Khan - www.MuazKhan.com 12 | // MIT License - www.WebRTC-Experiment.com/licence 13 | // -------------------------------------------------- 14 | 15 | (function() { 16 | 17 | window.DataChannel = function(channel, extras) { 18 | if (channel) { 19 | this.automatic = true; 20 | } 21 | 22 | this.channel = channel || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''); 23 | 24 | extras = extras || {}; 25 | 26 | var self = this; 27 | var dataConnector; 28 | var fileReceiver; 29 | var textReceiver; 30 | 31 | this.onmessage = function(message, userid) { 32 | console.debug(userid, 'sent message:', message); 33 | }; 34 | 35 | this.channels = {}; 36 | this.onopen = function(userid) { 37 | console.debug(userid, 'is connected with you.'); 38 | }; 39 | 40 | this.onclose = function(event) { 41 | console.error('data channel closed:', event); 42 | }; 43 | 44 | this.onerror = function(event) { 45 | console.error('data channel error:', event); 46 | }; 47 | 48 | // by default; received file will be auto-saved to disk 49 | this.autoSaveToDisk = true; 50 | this.onFileReceived = function(fileName) { 51 | console.debug('File <', fileName, '> received successfully.'); 52 | }; 53 | 54 | this.onFileSent = function(file) { 55 | console.debug('File <', file.name, '> sent successfully.'); 56 | }; 57 | 58 | this.onFileProgress = function(packets) { 59 | console.debug('<', packets.remaining, '> items remaining.'); 60 | }; 61 | 62 | function prepareInit(callback) { 63 | for (var extra in extras) { 64 | self[extra] = extras[extra]; 65 | } 66 | self.direction = self.direction || 'many-to-many'; 67 | if (self.userid) { 68 | window.userid = self.userid; 69 | } 70 | 71 | if (!self.openSignalingChannel) { 72 | if (typeof self.transmitRoomOnce === 'undefined') { 73 | self.transmitRoomOnce = true; 74 | } 75 | 76 | // socket.io over node.js: https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md 77 | self.openSignalingChannel = function(config) { 78 | config = config || {}; 79 | 80 | channel = config.channel || self.channel || 'default-channel'; 81 | var socket = new window.Firebase('https://' + (self.firebase || 'webrtc-experiment') + '.firebaseIO.com/' + channel); 82 | socket.channel = channel; 83 | 84 | socket.on('child_added', function(data) { 85 | config.onmessage(data.val()); 86 | }); 87 | 88 | socket.send = function(data) { 89 | this.push(data); 90 | }; 91 | 92 | if (!self.socket) { 93 | self.socket = socket; 94 | } 95 | 96 | if (channel !== self.channel || (self.isInitiator && channel === self.channel)) { 97 | socket.onDisconnect().remove(); 98 | } 99 | 100 | if (config.onopen) { 101 | setTimeout(config.onopen, 1); 102 | } 103 | 104 | return socket; 105 | }; 106 | 107 | if (!window.Firebase) { 108 | var script = document.createElement('script'); 109 | script.src = 'https://cdn.webrtc-experiment.com/firebase.js'; 110 | script.onload = callback; 111 | document.documentElement.appendChild(script); 112 | } else { 113 | callback(); 114 | } 115 | } else { 116 | callback(); 117 | } 118 | } 119 | 120 | function init() { 121 | if (self.config) { 122 | return; 123 | } 124 | 125 | self.config = { 126 | ondatachannel: function(room) { 127 | if (!dataConnector) { 128 | self.room = room; 129 | return; 130 | } 131 | 132 | var tempRoom = { 133 | id: room.roomToken, 134 | owner: room.broadcaster 135 | }; 136 | 137 | if (self.ondatachannel) { 138 | return self.ondatachannel(tempRoom); 139 | } 140 | 141 | if (self.joinedARoom) { 142 | return; 143 | } 144 | 145 | self.joinedARoom = true; 146 | 147 | self.join(tempRoom); 148 | }, 149 | onopen: function(userid, _channel) { 150 | self.onopen(userid, _channel); 151 | self.channels[userid] = { 152 | channel: _channel, 153 | send: function(data) { 154 | self.send(data, this.channel); 155 | } 156 | }; 157 | }, 158 | onmessage: function(data, userid) { 159 | if (IsDataChannelSupported && !data.size) { 160 | data = JSON.parse(data); 161 | } 162 | 163 | if (!IsDataChannelSupported) { 164 | if (data.userid === window.userid) { 165 | return; 166 | } 167 | 168 | data = data.message; 169 | } 170 | 171 | if (data.type === 'text') { 172 | textReceiver.receive(data, self.onmessage, userid); 173 | } else if (typeof data.maxChunks !== 'undefined') { 174 | fileReceiver.receive(data, self); 175 | } else { 176 | self.onmessage(data, userid); 177 | } 178 | }, 179 | onclose: function(event) { 180 | var myChannels = self.channels; 181 | var closedChannel = event.currentTarget; 182 | 183 | for (var userid in myChannels) { 184 | if (closedChannel === myChannels[userid].channel) { 185 | delete myChannels[userid]; 186 | } 187 | } 188 | 189 | self.onclose(event); 190 | }, 191 | openSignalingChannel: self.openSignalingChannel 192 | }; 193 | 194 | dataConnector = IsDataChannelSupported ? 195 | new DataConnector(self, self.config) : 196 | new SocketConnector(self.channel, self.config); 197 | 198 | fileReceiver = new FileReceiver(self); 199 | textReceiver = new TextReceiver(self); 200 | 201 | if (self.room) { 202 | self.config.ondatachannel(self.room); 203 | } 204 | } 205 | 206 | this.open = function(_channel) { 207 | self.joinedARoom = true; 208 | 209 | if (self.socket) { 210 | self.socket.onDisconnect().remove(); 211 | } else { 212 | self.isInitiator = true; 213 | } 214 | 215 | if (_channel) { 216 | self.channel = _channel; 217 | } 218 | 219 | prepareInit(function() { 220 | init(); 221 | if (IsDataChannelSupported) { 222 | dataConnector.createRoom(_channel); 223 | } 224 | }); 225 | }; 226 | 227 | this.connect = function(_channel) { 228 | if (_channel) { 229 | self.channel = _channel; 230 | } 231 | 232 | prepareInit(init); 233 | }; 234 | 235 | // manually join a room 236 | this.join = function(room) { 237 | if (!room.id || !room.owner) { 238 | throw 'Invalid room info passed.'; 239 | } 240 | 241 | if (!dataConnector) { 242 | init(); 243 | } 244 | 245 | if (!dataConnector.joinRoom) { 246 | return; 247 | } 248 | 249 | dataConnector.joinRoom({ 250 | roomToken: room.id, 251 | joinUser: room.owner 252 | }); 253 | }; 254 | 255 | this.send = function(data, _channel) { 256 | if (!data) { 257 | throw 'No file, data or text message to share.'; 258 | } 259 | 260 | if (typeof data.size !== 'undefined' && typeof data.type !== 'undefined') { 261 | FileSender.send({ 262 | file: data, 263 | channel: dataConnector, 264 | onFileSent: function(file) { 265 | self.onFileSent(file); 266 | }, 267 | onFileProgress: function(packets, uuid) { 268 | self.onFileProgress(packets, uuid); 269 | }, 270 | 271 | _channel: _channel, 272 | root: self 273 | }); 274 | 275 | return; 276 | } 277 | TextSender.send({ 278 | text: data, 279 | channel: dataConnector, 280 | _channel: _channel, 281 | root: self 282 | }); 283 | }; 284 | 285 | this.onleave = function(userid) { 286 | console.debug(userid, 'left!'); 287 | }; 288 | 289 | this.leave = this.eject = function(userid) { 290 | dataConnector.leave(userid, self.autoCloseEntireSession); 291 | }; 292 | 293 | this.openNewSession = function(isOpenNewSession, isNonFirebaseClient) { 294 | if (isOpenNewSession) { 295 | if (self.isNewSessionOpened) { 296 | return; 297 | } 298 | self.isNewSessionOpened = true; 299 | 300 | if (!self.joinedARoom) { 301 | self.open(); 302 | } 303 | } 304 | 305 | if (!isOpenNewSession || isNonFirebaseClient) { 306 | self.connect(); 307 | } 308 | 309 | if (!isNonFirebaseClient) { 310 | return; 311 | } 312 | 313 | // for non-firebase clients 314 | 315 | setTimeout(function() { 316 | self.openNewSession(true); 317 | }, 5000); 318 | }; 319 | 320 | if (typeof this.preferSCTP === 'undefined') { 321 | this.preferSCTP = isFirefox || chromeVersion >= 32 ? true : false; 322 | } 323 | 324 | if (typeof this.chunkSize === 'undefined') { 325 | this.chunkSize = isFirefox || chromeVersion >= 32 ? 13 * 1000 : 1000; // 1000 chars for RTP and 13000 chars for SCTP 326 | } 327 | 328 | if (typeof this.chunkInterval === 'undefined') { 329 | this.chunkInterval = isFirefox || chromeVersion >= 32 ? 100 : 500; // 500ms for RTP and 100ms for SCTP 330 | } 331 | 332 | if (self.automatic) { 333 | if (window.Firebase) { 334 | console.debug('checking presence of the room..'); 335 | new window.Firebase('https://' + (extras.firebase || self.firebase || 'muazkh') + '.firebaseIO.com/' + self.channel).once('value', function(data) { 336 | console.debug('room is present?', data.val() !== null); 337 | self.openNewSession(data.val() === null); 338 | }); 339 | } else { 340 | self.openNewSession(false, true); 341 | } 342 | } 343 | }; 344 | 345 | function DataConnector(root, config) { 346 | var self = {}; 347 | var that = this; 348 | 349 | self.userToken = (root.userid = root.userid || uniqueToken()).toString(); 350 | self.sockets = []; 351 | self.socketObjects = {}; 352 | 353 | var channels = '--'; 354 | var isbroadcaster = false; 355 | var isGetNewRoom = true; 356 | var rtcDataChannels = []; 357 | 358 | function newPrivateSocket(_config) { 359 | var socketConfig = { 360 | channel: _config.channel, 361 | onmessage: socketResponse, 362 | onopen: function() { 363 | if (isofferer && !peer) { 364 | initPeer(); 365 | } 366 | 367 | _config.socketIndex = socket.index = self.sockets.length; 368 | self.socketObjects[socketConfig.channel] = socket; 369 | self.sockets[_config.socketIndex] = socket; 370 | } 371 | }; 372 | 373 | socketConfig.callback = function(_socket) { 374 | socket = _socket; 375 | socketConfig.onopen(); 376 | }; 377 | 378 | var socket = root.openSignalingChannel(socketConfig); 379 | var isofferer = _config.isofferer; 380 | var gotstream; 381 | var inner = {}; 382 | var peer; 383 | 384 | var peerConfig = { 385 | onICE: function(candidate) { 386 | if (!socket) { 387 | return setTimeout(function() { 388 | peerConfig.onICE(candidate); 389 | }, 2000); 390 | } 391 | 392 | socket.send({ 393 | userToken: self.userToken, 394 | candidate: { 395 | sdpMLineIndex: candidate.sdpMLineIndex, 396 | candidate: JSON.stringify(candidate.candidate) 397 | } 398 | }); 399 | }, 400 | onopen: onChannelOpened, 401 | onmessage: function(event) { 402 | config.onmessage(event.data, _config.userid); 403 | }, 404 | onclose: config.onclose, 405 | onerror: root.onerror, 406 | preferSCTP: root.preferSCTP 407 | }; 408 | 409 | function initPeer(offerSDP) { 410 | if (root.direction === 'one-to-one' && window.isFirstConnectionOpened) { 411 | return; 412 | } 413 | 414 | if (!offerSDP) { 415 | peerConfig.onOfferSDP = sendsdp; 416 | } else { 417 | peerConfig.offerSDP = offerSDP; 418 | peerConfig.onAnswerSDP = sendsdp; 419 | } 420 | 421 | peer = new RTCPeerConnection(peerConfig); 422 | } 423 | 424 | function onChannelOpened(channel) { 425 | channel.peer = peer.peer; 426 | rtcDataChannels.push(channel); 427 | 428 | config.onopen(_config.userid, channel); 429 | 430 | if (root.direction === 'many-to-many' && isbroadcaster && channels.split('--').length > 3 && defaultSocket) { 431 | defaultSocket.send({ 432 | newParticipant: socket.channel, 433 | userToken: self.userToken 434 | }); 435 | } 436 | 437 | window.isFirstConnectionOpened = gotstream = true; 438 | } 439 | 440 | function sendsdp(sdp) { 441 | sdp = JSON.stringify(sdp); 442 | var part = parseInt(sdp.length / 3); 443 | 444 | var firstPart = sdp.slice(0, part), 445 | secondPart = sdp.slice(part, sdp.length - 1), 446 | thirdPart = ''; 447 | 448 | if (sdp.length > part + part) { 449 | secondPart = sdp.slice(part, part + part); 450 | thirdPart = sdp.slice(part + part, sdp.length); 451 | } 452 | 453 | socket.send({ 454 | userToken: self.userToken, 455 | firstPart: firstPart 456 | }); 457 | 458 | socket.send({ 459 | userToken: self.userToken, 460 | secondPart: secondPart 461 | }); 462 | 463 | socket.send({ 464 | userToken: self.userToken, 465 | thirdPart: thirdPart 466 | }); 467 | } 468 | 469 | function socketResponse(response) { 470 | if (response.userToken === self.userToken) { 471 | return; 472 | } 473 | 474 | if (response.firstPart || response.secondPart || response.thirdPart) { 475 | if (response.firstPart) { 476 | // sdp sender's user id passed over "onopen" method 477 | _config.userid = response.userToken; 478 | 479 | inner.firstPart = response.firstPart; 480 | if (inner.secondPart && inner.thirdPart) { 481 | selfInvoker(); 482 | } 483 | } 484 | if (response.secondPart) { 485 | inner.secondPart = response.secondPart; 486 | if (inner.firstPart && inner.thirdPart) { 487 | selfInvoker(); 488 | } 489 | } 490 | 491 | if (response.thirdPart) { 492 | inner.thirdPart = response.thirdPart; 493 | if (inner.firstPart && inner.secondPart) { 494 | selfInvoker(); 495 | } 496 | } 497 | } 498 | 499 | if (response.candidate && !gotstream && peer) { 500 | if (!inner.firstPart || !inner.secondPart || !inner.thirdPart) { 501 | return setTimeout(function() { 502 | socketResponse(response); 503 | }, 400); 504 | } 505 | 506 | peer.addICE({ 507 | sdpMLineIndex: response.candidate.sdpMLineIndex, 508 | candidate: JSON.parse(response.candidate.candidate) 509 | }); 510 | 511 | console.debug('ice candidate', response.candidate.candidate); 512 | } 513 | 514 | if (response.left) { 515 | if (peer && peer.peer) { 516 | peer.peer.close(); 517 | peer.peer = null; 518 | } 519 | 520 | if (response.closeEntireSession) { 521 | leaveChannels(); 522 | } else if (socket) { 523 | socket.send({ 524 | left: true, 525 | userToken: self.userToken 526 | }); 527 | socket = null; 528 | } 529 | 530 | root.onleave(response.userToken); 531 | } 532 | 533 | if (response.playRoleOfBroadcaster) { 534 | setTimeout(function() { 535 | self.roomToken = response.roomToken; 536 | root.open(self.roomToken); 537 | self.sockets = swap(self.sockets); 538 | }, 600); 539 | } 540 | } 541 | 542 | var invokedOnce = false; 543 | 544 | function selfInvoker() { 545 | if (invokedOnce) { 546 | return; 547 | } 548 | 549 | invokedOnce = true; 550 | inner.sdp = JSON.parse(inner.firstPart + inner.secondPart + inner.thirdPart); 551 | 552 | if (isofferer) { 553 | peer.addAnswerSDP(inner.sdp); 554 | } else { 555 | initPeer(inner.sdp); 556 | } 557 | 558 | console.debug('sdp', inner.sdp.sdp); 559 | } 560 | } 561 | 562 | function onNewParticipant(channel) { 563 | if (!channel || channels.indexOf(channel) !== -1 || channel === self.userToken) { 564 | return; 565 | } 566 | 567 | channels += channel + '--'; 568 | 569 | var newChannel = uniqueToken(); 570 | 571 | newPrivateSocket({ 572 | channel: newChannel, 573 | closeSocket: true 574 | }); 575 | 576 | if (!defaultSocket) { 577 | return; 578 | } 579 | 580 | defaultSocket.send({ 581 | participant: true, 582 | userToken: self.userToken, 583 | joinUser: channel, 584 | channel: newChannel 585 | }); 586 | } 587 | 588 | function uniqueToken() { 589 | return (Math.round(Math.random() * 60535) + 5000000).toString(); 590 | } 591 | 592 | function leaveChannels(channel) { 593 | var alert = { 594 | left: true, 595 | userToken: self.userToken 596 | }; 597 | 598 | var socket; 599 | 600 | // if room initiator is leaving the room; close the entire session 601 | if (isbroadcaster) { 602 | if (root.autoCloseEntireSession) { 603 | alert.closeEntireSession = true; 604 | } else { 605 | self.sockets[0].send({ 606 | playRoleOfBroadcaster: true, 607 | userToken: self.userToken, 608 | roomToken: self.roomToken 609 | }); 610 | } 611 | } 612 | 613 | if (!channel) { 614 | // closing all sockets 615 | var sockets = self.sockets, 616 | length = sockets.length; 617 | 618 | for (var i = 0; i < length; i++) { 619 | socket = sockets[i]; 620 | if (socket) { 621 | socket.send(alert); 622 | 623 | if (self.socketObjects[socket.channel]) { 624 | delete self.socketObjects[socket.channel]; 625 | } 626 | 627 | delete sockets[i]; 628 | } 629 | } 630 | 631 | that.left = true; 632 | } 633 | 634 | // eject a specific user! 635 | if (channel) { 636 | socket = self.socketObjects[channel]; 637 | if (socket) { 638 | socket.send(alert); 639 | 640 | if (self.sockets[socket.index]) { 641 | delete self.sockets[socket.index]; 642 | } 643 | 644 | delete self.socketObjects[channel]; 645 | } 646 | } 647 | self.sockets = swap(self.sockets); 648 | } 649 | 650 | window.addEventListener('beforeunload', function() { 651 | leaveChannels(); 652 | }, false); 653 | 654 | window.addEventListener('keydown', function(e) { 655 | if (e.keyCode === 116) { 656 | leaveChannels(); 657 | } 658 | }, false); 659 | 660 | var defaultSocket = root.openSignalingChannel({ 661 | onmessage: function(response) { 662 | if (response.userToken === self.userToken) { 663 | return; 664 | } 665 | 666 | if (isGetNewRoom && response.roomToken && response.broadcaster) { 667 | config.ondatachannel(response); 668 | } 669 | 670 | if (response.newParticipant) { 671 | onNewParticipant(response.newParticipant); 672 | } 673 | 674 | if (response.userToken && response.joinUser === self.userToken && response.participant && channels.indexOf(response.userToken) === -1) { 675 | channels += response.userToken + '--'; 676 | 677 | console.debug('Data connection is being opened between you and', response.userToken || response.channel); 678 | newPrivateSocket({ 679 | isofferer: true, 680 | channel: response.channel || response.userToken, 681 | closeSocket: true 682 | }); 683 | } 684 | }, 685 | callback: function(socket) { 686 | defaultSocket = socket; 687 | } 688 | }); 689 | 690 | return { 691 | createRoom: function(roomToken) { 692 | self.roomToken = (roomToken || uniqueToken()).toString(); 693 | 694 | isbroadcaster = true; 695 | isGetNewRoom = false; 696 | 697 | (function transmit() { 698 | if (defaultSocket) { 699 | defaultSocket.send({ 700 | roomToken: self.roomToken, 701 | broadcaster: self.userToken 702 | }); 703 | } 704 | 705 | if (!root.transmitRoomOnce && !that.leaving) { 706 | if (root.direction === 'one-to-one') { 707 | if (!window.isFirstConnectionOpened) { 708 | setTimeout(transmit, 3000); 709 | } 710 | } else { 711 | setTimeout(transmit, 3000); 712 | } 713 | } 714 | })(); 715 | }, 716 | joinRoom: function(_config) { 717 | self.roomToken = _config.roomToken; 718 | isGetNewRoom = false; 719 | 720 | newPrivateSocket({ 721 | channel: self.userToken 722 | }); 723 | 724 | defaultSocket.send({ 725 | participant: true, 726 | userToken: self.userToken, 727 | joinUser: _config.joinUser 728 | }); 729 | }, 730 | send: function(message, _channel) { 731 | var _channels = rtcDataChannels; 732 | var data; 733 | var length = _channels.length; 734 | 735 | if (!length) { 736 | return; 737 | } 738 | 739 | data = JSON.stringify(message); 740 | 741 | if (_channel) { 742 | if (_channel.readyState === 'open') { 743 | _channel.send(data); 744 | } 745 | return; 746 | } 747 | for (var i = 0; i < length; i++) { 748 | if (_channels[i].readyState === 'open') { 749 | _channels[i].send(data); 750 | } 751 | } 752 | }, 753 | leave: function(userid, autoCloseEntireSession) { 754 | if (autoCloseEntireSession) { 755 | root.autoCloseEntireSession = true; 756 | } 757 | leaveChannels(userid); 758 | if (!userid) { 759 | self.joinedARoom = isbroadcaster = false; 760 | isGetNewRoom = true; 761 | } 762 | } 763 | }; 764 | } 765 | 766 | var moz = !!navigator.mozGetUserMedia; 767 | var IsDataChannelSupported = !((moz && !navigator.mozGetUserMedia) || (!moz && !navigator.webkitGetUserMedia)); 768 | 769 | function getRandomString() { 770 | return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, '-'); 771 | } 772 | 773 | var userid = getRandomString(); 774 | 775 | var isMobileDevice = navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i); 776 | var isChrome = !!navigator.webkitGetUserMedia; 777 | var isFirefox = !!navigator.mozGetUserMedia; 778 | 779 | var chromeVersion = 50; 780 | if (isChrome) { 781 | chromeVersion = !!navigator.mozGetUserMedia ? 0 : parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]); 782 | } 783 | 784 | function swap(arr) { 785 | var swapped = []; 786 | var length = arr.length; 787 | 788 | for (var i = 0; i < length; i++) { 789 | if (arr[i]) { 790 | swapped.push(arr[i]); 791 | } 792 | } 793 | 794 | return swapped; 795 | } 796 | 797 | function listenEventHandler(eventName, eventHandler) { 798 | window.removeEventListener(eventName, eventHandler); 799 | window.addEventListener(eventName, eventHandler, false); 800 | } 801 | 802 | // IceServersHandler.js 803 | 804 | var IceServersHandler = (function() { 805 | function getIceServers(connection) { 806 | var iceServers = []; 807 | 808 | iceServers.push(getSTUNObj('stun:stun.l.google.com:19302')); 809 | 810 | iceServers.push(getTURNObj('stun:webrtcweb.com:7788', 'muazkh', 'muazkh')); // coTURN 811 | iceServers.push(getTURNObj('turn:webrtcweb.com:7788', 'muazkh', 'muazkh')); // coTURN 812 | iceServers.push(getTURNObj('turn:webrtcweb.com:8877', 'muazkh', 'muazkh')); // coTURN 813 | 814 | iceServers.push(getTURNObj('turns:webrtcweb.com:7788', 'muazkh', 'muazkh')); // coTURN 815 | iceServers.push(getTURNObj('turns:webrtcweb.com:8877', 'muazkh', 'muazkh')); // coTURN 816 | 817 | // iceServers.push(getTURNObj('turn:webrtcweb.com:3344', 'muazkh', 'muazkh')); // resiprocate 818 | // iceServers.push(getTURNObj('turn:webrtcweb.com:4433', 'muazkh', 'muazkh')); // resiprocate 819 | 820 | // check if restund is still active: http://webrtcweb.com:4050/ 821 | iceServers.push(getTURNObj('stun:webrtcweb.com:4455', 'muazkh', 'muazkh')); // restund 822 | iceServers.push(getTURNObj('turn:webrtcweb.com:4455', 'muazkh', 'muazkh')); // restund 823 | iceServers.push(getTURNObj('turn:webrtcweb.com:5544?transport=tcp', 'muazkh', 'muazkh')); // restund 824 | 825 | return iceServers; 826 | } 827 | 828 | function getSTUNObj(stunStr) { 829 | var urlsParam = 'urls'; 830 | if (typeof isPluginRTC !== 'undefined') { 831 | urlsParam = 'url'; 832 | } 833 | 834 | var obj = {}; 835 | obj[urlsParam] = stunStr; 836 | return obj; 837 | } 838 | 839 | function getTURNObj(turnStr, username, credential) { 840 | var urlsParam = 'urls'; 841 | if (typeof isPluginRTC !== 'undefined') { 842 | urlsParam = 'url'; 843 | } 844 | 845 | var obj = { 846 | username: username, 847 | credential: credential 848 | }; 849 | obj[urlsParam] = turnStr; 850 | return obj; 851 | } 852 | 853 | return { 854 | getIceServers: getIceServers 855 | }; 856 | })(); 857 | 858 | function RTCPeerConnection(options) { 859 | var w = window; 860 | var PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection; 861 | var SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription; 862 | var IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; 863 | 864 | var iceServers = { 865 | iceServers: IceServersHandler.getIceServers() 866 | }; 867 | 868 | var optional = { 869 | optional: [] 870 | }; 871 | 872 | if (!navigator.onLine) { 873 | iceServers = null; 874 | console.warn('No internet connection detected. No STUN/TURN server is used to make sure local/host candidates are used for peers connection.'); 875 | } 876 | 877 | var peerConnection = new PeerConnection(iceServers, optional); 878 | 879 | openOffererChannel(); 880 | peerConnection.onicecandidate = onicecandidate; 881 | 882 | function onicecandidate(event) { 883 | if (!event.candidate || !peerConnection) { 884 | return; 885 | } 886 | 887 | if (options.onICE) { 888 | options.onICE(event.candidate); 889 | } 890 | } 891 | 892 | var constraints = options.constraints || { 893 | optional: [], 894 | mandatory: { 895 | OfferToReceiveAudio: false, 896 | OfferToReceiveVideo: false 897 | } 898 | }; 899 | 900 | function onSdpError(e) { 901 | var message = JSON.stringify(e, null, '\t'); 902 | 903 | if (message.indexOf('RTP/SAVPF Expects at least 4 fields') !== -1) { 904 | message = 'It seems that you are trying to interop RTP-datachannels with SCTP. It is not supported!'; 905 | } 906 | 907 | console.error('onSdpError:', message); 908 | } 909 | 910 | function onSdpSuccess() {} 911 | 912 | function createOffer() { 913 | if (!options.onOfferSDP) { 914 | return; 915 | } 916 | 917 | peerConnection.createOffer(function(sessionDescription) { 918 | peerConnection.setLocalDescription(sessionDescription); 919 | options.onOfferSDP(sessionDescription); 920 | }, onSdpError, constraints); 921 | } 922 | 923 | function createAnswer() { 924 | if (!options.onAnswerSDP) { 925 | return; 926 | } 927 | 928 | options.offerSDP = new SessionDescription(options.offerSDP); 929 | peerConnection.setRemoteDescription(options.offerSDP, onSdpSuccess, onSdpError); 930 | 931 | peerConnection.createAnswer(function(sessionDescription) { 932 | peerConnection.setLocalDescription(sessionDescription); 933 | options.onAnswerSDP(sessionDescription); 934 | }, onSdpError, constraints); 935 | } 936 | 937 | if (!moz) { 938 | createOffer(); 939 | createAnswer(); 940 | } 941 | 942 | var channel; 943 | 944 | function openOffererChannel() { 945 | if (moz && !options.onOfferSDP) { 946 | return; 947 | } 948 | 949 | if (!moz && !options.onOfferSDP) { 950 | return; 951 | } 952 | 953 | _openOffererChannel(); 954 | if (moz) { 955 | createOffer(); 956 | } 957 | } 958 | 959 | function _openOffererChannel() { 960 | // protocol: 'text/chat', preset: true, stream: 16 961 | // maxRetransmits:0 && ordered:false 962 | var dataChannelDict = {}; 963 | 964 | console.debug('dataChannelDict', dataChannelDict); 965 | 966 | channel = peerConnection.createDataChannel('channel', dataChannelDict); 967 | setChannelEvents(); 968 | } 969 | 970 | function setChannelEvents() { 971 | channel.onmessage = options.onmessage; 972 | channel.onopen = function() { 973 | options.onopen(channel); 974 | }; 975 | channel.onclose = options.onclose; 976 | channel.onerror = options.onerror; 977 | } 978 | 979 | if (options.onAnswerSDP && moz && options.onmessage) { 980 | openAnswererChannel(); 981 | } 982 | 983 | if (!moz && !options.onOfferSDP) { 984 | openAnswererChannel(); 985 | } 986 | 987 | function openAnswererChannel() { 988 | peerConnection.ondatachannel = function(event) { 989 | channel = event.channel; 990 | setChannelEvents(); 991 | }; 992 | 993 | if (moz) { 994 | createAnswer(); 995 | } 996 | } 997 | 998 | function useless() {} 999 | 1000 | return { 1001 | addAnswerSDP: function(sdp) { 1002 | sdp = new SessionDescription(sdp); 1003 | peerConnection.setRemoteDescription(sdp, onSdpSuccess, onSdpError); 1004 | }, 1005 | addICE: function(candidate) { 1006 | peerConnection.addIceCandidate(new IceCandidate({ 1007 | sdpMLineIndex: candidate.sdpMLineIndex, 1008 | candidate: candidate.candidate 1009 | })); 1010 | }, 1011 | 1012 | peer: peerConnection, 1013 | channel: channel, 1014 | sendData: function(message) { 1015 | if (!channel) { 1016 | return; 1017 | } 1018 | 1019 | channel.send(message); 1020 | } 1021 | }; 1022 | } 1023 | 1024 | var FileConverter = { 1025 | DataURLToBlob: function(dataURL, fileType, callback) { 1026 | 1027 | function processInWebWorker() { 1028 | var blob = URL.createObjectURL(new Blob(['function getBlob(_dataURL, _fileType) {var binary = atob(_dataURL.substr(_dataURL.indexOf(",") + 1)),i = binary.length,view = new Uint8Array(i);while (i--) {view[i] = binary.charCodeAt(i);};postMessage(new Blob([view], {type: _fileType}));};this.onmessage = function (e) {var data = JSON.parse(e.data); getBlob(data.dataURL, data.fileType);}'], { 1029 | type: 'application/javascript' 1030 | })); 1031 | 1032 | var worker = new Worker(blob); 1033 | URL.revokeObjectURL(blob); 1034 | return worker; 1035 | } 1036 | 1037 | if (!!window.Worker && !isMobileDevice) { 1038 | var webWorker = processInWebWorker(); 1039 | 1040 | webWorker.onmessage = function(event) { 1041 | callback(event.data); 1042 | }; 1043 | 1044 | webWorker.postMessage(JSON.stringify({ 1045 | dataURL: dataURL, 1046 | fileType: fileType 1047 | })); 1048 | } else { 1049 | var binary = atob(dataURL.substr(dataURL.indexOf(',') + 1)), 1050 | i = binary.length, 1051 | view = new Uint8Array(i); 1052 | 1053 | while (i--) { 1054 | view[i] = binary.charCodeAt(i); 1055 | } 1056 | 1057 | callback(new Blob([view])); 1058 | } 1059 | } 1060 | }; 1061 | 1062 | function FileReceiver(root) { 1063 | var content = {}; 1064 | var packets = {}; 1065 | var numberOfPackets = {}; 1066 | 1067 | function receive(data) { 1068 | var uuid = data.uuid; 1069 | 1070 | if (typeof data.packets !== 'undefined') { 1071 | numberOfPackets[uuid] = packets[uuid] = parseInt(data.packets); 1072 | } 1073 | 1074 | if (root.onFileProgress) { 1075 | root.onFileProgress({ 1076 | remaining: packets[uuid]--, 1077 | length: numberOfPackets[uuid], 1078 | received: numberOfPackets[uuid] - packets[uuid], 1079 | 1080 | maxChunks: numberOfPackets[uuid], 1081 | uuid: uuid, 1082 | currentPosition: numberOfPackets[uuid] - packets[uuid] 1083 | }, uuid); 1084 | } 1085 | 1086 | if (!content[uuid]) { 1087 | content[uuid] = []; 1088 | } 1089 | 1090 | content[uuid].push(data.message); 1091 | 1092 | if (data.last) { 1093 | var dataURL = content[uuid].join(''); 1094 | 1095 | FileConverter.DataURLToBlob(dataURL, data.fileType, function(blob) { 1096 | blob.uuid = uuid; 1097 | blob.name = data.name; 1098 | // blob.type = data.fileType; 1099 | blob.extra = data.extra || {}; 1100 | 1101 | blob.url = (window.URL || window.webkitURL).createObjectURL(blob); 1102 | 1103 | if (root.autoSaveToDisk) { 1104 | FileSaver.SaveToDisk(blob.url, data.name); 1105 | } 1106 | 1107 | if (root.onFileReceived) { 1108 | root.onFileReceived(blob); 1109 | } 1110 | 1111 | delete content[uuid]; 1112 | }); 1113 | } 1114 | } 1115 | 1116 | return { 1117 | receive: receive 1118 | }; 1119 | } 1120 | 1121 | var FileSaver = { 1122 | SaveToDisk: function(fileUrl, fileName) { 1123 | var hyperlink = document.createElement('a'); 1124 | hyperlink.href = fileUrl; 1125 | hyperlink.target = '_blank'; 1126 | hyperlink.download = fileName || fileUrl; 1127 | 1128 | var mouseEvent = new MouseEvent('click', { 1129 | view: window, 1130 | bubbles: true, 1131 | cancelable: true 1132 | }); 1133 | 1134 | hyperlink.dispatchEvent(mouseEvent); 1135 | (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); 1136 | } 1137 | }; 1138 | 1139 | var FileSender = { 1140 | send: function(config) { 1141 | var root = config.root; 1142 | var channel = config.channel; 1143 | var privateChannel = config._channel; 1144 | var file = config.file; 1145 | 1146 | if (!config.file) { 1147 | console.error('You must attach/select a file.'); 1148 | return; 1149 | } 1150 | 1151 | // max chunk sending limit on chrome is 64k 1152 | // max chunk receiving limit on firefox is 16k 1153 | var packetSize = 15 * 1000; 1154 | 1155 | if (root.chunkSize) { 1156 | packetSize = root.chunkSize; 1157 | } 1158 | 1159 | var textToTransfer = ''; 1160 | var numberOfPackets = 0; 1161 | var packets = 0; 1162 | 1163 | file.uuid = getRandomString(); 1164 | 1165 | function processInWebWorker() { 1166 | var blob = URL.createObjectURL(new Blob(['function readFile(_file) {postMessage(new FileReaderSync().readAsDataURL(_file));};this.onmessage = function (e) {readFile(e.data);}'], { 1167 | type: 'application/javascript' 1168 | })); 1169 | 1170 | var worker = new Worker(blob); 1171 | URL.revokeObjectURL(blob); 1172 | return worker; 1173 | } 1174 | 1175 | if (!!window.Worker && !isMobileDevice) { 1176 | var webWorker = processInWebWorker(); 1177 | 1178 | webWorker.onmessage = function(event) { 1179 | onReadAsDataURL(event.data); 1180 | }; 1181 | 1182 | webWorker.postMessage(file); 1183 | } else { 1184 | var reader = new FileReader(); 1185 | reader.onload = function(e) { 1186 | onReadAsDataURL(e.target.result); 1187 | }; 1188 | reader.readAsDataURL(file); 1189 | } 1190 | 1191 | function onReadAsDataURL(dataURL, text) { 1192 | var data = { 1193 | type: 'file', 1194 | uuid: file.uuid, 1195 | maxChunks: numberOfPackets, 1196 | currentPosition: numberOfPackets - packets, 1197 | name: file.name, 1198 | fileType: file.type, 1199 | size: file.size 1200 | }; 1201 | 1202 | if (dataURL) { 1203 | text = dataURL; 1204 | numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); 1205 | 1206 | file.maxChunks = data.maxChunks = numberOfPackets; 1207 | data.currentPosition = numberOfPackets - packets; 1208 | 1209 | if (root.onFileSent) { 1210 | root.onFileSent(file); 1211 | } 1212 | } 1213 | 1214 | if (root.onFileProgress) { 1215 | root.onFileProgress({ 1216 | remaining: packets--, 1217 | length: numberOfPackets, 1218 | sent: numberOfPackets - packets, 1219 | 1220 | maxChunks: numberOfPackets, 1221 | uuid: file.uuid, 1222 | currentPosition: numberOfPackets - packets 1223 | }, file.uuid); 1224 | } 1225 | 1226 | if (text.length > packetSize) { 1227 | data.message = text.slice(0, packetSize); 1228 | } else { 1229 | data.message = text; 1230 | data.last = true; 1231 | data.name = file.name; 1232 | 1233 | file.url = URL.createObjectURL(file); 1234 | root.onFileSent(file, file.uuid); 1235 | } 1236 | 1237 | channel.send(data, privateChannel); 1238 | 1239 | textToTransfer = text.slice(data.message.length); 1240 | if (textToTransfer.length) { 1241 | setTimeout(function() { 1242 | onReadAsDataURL(null, textToTransfer); 1243 | }, root.chunkInterval || 100); 1244 | } 1245 | } 1246 | } 1247 | }; 1248 | 1249 | function SocketConnector(_channel, config) { 1250 | var socket = config.openSignalingChannel({ 1251 | channel: _channel, 1252 | onopen: config.onopen, 1253 | onmessage: config.onmessage, 1254 | callback: function(_socket) { 1255 | socket = _socket; 1256 | } 1257 | }); 1258 | 1259 | return { 1260 | send: function(message) { 1261 | if (!socket) { 1262 | return; 1263 | } 1264 | 1265 | socket.send({ 1266 | userid: userid, 1267 | message: message 1268 | }); 1269 | } 1270 | }; 1271 | } 1272 | 1273 | function TextReceiver() { 1274 | var content = {}; 1275 | 1276 | function receive(data, onmessage, userid) { 1277 | // uuid is used to uniquely identify sending instance 1278 | var uuid = data.uuid; 1279 | if (!content[uuid]) { 1280 | content[uuid] = []; 1281 | } 1282 | 1283 | content[uuid].push(data.message); 1284 | if (data.last) { 1285 | var message = content[uuid].join(''); 1286 | if (data.isobject) { 1287 | message = JSON.parse(message); 1288 | } 1289 | 1290 | // latency detection 1291 | var receivingTime = new Date().getTime(); 1292 | var latency = receivingTime - data.sendingTime; 1293 | 1294 | onmessage(message, userid, latency); 1295 | 1296 | delete content[uuid]; 1297 | } 1298 | } 1299 | 1300 | return { 1301 | receive: receive 1302 | }; 1303 | } 1304 | 1305 | var TextSender = { 1306 | send: function(config) { 1307 | var root = config.root; 1308 | 1309 | var channel = config.channel; 1310 | var _channel = config._channel; 1311 | var initialText = config.text; 1312 | var packetSize = root.chunkSize || 1000; 1313 | var textToTransfer = ''; 1314 | var isobject = false; 1315 | 1316 | if (typeof initialText !== 'string') { 1317 | isobject = true; 1318 | initialText = JSON.stringify(initialText); 1319 | } 1320 | 1321 | // uuid is used to uniquely identify sending instance 1322 | var uuid = getRandomString(); 1323 | var sendingTime = new Date().getTime(); 1324 | 1325 | sendText(initialText); 1326 | 1327 | function sendText(textMessage, text) { 1328 | var data = { 1329 | type: 'text', 1330 | uuid: uuid, 1331 | sendingTime: sendingTime 1332 | }; 1333 | 1334 | if (textMessage) { 1335 | text = textMessage; 1336 | data.packets = parseInt(text.length / packetSize); 1337 | } 1338 | 1339 | if (text.length > packetSize) { 1340 | data.message = text.slice(0, packetSize); 1341 | } else { 1342 | data.message = text; 1343 | data.last = true; 1344 | data.isobject = isobject; 1345 | } 1346 | 1347 | channel.send(data, _channel); 1348 | 1349 | textToTransfer = text.slice(data.message.length); 1350 | 1351 | if (textToTransfer.length) { 1352 | setTimeout(function() { 1353 | sendText(null, textToTransfer); 1354 | }, root.chunkInterval || 100); 1355 | } 1356 | } 1357 | } 1358 | }; 1359 | 1360 | })(); 1361 | -------------------------------------------------------------------------------- /DataChannel.min.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Last time updated: 2017-07-29 4:31:53 PM UTC 4 | 5 | // __________________ 6 | // DataChannel v1.0.0 7 | 8 | // Open-Sourced: https://github.com/muaz-khan/DataChannel 9 | 10 | // -------------------------------------------------- 11 | // Muaz Khan - www.MuazKhan.com 12 | // MIT License - www.WebRTC-Experiment.com/licence 13 | // -------------------------------------------------- 14 | 15 | 16 | "use strict";!function(){function DataConnector(root,config){function newPrivateSocket(_config){function initPeer(offerSDP){"one-to-one"===root.direction&&window.isFirstConnectionOpened||(offerSDP?(peerConfig.offerSDP=offerSDP,peerConfig.onAnswerSDP=sendsdp):peerConfig.onOfferSDP=sendsdp,peer=new RTCPeerConnection(peerConfig))}function sendsdp(sdp){sdp=JSON.stringify(sdp);var part=parseInt(sdp.length/3),firstPart=sdp.slice(0,part),secondPart=sdp.slice(part,sdp.length-1),thirdPart="";sdp.length>part+part&&(secondPart=sdp.slice(part,part+part),thirdPart=sdp.slice(part+part,sdp.length)),socket.send({userToken:self.userToken,firstPart:firstPart}),socket.send({userToken:self.userToken,secondPart:secondPart}),socket.send({userToken:self.userToken,thirdPart:thirdPart})}function socketResponse(response){if(response.userToken!==self.userToken){if((response.firstPart||response.secondPart||response.thirdPart)&&(response.firstPart&&(_config.userid=response.userToken,inner.firstPart=response.firstPart,inner.secondPart&&inner.thirdPart&&selfInvoker()),response.secondPart&&(inner.secondPart=response.secondPart,inner.firstPart&&inner.thirdPart&&selfInvoker()),response.thirdPart&&(inner.thirdPart=response.thirdPart,inner.firstPart&&inner.secondPart&&selfInvoker())),response.candidate&&!gotstream&&peer){if(!inner.firstPart||!inner.secondPart||!inner.thirdPart)return setTimeout(function(){socketResponse(response)},400);peer.addICE({sdpMLineIndex:response.candidate.sdpMLineIndex,candidate:JSON.parse(response.candidate.candidate)}),console.debug("ice candidate",response.candidate.candidate)}response.left&&(peer&&peer.peer&&(peer.peer.close(),peer.peer=null),response.closeEntireSession?leaveChannels():socket&&(socket.send({left:!0,userToken:self.userToken}),socket=null),root.onleave(response.userToken)),response.playRoleOfBroadcaster&&setTimeout(function(){self.roomToken=response.roomToken,root.open(self.roomToken),self.sockets=swap(self.sockets)},600)}}function selfInvoker(){invokedOnce||(invokedOnce=!0,inner.sdp=JSON.parse(inner.firstPart+inner.secondPart+inner.thirdPart),isofferer?peer.addAnswerSDP(inner.sdp):initPeer(inner.sdp),console.debug("sdp",inner.sdp.sdp))}var socketConfig={channel:_config.channel,onmessage:socketResponse,onopen:function(){isofferer&&!peer&&initPeer(),_config.socketIndex=socket.index=self.sockets.length,self.socketObjects[socketConfig.channel]=socket,self.sockets[_config.socketIndex]=socket}};socketConfig.callback=function(_socket){socket=_socket,socketConfig.onopen()};var gotstream,peer,socket=root.openSignalingChannel(socketConfig),isofferer=_config.isofferer,inner={},peerConfig={onICE:function(candidate){if(!socket)return setTimeout(function(){peerConfig.onICE(candidate)},2e3);socket.send({userToken:self.userToken,candidate:{sdpMLineIndex:candidate.sdpMLineIndex,candidate:JSON.stringify(candidate.candidate)}})},onopen:function(channel){channel.peer=peer.peer,rtcDataChannels.push(channel),config.onopen(_config.userid,channel),"many-to-many"===root.direction&&isbroadcaster&&channels.split("--").length>3&&defaultSocket&&defaultSocket.send({newParticipant:socket.channel,userToken:self.userToken}),window.isFirstConnectionOpened=gotstream=!0},onmessage:function(event){config.onmessage(event.data,_config.userid)},onclose:config.onclose,onerror:root.onerror,preferSCTP:root.preferSCTP},invokedOnce=!1}function onNewParticipant(channel){if(channel&&-1===channels.indexOf(channel)&&channel!==self.userToken){channels+=channel+"--";var newChannel=uniqueToken();newPrivateSocket({channel:newChannel,closeSocket:!0}),defaultSocket&&defaultSocket.send({participant:!0,userToken:self.userToken,joinUser:channel,channel:newChannel})}}function uniqueToken(){return(Math.round(60535*Math.random())+5e6).toString()}function leaveChannels(channel){var socket,alert={left:!0,userToken:self.userToken};if(isbroadcaster&&(root.autoCloseEntireSession?alert.closeEntireSession=!0:self.sockets[0].send({playRoleOfBroadcaster:!0,userToken:self.userToken,roomToken:self.roomToken})),!channel){for(var sockets=self.sockets,length=sockets.length,i=0;i received successfully.")},this.onFileSent=function(file){console.debug("File <",file.name,"> sent successfully.")},this.onFileProgress=function(packets){console.debug("<",packets.remaining,"> items remaining.")},this.open=function(_channel){self.joinedARoom=!0,self.socket?self.socket.onDisconnect().remove():self.isInitiator=!0,_channel&&(self.channel=_channel),prepareInit(function(){init(),IsDataChannelSupported&&dataConnector.createRoom(_channel)})},this.connect=function(_channel){_channel&&(self.channel=_channel),prepareInit(init)},this.join=function(room){if(!room.id||!room.owner)throw"Invalid room info passed.";dataConnector||init(),dataConnector.joinRoom&&dataConnector.joinRoom({roomToken:room.id,joinUser:room.owner})},this.send=function(data,_channel){if(!data)throw"No file, data or text message to share.";void 0===data.size||void 0===data.type?TextSender.send({text:data,channel:dataConnector,_channel:_channel,root:self}):FileSender.send({file:data,channel:dataConnector,onFileSent:function(file){self.onFileSent(file)},onFileProgress:function(packets,uuid){self.onFileProgress(packets,uuid)},_channel:_channel,root:self})},this.onleave=function(userid){console.debug(userid,"left!")},this.leave=this.eject=function(userid){dataConnector.leave(userid,self.autoCloseEntireSession)},this.openNewSession=function(isOpenNewSession,isNonFirebaseClient){if(isOpenNewSession){if(self.isNewSessionOpened)return;self.isNewSessionOpened=!0,self.joinedARoom||self.open()}isOpenNewSession&&!isNonFirebaseClient||self.connect(),isNonFirebaseClient&&setTimeout(function(){self.openNewSession(!0)},5e3)},void 0===this.preferSCTP&&(this.preferSCTP=!!(isFirefox||chromeVersion>=32)),void 0===this.chunkSize&&(this.chunkSize=isFirefox||chromeVersion>=32?13e3:1e3),void 0===this.chunkInterval&&(this.chunkInterval=isFirefox||chromeVersion>=32?100:500),self.automatic&&(window.Firebase?(console.debug("checking presence of the room.."),new window.Firebase("https://"+(extras.firebase||self.firebase||"muazkh")+".firebaseIO.com/"+self.channel).once("value",function(data){console.debug("room is present?",null!==data.val()),self.openNewSession(null===data.val())})):self.openNewSession(!1,!0))};var moz=!!navigator.mozGetUserMedia,IsDataChannelSupported=!(moz&&!navigator.mozGetUserMedia||!moz&&!navigator.webkitGetUserMedia),userid=getRandomString(),isMobileDevice=navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i),isChrome=!!navigator.webkitGetUserMedia,isFirefox=!!navigator.mozGetUserMedia,chromeVersion=50;isChrome&&(chromeVersion=navigator.mozGetUserMedia?0:parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]));var IceServersHandler=function(){function getSTUNObj(stunStr){var urlsParam="urls";"undefined"!=typeof isPluginRTC&&(urlsParam="url");var obj={};return obj[urlsParam]=stunStr,obj}function getTURNObj(turnStr,username,credential){var urlsParam="urls";"undefined"!=typeof isPluginRTC&&(urlsParam="url");var obj={username:username,credential:credential};return obj[urlsParam]=turnStr,obj}return{getIceServers:function(connection){var iceServers=[];return iceServers.push(getSTUNObj("stun:stun.l.google.com:19302")),iceServers.push(getTURNObj("stun:webrtcweb.com:7788","muazkh","muazkh")),iceServers.push(getTURNObj("turn:webrtcweb.com:7788","muazkh","muazkh")),iceServers.push(getTURNObj("turn:webrtcweb.com:8877","muazkh","muazkh")),iceServers.push(getTURNObj("turns:webrtcweb.com:7788","muazkh","muazkh")),iceServers.push(getTURNObj("turns:webrtcweb.com:8877","muazkh","muazkh")),iceServers.push(getTURNObj("stun:webrtcweb.com:4455","muazkh","muazkh")),iceServers.push(getTURNObj("turn:webrtcweb.com:4455","muazkh","muazkh")),iceServers.push(getTURNObj("turn:webrtcweb.com:5544?transport=tcp","muazkh","muazkh")),iceServers}}}(),FileConverter={DataURLToBlob:function(dataURL,fileType,callback){if(window.Worker&&!isMobileDevice){var webWorker=function(){var blob=URL.createObjectURL(new Blob(['function getBlob(_dataURL, _fileType) {var binary = atob(_dataURL.substr(_dataURL.indexOf(",") + 1)),i = binary.length,view = new Uint8Array(i);while (i--) {view[i] = binary.charCodeAt(i);};postMessage(new Blob([view], {type: _fileType}));};this.onmessage = function (e) {var data = JSON.parse(e.data); getBlob(data.dataURL, data.fileType);}'],{type:"application/javascript"})),worker=new Worker(blob);return URL.revokeObjectURL(blob),worker}();webWorker.onmessage=function(event){callback(event.data)},webWorker.postMessage(JSON.stringify({dataURL:dataURL,fileType:fileType}))}else{for(var binary=atob(dataURL.substr(dataURL.indexOf(",")+1)),i=binary.length,view=new Uint8Array(i);i--;)view[i]=binary.charCodeAt(i);callback(new Blob([view]))}}},FileSaver={SaveToDisk:function(fileUrl,fileName){var hyperlink=document.createElement("a");hyperlink.href=fileUrl,hyperlink.target="_blank",hyperlink.download=fileName||fileUrl;var mouseEvent=new MouseEvent("click",{view:window,bubbles:!0,cancelable:!0});hyperlink.dispatchEvent(mouseEvent),(window.URL||window.webkitURL).revokeObjectURL(hyperlink.href)}},FileSender={send:function(config){function onReadAsDataURL(dataURL,text){var data={type:"file",uuid:file.uuid,maxChunks:numberOfPackets,currentPosition:numberOfPackets-packets,name:file.name,fileType:file.type,size:file.size};dataURL&&(text=dataURL,numberOfPackets=packets=data.packets=parseInt(text.length/packetSize),file.maxChunks=data.maxChunks=numberOfPackets,data.currentPosition=numberOfPackets-packets,root.onFileSent&&root.onFileSent(file)),root.onFileProgress&&root.onFileProgress({remaining:packets--,length:numberOfPackets,sent:numberOfPackets-packets,maxChunks:numberOfPackets,uuid:file.uuid,currentPosition:numberOfPackets-packets},file.uuid),text.length>packetSize?data.message=text.slice(0,packetSize):(data.message=text,data.last=!0,data.name=file.name,file.url=URL.createObjectURL(file),root.onFileSent(file,file.uuid)),channel.send(data,privateChannel),(textToTransfer=text.slice(data.message.length)).length&&setTimeout(function(){onReadAsDataURL(null,textToTransfer)},root.chunkInterval||100)}var root=config.root,channel=config.channel,privateChannel=config._channel,file=config.file;if(config.file){var packetSize=15e3;root.chunkSize&&(packetSize=root.chunkSize);var textToTransfer="",numberOfPackets=0,packets=0;if(file.uuid=getRandomString(),window.Worker&&!isMobileDevice){var webWorker=function(){var blob=URL.createObjectURL(new Blob(["function readFile(_file) {postMessage(new FileReaderSync().readAsDataURL(_file));};this.onmessage = function (e) {readFile(e.data);}"],{type:"application/javascript"})),worker=new Worker(blob);return URL.revokeObjectURL(blob),worker}();webWorker.onmessage=function(event){onReadAsDataURL(event.data)},webWorker.postMessage(file)}else{var reader=new FileReader;reader.onload=function(e){onReadAsDataURL(e.target.result)},reader.readAsDataURL(file)}}else console.error("You must attach/select a file.")}},TextSender={send:function(config){function sendText(textMessage,text){var data={type:"text",uuid:uuid,sendingTime:sendingTime};textMessage&&(text=textMessage,data.packets=parseInt(text.length/packetSize)),text.length>packetSize?data.message=text.slice(0,packetSize):(data.message=text,data.last=!0,data.isobject=isobject),channel.send(data,_channel),(textToTransfer=text.slice(data.message.length)).length&&setTimeout(function(){sendText(null,textToTransfer)},root.chunkInterval||100)}var root=config.root,channel=config.channel,_channel=config._channel,initialText=config.text,packetSize=root.chunkSize||1e3,textToTransfer="",isobject=!1;"string"!=typeof initialText&&(isobject=!0,initialText=JSON.stringify(initialText));var uuid=getRandomString(),sendingTime=(new Date).getTime();sendText(initialText)}}}(); -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | require('load-grunt-tasks')(grunt, { 5 | pattern: 'grunt-*', 6 | config: 'package.json', 7 | scope: 'devDependencies' 8 | }); 9 | 10 | var versionNumber = grunt.file.readJSON('package.json').version; 11 | 12 | var banner = '\'use strict\';\n\n'; 13 | 14 | banner += '// Last time updated: <%= grunt.template.today("UTC:yyyy-mm-dd h:MM:ss TT Z") %>\n\n'; 15 | 16 | banner += '// __________________\n'; 17 | banner += '// DataChannel v' + versionNumber + '\n\n'; 18 | 19 | banner += '// Open-Sourced: https://github.com/muaz-khan/DataChannel\n\n'; 20 | 21 | banner += '// --------------------------------------------------\n'; 22 | banner += '// Muaz Khan - www.MuazKhan.com\n'; 23 | banner += '// MIT License - www.WebRTC-Experiment.com/licence\n'; 24 | banner += '// --------------------------------------------------\n\n'; 25 | 26 | // configure project 27 | grunt.initConfig({ 28 | // make node configurations available 29 | pkg: grunt.file.readJSON('package.json'), 30 | concat: { 31 | options: { 32 | stripBanners: true, 33 | separator: '\n', 34 | banner: banner 35 | }, 36 | dist: { 37 | src: [ 38 | 'dev/head.js', 39 | 'dev/DataChannel.js', 40 | 'dev/DataConnector.js', 41 | 'dev/globals.js', 42 | // 'dev/externalIceServers.js', 43 | 'dev/IceServersHandler.js', 44 | 'dev/RTCPeerConnection.js', 45 | 'dev/FileConverter.js', 46 | 'dev/FileReceiver.js', 47 | 'dev/FileSaver.js', 48 | 'dev/FileSender.js', 49 | 'dev/SocketConnector.js', 50 | 'dev/TextReceiver.js', 51 | 'dev/TextSender.js', 52 | 'dev/tail.js' 53 | ], 54 | dest: 'DataChannel.js', 55 | }, 56 | }, 57 | htmlhint: { 58 | html1: { 59 | src: [ 60 | './*.html' 61 | ], 62 | options: { 63 | 'tag-pair': true 64 | } 65 | } 66 | }, 67 | jshint: { 68 | options: { 69 | ignores: [], 70 | // use default .jshintrc files 71 | jshintrc: true 72 | }, 73 | files: ['DataChannel.js'] 74 | }, 75 | uglify: { 76 | options: { 77 | mangle: false, 78 | banner: banner 79 | }, 80 | my_target: { 81 | files: { 82 | 'DataChannel.min.js': ['DataChannel.js'] 83 | } 84 | } 85 | }, 86 | jsbeautifier: { 87 | files: [ 88 | 'dev/*.js', 89 | 'Gruntfile.js', 90 | 'DataChannel.js' 91 | ], 92 | options: { 93 | js: { 94 | braceStyle: "collapse", 95 | breakChainedMethods: false, 96 | e4x: false, 97 | evalCode: false, 98 | indentChar: " ", 99 | indentLevel: 0, 100 | indentSize: 4, 101 | indentWithTabs: false, 102 | jslintHappy: false, 103 | keepArrayIndentation: false, 104 | keepFunctionIndentation: false, 105 | maxPreserveNewlines: 10, 106 | preserveNewlines: true, 107 | spaceBeforeConditional: true, 108 | spaceInParen: false, 109 | unescapeStrings: false, 110 | wrapLineLength: 0 111 | }, 112 | html: { 113 | braceStyle: "collapse", 114 | indentChar: " ", 115 | indentScripts: "keep", 116 | indentSize: 4, 117 | maxPreserveNewlines: 10, 118 | preserveNewlines: true, 119 | unformatted: ["a", "sub", "sup", "b", "i", "u"], 120 | wrapLineLength: 0 121 | }, 122 | css: { 123 | indentChar: " ", 124 | indentSize: 4 125 | } 126 | } 127 | }, 128 | bump: { 129 | options: { 130 | files: ['package.json', 'bower.json'], 131 | updateConfigs: [], 132 | commit: true, 133 | commitMessage: 'v%VERSION%', 134 | commitFiles: ['package.json', 'bower.json'], 135 | createTag: true, 136 | tagName: '%VERSION%', 137 | tagMessage: '%VERSION%', 138 | push: false, 139 | pushTo: 'upstream', 140 | gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' 141 | } 142 | } 143 | }); 144 | 145 | // enable plugins 146 | 147 | // set default tasks to run when grunt is called without parameters 148 | // http://gruntjs.com/api/grunt.task 149 | grunt.registerTask('default', ['concat', 'jsbeautifier', 'htmlhint', 'jshint', 'uglify']); 150 | }; 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [DataChannel.js](https://github.com/muaz-khan/DataChannel) : A JavaScript wrapper library for RTCDataChannel APIs / [Demos](https://www.webrtc-experiment.com/#DataChannel) 2 | 3 | [![npm](https://img.shields.io/npm/v/datachannel.svg)](https://npmjs.org/package/datachannel) [![downloads](https://img.shields.io/npm/dm/datachannel.svg)](https://npmjs.org/package/datachannel) [![Build Status: Linux](https://travis-ci.org/muaz-khan/DataChannel.png?branch=master)](https://travis-ci.org/muaz-khan/DataChannel) 4 | 5 | ``` 6 | npm install datachannel 7 | 8 | # or 9 | bower install datachannel 10 | ``` 11 | 12 | DataChannel.js is a JavaScript library useful to write many-to-many i.e. group file/data sharing or text chat applications. Its syntax is easier to use and understand. It highly simplifies complex tasks like any or all user rejection/ejection; direct messages delivery; and more. 13 | 14 | If you want all DataChannel.js functionalities along with media streaming and runtime additions/deletions then [RTCMultiConnection.js](http://www.rtcmulticonnection.org/) is a good chose with similar APIs/syntax. 15 | 16 | * [DataChanel.js and Reliable Signaling](https://github.com/muaz-khan/Reliable-Signaler/tree/master/datachannel-client) 17 | 18 | ## Features 19 | 20 | 1. Direct messages — to any user using their `user-id` 21 | 2. Eject/Reject any user — using their `user-id` 22 | 3. Leave any room (i.e. data session) or close entire session using `leave` method 23 | 4. File size is limitless! 24 | 5. Text message length is limitless! 25 | 6. Size of data is also limitless! 26 | 7. Fallback to socket.io/websockets/etc. 27 | 8. Users' presence detection using `onleave` 28 | 9. Latency detection 29 | 10. Multi-longest strings/files concurrent 30 | 11. File queue support added. Previously shared files will be auto transmitted to each new peer. 31 | 32 | ## [Demos using DataChannel.js](https://www.webrtc-experiment.com/#DataChannel) 33 | 34 | 1. [DataChannel basic demo](https://www.webrtc-experiment.com/DataChannel/) 35 | 2. [Auto Session Establishment and Users presence detection](https://www.webrtc-experiment.com/DataChannel/auto-session-establishment.html) 36 | 3. [Text Chat using Pusher and DataChannel.js](http://webrtc-chat-demo.pusher.io/) 37 | 38 | ## Try a Quick Demo (Text Chat) 39 | 40 | ```html 41 | 42 | 43 |
44 |
45 | 46 | 80 | ``` 81 | 82 | ## First Step: Link the library 83 | 84 | ```html 85 | 86 | ``` 87 | 88 | ## Last Step: Start using it! 89 | 90 | ```javascript 91 | var channel = new DataChannel('[optional] channel-name'); 92 | channel.send(file || data || 'text-message'); 93 | ``` 94 | 95 | ## open/connect data channels 96 | 97 | ```javascript 98 | // to create/open a new channel 99 | channel.open('channel-name'); 100 | 101 | // if someone already created a channel; to join it: use "connect" method 102 | channel.connect('channel-name'); 103 | ``` 104 | 105 | ## `onopen` and `onmessage` 106 | 107 | If you're familiar with WebSockets; these two methods works in the exact same way: 108 | 109 | ```javascript 110 | channel.onopen = function(userid) { } 111 | channel.onmessage = function(message, userid, latency) { } 112 | ``` 113 | 114 | `user-ids` can be used to send **direct messages** or to **eject/leave** any user: 115 | 116 | ## `ondatachannel` 117 | 118 | Allows you show list of all available data channels to the user; and let him choose which one to join: 119 | 120 | ```javascript 121 | channel.ondatachannel = function(data_channel) { 122 | channel.join(data_channel); 123 | 124 | // or 125 | channel.join({ 126 | id: data_channel.id, 127 | owner: data_channel.owner 128 | }); 129 | 130 | // id: unique identifier for the session 131 | // owner: unique identifier for the session initiator 132 | }; 133 | ``` 134 | 135 | ## Use custom user-ids 136 | 137 | ```javascript 138 | channel.userid = 'predefined-userid'; 139 | ``` 140 | 141 | Remember; custom defined `user-id` must be unique username. 142 | 143 | ## Direct messages 144 | 145 | ```javascript 146 | channel.channels[userid].send(file || data || 'text message'); 147 | ``` 148 | 149 | ## Eject/Reject users using their `user-ids` 150 | 151 | ```javascript 152 | channel.eject(userid); // throw a user out of your room! 153 | ``` 154 | 155 | ## Close/Leave the entire room 156 | 157 | ```javascript 158 | channel.leave(); // close your own entire data session 159 | ``` 160 | 161 | ## Detect users' presence 162 | 163 | ```javascript 164 | channel.onleave = function(userid) { }; 165 | ``` 166 | 167 | ## Auto Close Entire Session 168 | 169 | When room initiator leaves; you can enforce auto-closing of the entire session. By default: it is `false`: 170 | 171 | ```javascript 172 | channel.autoCloseEntireSession = true; 173 | ``` 174 | 175 | It means that session will be kept active all the time; even if initiator leaves the session. 176 | 177 | You can set `autoCloseEntireSession` before calling `leave` method; which will enforce closing of the entire session: 178 | 179 | ```javascript 180 | channel.autoCloseEntireSession = true; 181 | channel.leave(); // closing entire session 182 | ``` 183 | 184 | ## `uuid` for files 185 | 186 | You can get `uuid` for each file (being sent) like this: 187 | 188 | ```javascript 189 | channel.send(file); 190 | var uuid = file.uuid; // "file"-Dot-uuid 191 | ``` 192 | 193 | ## To Share files 194 | 195 | ```javascript 196 | var progressHelper = {}; 197 | 198 | // to make sure file-saver dialog is not invoked. 199 | channel.autoSaveToDisk = false; 200 | 201 | channel.onFileProgress = function (chunk, uuid) { 202 | var helper = progressHelper[chunk.uuid]; 203 | helper.progress.value = chunk.currentPosition || chunk.maxChunks || helper.progress.max; 204 | updateLabel(helper.progress, helper.label); 205 | }; 206 | 207 | channel.onFileStart = function (file) { 208 | var div = document.createElement('div'); 209 | div.title = file.name; 210 | div.innerHTML = ' '; 211 | appendDIV(div, fileProgress); 212 | progressHelper[file.uuid] = { 213 | div: div, 214 | progress: div.querySelector('progress'), 215 | label: div.querySelector('label') 216 | }; 217 | progressHelper[file.uuid].progress.max = file.maxChunks; 218 | }; 219 | 220 | channel.onFileSent = function (file) { 221 | progressHelper[file.uuid].div.innerHTML = '' + file.name + ''; 222 | }; 223 | 224 | channel.onFileReceived = function (fileName, file) { 225 | progressHelper[file.uuid].div.innerHTML = '' + file.name + ''; 226 | }; 227 | 228 | function updateLabel(progress, label) { 229 | if (progress.position == -1) return; 230 | var position = +progress.position.toFixed(2).split('.')[1] || 100; 231 | label.innerHTML = position + '%'; 232 | } 233 | ``` 234 | 235 | ## File Queue 236 | 237 | File Queue support added to make sure newly connected users gets all previously shared files. 238 | 239 | You can see list of previously shared files: 240 | 241 | ```javascript 242 | console.log( channel.fileQueue ); 243 | ``` 244 | 245 | ## Auto-Save file to Disk 246 | 247 | By default; `autoSaveToDisk` is set to `true`. When it is `true`; it will save file to disk as soon as it is received. To prevent auto-saving feature; just set it `false`: 248 | 249 | ```javascript 250 | channel.autoSaveToDisk = false; // prevent auto-saving! 251 | channel.onFileReceived = function (fileName, file) { 252 | // file.url 253 | // file.uuid 254 | 255 | hyperlink.href = file.url; 256 | }; 257 | ``` 258 | 259 | ## Latency Detection 260 | 261 | ```javascript 262 | channel.onmessage = function(message, userid, latency) { 263 | console.log('latency:', latency, 'milliseconds'); 264 | }; 265 | ``` 266 | 267 | ## Concurrent Transmission 268 | 269 | You can send multiple files concurrently; or multiple longer text messages: 270 | 271 | ```javascript 272 | // individually 273 | channel.send(fileNumber1); 274 | channel.send(fileNumber2); 275 | channel.send(fileNumber3); 276 | 277 | // or as an array 278 | channel.send([fileNumber1, fileNumber2, fileNumber3]); 279 | 280 | channel.send('longer string-1'); 281 | channel.send('longer string-2'); 282 | channel.send('longer string-3'); 283 | ``` 284 | 285 | ## Errors Handling 286 | 287 | ```javascript 288 | // error to open data ports 289 | channel.onerror = function(event) {} 290 | 291 | // data ports suddenly dropped 292 | channel.onclose = function(event) {} 293 | ``` 294 | 295 | ## Data session direction 296 | 297 | Default direction is `many-to-many`. 298 | 299 | ```javascript 300 | channel.direction = 'one-to-one'; 301 | channel.direction = 'one-to-many'; 302 | channel.direction = 'many-to-many'; 303 | ``` 304 | 305 | = 306 | 307 | For signaling; please check following page: 308 | 309 | https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md 310 | 311 | Remember, you can use any signaling implementation that exists out there without modifying any single line! Just skip below code and open [above link](https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md)! 312 | 313 | ## Resources 314 | 315 | 1. Video Presentation for `openSignalingChannel`: https://vimeo.com/91780227 316 | 2. Documentation for `openSignalingChannel`: http://www.rtcmulticonnection.org/docs/openSignalingChannel/ 317 | 318 | ## Use [your own socket.io for signaling](https://github.com/muaz-khan/WebRTC-Experiment/blob/master/socketio-over-nodejs) 319 | 320 | ```javascript 321 | dataChannel.openSignalingChannel = function(config) { 322 | var channel = config.channel || this.channel || 'default-channel'; 323 | 324 | var socket = io.connect('/?channel=' + channel); 325 | socket.channel = channel; 326 | 327 | socket.on('connect', function () { 328 | if (config.callback) config.callback(socket); 329 | }); 330 | 331 | socket.send = function (message) { 332 | socket.emit('message', { 333 | sender: dataChannel.userid, 334 | data : message 335 | }); 336 | }; 337 | 338 | socket.on('message', config.onmessage); 339 | }; 340 | ``` 341 | 342 | ## Use Pusher for signaling 343 | 344 | A demo & tutorial available here: http://pusher.com/tutorials/webrtc_chat 345 | 346 | Another link: http://www.rtcmulticonnection.org/docs/openSignalingChannel/#pusher-signaling 347 | 348 | ## Other Signaling resources 349 | 350 | * [DataChanel.js and Reliable Signaling](https://github.com/muaz-khan/Reliable-Signaler/tree/master/datachannel-client) 351 | 352 | 1. [XHR for Signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#xhr-signaling) 353 | 2. [WebSync for Signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#websync-signaling) 354 | 3. [SignalR for Signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#signalr-signaling) 355 | 4. [Pusher for Signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#pusher-signaling) 356 | 5. [Firebase for Signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#firebase-signaling) 357 | 6. [PubNub for Signaling](http://www.rtcmulticonnection.org/docs/openSignalingChannel/#pubnub-signaling) 358 | 359 | ## `transmitRoomOnce` 360 | 361 | `transmitRoomOnce` is preferred when using Firebase for signaling. It saves bandwidth and asks DataChannel.js library to not repeatedly transmit room details. 362 | 363 | ```javascript 364 | channel.transmitRoomOnce = true; 365 | ``` 366 | 367 | ## Browser Support 368 | 369 | [DataChannel.js](https://github.com/muaz-khan/DataChannel) works fine on following browsers: 370 | 371 | | Browser | Support | 372 | | ------------- |:-------------| 373 | | Firefox | [Stable](http://www.mozilla.org/en-US/firefox/new/) / [Aurora](http://www.mozilla.org/en-US/firefox/aurora/) / [Nightly](http://nightly.mozilla.org/) | 374 | | Google Chrome | [Stable](https://www.google.com/intl/en_uk/chrome/browser/) / [Canary](https://www.google.com/intl/en/chrome/browser/canary.html) / [Beta](https://www.google.com/intl/en/chrome/browser/beta.html) / [Dev](https://www.google.com/intl/en/chrome/browser/index.html?extra=devchannel#eula) | 375 | | Android | [Chrome Beta](https://play.google.com/store/apps/details?id=com.chrome.beta&hl=en) | 376 | 377 | ## License 378 | 379 | [DataChannel.js](https://github.com/muaz-khan/DataChannel) is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](http://www.MuazKhan.com). 380 | -------------------------------------------------------------------------------- /auto-session-establishment.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Auto Session Establishment using DataChannel.js 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 | 37 | 38 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 |
50 |

Auto Session Establishment using DataChannel.js

51 |

52 | HOME 53 | © 54 | Muaz Khan 55 | 56 | . 57 | @WebRTCWeb 58 | 59 | . 60 | Github 61 | 62 | . 63 | Latest issues 64 | 65 | . 66 | What's New? 67 |

68 |
69 | 70 |
71 | 72 |
73 | This demo does NOT works anymore. Please check this one instead:
74 | https://www.webrtc-experiment.com/DataChannel/ 75 |
76 | 77 | 78 | 79 | 89 | 95 | 96 |
80 |

Text Chat

81 | 82 |
83 | 84 | 86 | 88 |
90 |

Share Files

91 | 92 | 93 |
94 |
97 | 98 | 209 |
210 |
211 | 212 |

Getting started with WebRTC DataChannel

214 |
215 | <script src="https://cdn.webrtc-experiment.com/DataChannel.js"></script>
216 | <script>
217 |     var channel = new DataChannel('default-channel');
218 | 
219 |     // to send text/data or file
220 |     channel.send(file || data || 'text');
221 | </script>
222 | 
223 | 224 |

225 | 226 |

Features:

227 |
    228 |
  1. Send file directly — of any size
  2. 229 |
  3. Send text-message of any length
  4. 230 |
  5. Send data directly
  6. 231 |
  7. Simplest syntax ever! Same like WebSockets.
  8. 232 |
  9. Supports fallback to socket.io/websockets/etc.
  10. 233 |
  11. Auto users' presence detection
  12. 234 |
  13. Allows you eject any user; or close your entire data session
  14. 235 |
236 |
237 |
238 | 239 |

Send direct messages!

240 | 241 |

In many-to-many data session; you can share direct messages or files between specific users:

242 |
243 | channel.channels[userid].send(file || data || 'text message');
244 | 
245 |

Detect users' presence

246 | 247 |

To be alerted if a user leaves your room:

248 |
249 | channel.onleave = function(userid) {
250 |     // remove that user's photo/image using his user-id
251 | };
252 | 
253 |
254 |
255 | 256 |

Manually eject a user or close your data session

257 |
258 | channel.leave(userid);  // throw a user out of your room!
259 | channel.leave();        // close your own entire data session
260 | 
261 | 262 |

Following things will happen if you are a room owner and you tried to close your data session using channel.leave(): 263 |

264 | 265 |
    266 |
  1. The entire data session (i.e. all peers, sockets and data ports) will be closed. 267 | (from each and every user's side) 268 |
  2. 269 |
  3. All participants will be alerted about room owner's (i.e. yours) action. They'll unable to send any single 270 | message over same room because everything is closed! 271 |
  4. 272 |
273 | 274 |

Note

: DataChannel.js will never "auto" reinitiate the data session. 275 |
276 |
277 | 278 |

Additional:

279 |
280 | <script>
281 |     // to be alerted when data ports get open
282 |     channel.onopen = function(userid) {
283 |         // user.photo.id = userid; ---- see "onleave" and "leave" above ↑
284 |         
285 |         // direct messages!
286 |         channel.channels[userid].send(file || data || 'text message');
287 |     }
288 | 	
289 |     // to be alerted when data ports get new message
290 |     channel.onmessage = function(message, userid) {
291 |         // send direct message to same user using his user-id
292 |         channel.channels[userid].send('cool!');
293 |     }
294 | 	
295 |     // show progress bar!
296 |     channel.onFileProgress = function (packets) {
297 |         // packets.remaining
298 |         // packets.sent
299 |         // packets.received
300 |         // packets.length
301 |     };
302 | 
303 |     // on file successfully sent
304 |     channel.onFileSent = function (file) {
305 |         // file.name
306 |         // file.size
307 |     };
308 | 
309 |     // on file successfully received
310 |     channel.onFileReceived = function (fileName) {};
311 | </script>
312 | 
313 |
314 |
315 | 316 |

Errors Handling

317 |
318 | <script>
319 |     // error to open data ports
320 |     channel.onerror = function(event) {}
321 | 	
322 |     // data ports suddenly dropped
323 |     channel.onclose = function(event) {}
324 | </script>
325 | 
326 |
327 |
328 | 329 |

Use your own socket.io for signaling

330 |
331 | <script>
332 |     // by default socket.io is used for signaling; you can override it
333 |     var channel = new DataChannel('default-channel', {
334 |         openSignalingChannel: function(config) {
335 |             var socket = io.connect('http://your-site:8888');
336 |             socket.channel = config.channel || this.channel || 'default-channel';
337 |             socket.on('message', config.onmessage);
338 | 
339 |             socket.send = function (data) {
340 |                 socket.emit('message', data);
341 |             };
342 | 
343 |             if (config.onopen) setTimeout(config.onopen, 1);
344 |             return socket;
345 |         }
346 |     });
347 | </script>
348 | 
349 |
350 |
351 |
352 |

Feedback

353 | 354 |
355 | 358 |
359 | 360 |
361 | 362 |
363 |

Latest Updates

364 |
365 |
366 |
367 | 368 | 369 | 370 | 377 | 378 | 379 | 380 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datachannel", 3 | "version": "1.0.0", 4 | "authors": [ 5 | { 6 | "name": "Muaz Khan", 7 | "email": "muazkh@gmail.com", 8 | "homepage": "http://www.muazkhan.com/" 9 | } 10 | ], 11 | "homepage": "https://www.webrtc-experiment.com/DataChannel/", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/muaz-khan/DataChannel.git" 15 | }, 16 | "description": "DataChannel.js is a JavaScript library useful to write many-to-many i.e. group file/data sharing or text chat applications. Its syntax is easier to use and understand. It highly simplifies complex tasks like any or all user rejection/ejection; direct messages delivery; and more.", 17 | "main": "DataChannel.js", 18 | "keywords": [ 19 | "webrtc", 20 | "datachannel", 21 | "file-sharing", 22 | "data-sharing", 23 | "text-chat", 24 | "chat", 25 | "p2p-streaming", 26 | "data" 27 | ], 28 | "license": "MIT", 29 | "ignore": [ 30 | "**/.*", 31 | "node_modules", 32 | "test", 33 | "tests" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /dev/DataChannel.js: -------------------------------------------------------------------------------- 1 | window.DataChannel = function(channel, extras) { 2 | if (channel) { 3 | this.automatic = true; 4 | } 5 | 6 | this.channel = channel || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''); 7 | 8 | extras = extras || {}; 9 | 10 | var self = this; 11 | var dataConnector; 12 | var fileReceiver; 13 | var textReceiver; 14 | 15 | this.onmessage = function(message, userid) { 16 | console.debug(userid, 'sent message:', message); 17 | }; 18 | 19 | this.channels = {}; 20 | this.onopen = function(userid) { 21 | console.debug(userid, 'is connected with you.'); 22 | }; 23 | 24 | this.onclose = function(event) { 25 | console.error('data channel closed:', event); 26 | }; 27 | 28 | this.onerror = function(event) { 29 | console.error('data channel error:', event); 30 | }; 31 | 32 | // by default; received file will be auto-saved to disk 33 | this.autoSaveToDisk = true; 34 | this.onFileReceived = function(fileName) { 35 | console.debug('File <', fileName, '> received successfully.'); 36 | }; 37 | 38 | this.onFileSent = function(file) { 39 | console.debug('File <', file.name, '> sent successfully.'); 40 | }; 41 | 42 | this.onFileProgress = function(packets) { 43 | console.debug('<', packets.remaining, '> items remaining.'); 44 | }; 45 | 46 | function prepareInit(callback) { 47 | for (var extra in extras) { 48 | self[extra] = extras[extra]; 49 | } 50 | self.direction = self.direction || 'many-to-many'; 51 | if (self.userid) { 52 | window.userid = self.userid; 53 | } 54 | 55 | if (!self.openSignalingChannel) { 56 | if (typeof self.transmitRoomOnce === 'undefined') { 57 | self.transmitRoomOnce = true; 58 | } 59 | 60 | // socket.io over node.js: https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Signaling.md 61 | self.openSignalingChannel = function(config) { 62 | config = config || {}; 63 | 64 | channel = config.channel || self.channel || 'default-channel'; 65 | var socket = new window.Firebase('https://' + (self.firebase || 'webrtc-experiment') + '.firebaseIO.com/' + channel); 66 | socket.channel = channel; 67 | 68 | socket.on('child_added', function(data) { 69 | config.onmessage(data.val()); 70 | }); 71 | 72 | socket.send = function(data) { 73 | this.push(data); 74 | }; 75 | 76 | if (!self.socket) { 77 | self.socket = socket; 78 | } 79 | 80 | if (channel !== self.channel || (self.isInitiator && channel === self.channel)) { 81 | socket.onDisconnect().remove(); 82 | } 83 | 84 | if (config.onopen) { 85 | setTimeout(config.onopen, 1); 86 | } 87 | 88 | return socket; 89 | }; 90 | 91 | if (!window.Firebase) { 92 | var script = document.createElement('script'); 93 | script.src = 'https://cdn.webrtc-experiment.com/firebase.js'; 94 | script.onload = callback; 95 | document.documentElement.appendChild(script); 96 | } else { 97 | callback(); 98 | } 99 | } else { 100 | callback(); 101 | } 102 | } 103 | 104 | function init() { 105 | if (self.config) { 106 | return; 107 | } 108 | 109 | self.config = { 110 | ondatachannel: function(room) { 111 | if (!dataConnector) { 112 | self.room = room; 113 | return; 114 | } 115 | 116 | var tempRoom = { 117 | id: room.roomToken, 118 | owner: room.broadcaster 119 | }; 120 | 121 | if (self.ondatachannel) { 122 | return self.ondatachannel(tempRoom); 123 | } 124 | 125 | if (self.joinedARoom) { 126 | return; 127 | } 128 | 129 | self.joinedARoom = true; 130 | 131 | self.join(tempRoom); 132 | }, 133 | onopen: function(userid, _channel) { 134 | self.onopen(userid, _channel); 135 | self.channels[userid] = { 136 | channel: _channel, 137 | send: function(data) { 138 | self.send(data, this.channel); 139 | } 140 | }; 141 | }, 142 | onmessage: function(data, userid) { 143 | if (IsDataChannelSupported && !data.size) { 144 | data = JSON.parse(data); 145 | } 146 | 147 | if (!IsDataChannelSupported) { 148 | if (data.userid === window.userid) { 149 | return; 150 | } 151 | 152 | data = data.message; 153 | } 154 | 155 | if (data.type === 'text') { 156 | textReceiver.receive(data, self.onmessage, userid); 157 | } else if (typeof data.maxChunks !== 'undefined') { 158 | fileReceiver.receive(data, self); 159 | } else { 160 | self.onmessage(data, userid); 161 | } 162 | }, 163 | onclose: function(event) { 164 | var myChannels = self.channels; 165 | var closedChannel = event.currentTarget; 166 | 167 | for (var userid in myChannels) { 168 | if (closedChannel === myChannels[userid].channel) { 169 | delete myChannels[userid]; 170 | } 171 | } 172 | 173 | self.onclose(event); 174 | }, 175 | openSignalingChannel: self.openSignalingChannel 176 | }; 177 | 178 | dataConnector = IsDataChannelSupported ? 179 | new DataConnector(self, self.config) : 180 | new SocketConnector(self.channel, self.config); 181 | 182 | fileReceiver = new FileReceiver(self); 183 | textReceiver = new TextReceiver(self); 184 | 185 | if (self.room) { 186 | self.config.ondatachannel(self.room); 187 | } 188 | } 189 | 190 | this.open = function(_channel) { 191 | self.joinedARoom = true; 192 | 193 | if (self.socket) { 194 | self.socket.onDisconnect().remove(); 195 | } else { 196 | self.isInitiator = true; 197 | } 198 | 199 | if (_channel) { 200 | self.channel = _channel; 201 | } 202 | 203 | prepareInit(function() { 204 | init(); 205 | if (IsDataChannelSupported) { 206 | dataConnector.createRoom(_channel); 207 | } 208 | }); 209 | }; 210 | 211 | this.connect = function(_channel) { 212 | if (_channel) { 213 | self.channel = _channel; 214 | } 215 | 216 | prepareInit(init); 217 | }; 218 | 219 | // manually join a room 220 | this.join = function(room) { 221 | if (!room.id || !room.owner) { 222 | throw 'Invalid room info passed.'; 223 | } 224 | 225 | if (!dataConnector) { 226 | init(); 227 | } 228 | 229 | if (!dataConnector.joinRoom) { 230 | return; 231 | } 232 | 233 | dataConnector.joinRoom({ 234 | roomToken: room.id, 235 | joinUser: room.owner 236 | }); 237 | }; 238 | 239 | this.send = function(data, _channel) { 240 | if (!data) { 241 | throw 'No file, data or text message to share.'; 242 | } 243 | 244 | if (typeof data.size !== 'undefined' && typeof data.type !== 'undefined') { 245 | FileSender.send({ 246 | file: data, 247 | channel: dataConnector, 248 | onFileSent: function(file) { 249 | self.onFileSent(file); 250 | }, 251 | onFileProgress: function(packets, uuid) { 252 | self.onFileProgress(packets, uuid); 253 | }, 254 | 255 | _channel: _channel, 256 | root: self 257 | }); 258 | 259 | return; 260 | } 261 | TextSender.send({ 262 | text: data, 263 | channel: dataConnector, 264 | _channel: _channel, 265 | root: self 266 | }); 267 | }; 268 | 269 | this.onleave = function(userid) { 270 | console.debug(userid, 'left!'); 271 | }; 272 | 273 | this.leave = this.eject = function(userid) { 274 | dataConnector.leave(userid, self.autoCloseEntireSession); 275 | }; 276 | 277 | this.openNewSession = function(isOpenNewSession, isNonFirebaseClient) { 278 | if (isOpenNewSession) { 279 | if (self.isNewSessionOpened) { 280 | return; 281 | } 282 | self.isNewSessionOpened = true; 283 | 284 | if (!self.joinedARoom) { 285 | self.open(); 286 | } 287 | } 288 | 289 | if (!isOpenNewSession || isNonFirebaseClient) { 290 | self.connect(); 291 | } 292 | 293 | if (!isNonFirebaseClient) { 294 | return; 295 | } 296 | 297 | // for non-firebase clients 298 | 299 | setTimeout(function() { 300 | self.openNewSession(true); 301 | }, 5000); 302 | }; 303 | 304 | if (typeof this.preferSCTP === 'undefined') { 305 | this.preferSCTP = isFirefox || chromeVersion >= 32 ? true : false; 306 | } 307 | 308 | if (typeof this.chunkSize === 'undefined') { 309 | this.chunkSize = isFirefox || chromeVersion >= 32 ? 13 * 1000 : 1000; // 1000 chars for RTP and 13000 chars for SCTP 310 | } 311 | 312 | if (typeof this.chunkInterval === 'undefined') { 313 | this.chunkInterval = isFirefox || chromeVersion >= 32 ? 100 : 500; // 500ms for RTP and 100ms for SCTP 314 | } 315 | 316 | if (self.automatic) { 317 | if (window.Firebase) { 318 | console.debug('checking presence of the room..'); 319 | new window.Firebase('https://' + (extras.firebase || self.firebase || 'muazkh') + '.firebaseIO.com/' + self.channel).once('value', function(data) { 320 | console.debug('room is present?', data.val() !== null); 321 | self.openNewSession(data.val() === null); 322 | }); 323 | } else { 324 | self.openNewSession(false, true); 325 | } 326 | } 327 | }; 328 | -------------------------------------------------------------------------------- /dev/DataConnector.js: -------------------------------------------------------------------------------- 1 | function DataConnector(root, config) { 2 | var self = {}; 3 | var that = this; 4 | 5 | self.userToken = (root.userid = root.userid || uniqueToken()).toString(); 6 | self.sockets = []; 7 | self.socketObjects = {}; 8 | 9 | var channels = '--'; 10 | var isbroadcaster = false; 11 | var isGetNewRoom = true; 12 | var rtcDataChannels = []; 13 | 14 | function newPrivateSocket(_config) { 15 | var socketConfig = { 16 | channel: _config.channel, 17 | onmessage: socketResponse, 18 | onopen: function() { 19 | if (isofferer && !peer) { 20 | initPeer(); 21 | } 22 | 23 | _config.socketIndex = socket.index = self.sockets.length; 24 | self.socketObjects[socketConfig.channel] = socket; 25 | self.sockets[_config.socketIndex] = socket; 26 | } 27 | }; 28 | 29 | socketConfig.callback = function(_socket) { 30 | socket = _socket; 31 | socketConfig.onopen(); 32 | }; 33 | 34 | var socket = root.openSignalingChannel(socketConfig); 35 | var isofferer = _config.isofferer; 36 | var gotstream; 37 | var inner = {}; 38 | var peer; 39 | 40 | var peerConfig = { 41 | onICE: function(candidate) { 42 | if (!socket) { 43 | return setTimeout(function() { 44 | peerConfig.onICE(candidate); 45 | }, 2000); 46 | } 47 | 48 | socket.send({ 49 | userToken: self.userToken, 50 | candidate: { 51 | sdpMLineIndex: candidate.sdpMLineIndex, 52 | candidate: JSON.stringify(candidate.candidate) 53 | } 54 | }); 55 | }, 56 | onopen: onChannelOpened, 57 | onmessage: function(event) { 58 | config.onmessage(event.data, _config.userid); 59 | }, 60 | onclose: config.onclose, 61 | onerror: root.onerror, 62 | preferSCTP: root.preferSCTP 63 | }; 64 | 65 | function initPeer(offerSDP) { 66 | if (root.direction === 'one-to-one' && window.isFirstConnectionOpened) { 67 | return; 68 | } 69 | 70 | if (!offerSDP) { 71 | peerConfig.onOfferSDP = sendsdp; 72 | } else { 73 | peerConfig.offerSDP = offerSDP; 74 | peerConfig.onAnswerSDP = sendsdp; 75 | } 76 | 77 | peer = new RTCPeerConnection(peerConfig); 78 | } 79 | 80 | function onChannelOpened(channel) { 81 | channel.peer = peer.peer; 82 | rtcDataChannels.push(channel); 83 | 84 | config.onopen(_config.userid, channel); 85 | 86 | if (root.direction === 'many-to-many' && isbroadcaster && channels.split('--').length > 3 && defaultSocket) { 87 | defaultSocket.send({ 88 | newParticipant: socket.channel, 89 | userToken: self.userToken 90 | }); 91 | } 92 | 93 | window.isFirstConnectionOpened = gotstream = true; 94 | } 95 | 96 | function sendsdp(sdp) { 97 | sdp = JSON.stringify(sdp); 98 | var part = parseInt(sdp.length / 3); 99 | 100 | var firstPart = sdp.slice(0, part), 101 | secondPart = sdp.slice(part, sdp.length - 1), 102 | thirdPart = ''; 103 | 104 | if (sdp.length > part + part) { 105 | secondPart = sdp.slice(part, part + part); 106 | thirdPart = sdp.slice(part + part, sdp.length); 107 | } 108 | 109 | socket.send({ 110 | userToken: self.userToken, 111 | firstPart: firstPart 112 | }); 113 | 114 | socket.send({ 115 | userToken: self.userToken, 116 | secondPart: secondPart 117 | }); 118 | 119 | socket.send({ 120 | userToken: self.userToken, 121 | thirdPart: thirdPart 122 | }); 123 | } 124 | 125 | function socketResponse(response) { 126 | if (response.userToken === self.userToken) { 127 | return; 128 | } 129 | 130 | if (response.firstPart || response.secondPart || response.thirdPart) { 131 | if (response.firstPart) { 132 | // sdp sender's user id passed over "onopen" method 133 | _config.userid = response.userToken; 134 | 135 | inner.firstPart = response.firstPart; 136 | if (inner.secondPart && inner.thirdPart) { 137 | selfInvoker(); 138 | } 139 | } 140 | if (response.secondPart) { 141 | inner.secondPart = response.secondPart; 142 | if (inner.firstPart && inner.thirdPart) { 143 | selfInvoker(); 144 | } 145 | } 146 | 147 | if (response.thirdPart) { 148 | inner.thirdPart = response.thirdPart; 149 | if (inner.firstPart && inner.secondPart) { 150 | selfInvoker(); 151 | } 152 | } 153 | } 154 | 155 | if (response.candidate && !gotstream && peer) { 156 | if (!inner.firstPart || !inner.secondPart || !inner.thirdPart) { 157 | return setTimeout(function() { 158 | socketResponse(response); 159 | }, 400); 160 | } 161 | 162 | peer.addICE({ 163 | sdpMLineIndex: response.candidate.sdpMLineIndex, 164 | candidate: JSON.parse(response.candidate.candidate) 165 | }); 166 | 167 | console.debug('ice candidate', response.candidate.candidate); 168 | } 169 | 170 | if (response.left) { 171 | if (peer && peer.peer) { 172 | peer.peer.close(); 173 | peer.peer = null; 174 | } 175 | 176 | if (response.closeEntireSession) { 177 | leaveChannels(); 178 | } else if (socket) { 179 | socket.send({ 180 | left: true, 181 | userToken: self.userToken 182 | }); 183 | socket = null; 184 | } 185 | 186 | root.onleave(response.userToken); 187 | } 188 | 189 | if (response.playRoleOfBroadcaster) { 190 | setTimeout(function() { 191 | self.roomToken = response.roomToken; 192 | root.open(self.roomToken); 193 | self.sockets = swap(self.sockets); 194 | }, 600); 195 | } 196 | } 197 | 198 | var invokedOnce = false; 199 | 200 | function selfInvoker() { 201 | if (invokedOnce) { 202 | return; 203 | } 204 | 205 | invokedOnce = true; 206 | inner.sdp = JSON.parse(inner.firstPart + inner.secondPart + inner.thirdPart); 207 | 208 | if (isofferer) { 209 | peer.addAnswerSDP(inner.sdp); 210 | } else { 211 | initPeer(inner.sdp); 212 | } 213 | 214 | console.debug('sdp', inner.sdp.sdp); 215 | } 216 | } 217 | 218 | function onNewParticipant(channel) { 219 | if (!channel || channels.indexOf(channel) !== -1 || channel === self.userToken) { 220 | return; 221 | } 222 | 223 | channels += channel + '--'; 224 | 225 | var newChannel = uniqueToken(); 226 | 227 | newPrivateSocket({ 228 | channel: newChannel, 229 | closeSocket: true 230 | }); 231 | 232 | if (!defaultSocket) { 233 | return; 234 | } 235 | 236 | defaultSocket.send({ 237 | participant: true, 238 | userToken: self.userToken, 239 | joinUser: channel, 240 | channel: newChannel 241 | }); 242 | } 243 | 244 | function uniqueToken() { 245 | return (Math.round(Math.random() * 60535) + 5000000).toString(); 246 | } 247 | 248 | function leaveChannels(channel) { 249 | var alert = { 250 | left: true, 251 | userToken: self.userToken 252 | }; 253 | 254 | var socket; 255 | 256 | // if room initiator is leaving the room; close the entire session 257 | if (isbroadcaster) { 258 | if (root.autoCloseEntireSession) { 259 | alert.closeEntireSession = true; 260 | } else { 261 | self.sockets[0].send({ 262 | playRoleOfBroadcaster: true, 263 | userToken: self.userToken, 264 | roomToken: self.roomToken 265 | }); 266 | } 267 | } 268 | 269 | if (!channel) { 270 | // closing all sockets 271 | var sockets = self.sockets, 272 | length = sockets.length; 273 | 274 | for (var i = 0; i < length; i++) { 275 | socket = sockets[i]; 276 | if (socket) { 277 | socket.send(alert); 278 | 279 | if (self.socketObjects[socket.channel]) { 280 | delete self.socketObjects[socket.channel]; 281 | } 282 | 283 | delete sockets[i]; 284 | } 285 | } 286 | 287 | that.left = true; 288 | } 289 | 290 | // eject a specific user! 291 | if (channel) { 292 | socket = self.socketObjects[channel]; 293 | if (socket) { 294 | socket.send(alert); 295 | 296 | if (self.sockets[socket.index]) { 297 | delete self.sockets[socket.index]; 298 | } 299 | 300 | delete self.socketObjects[channel]; 301 | } 302 | } 303 | self.sockets = swap(self.sockets); 304 | } 305 | 306 | window.addEventListener('beforeunload', function() { 307 | leaveChannels(); 308 | }, false); 309 | 310 | window.addEventListener('keydown', function(e) { 311 | if (e.keyCode === 116) { 312 | leaveChannels(); 313 | } 314 | }, false); 315 | 316 | var defaultSocket = root.openSignalingChannel({ 317 | onmessage: function(response) { 318 | if (response.userToken === self.userToken) { 319 | return; 320 | } 321 | 322 | if (isGetNewRoom && response.roomToken && response.broadcaster) { 323 | config.ondatachannel(response); 324 | } 325 | 326 | if (response.newParticipant) { 327 | onNewParticipant(response.newParticipant); 328 | } 329 | 330 | if (response.userToken && response.joinUser === self.userToken && response.participant && channels.indexOf(response.userToken) === -1) { 331 | channels += response.userToken + '--'; 332 | 333 | console.debug('Data connection is being opened between you and', response.userToken || response.channel); 334 | newPrivateSocket({ 335 | isofferer: true, 336 | channel: response.channel || response.userToken, 337 | closeSocket: true 338 | }); 339 | } 340 | }, 341 | callback: function(socket) { 342 | defaultSocket = socket; 343 | } 344 | }); 345 | 346 | return { 347 | createRoom: function(roomToken) { 348 | self.roomToken = (roomToken || uniqueToken()).toString(); 349 | 350 | isbroadcaster = true; 351 | isGetNewRoom = false; 352 | 353 | (function transmit() { 354 | if (defaultSocket) { 355 | defaultSocket.send({ 356 | roomToken: self.roomToken, 357 | broadcaster: self.userToken 358 | }); 359 | } 360 | 361 | if (!root.transmitRoomOnce && !that.leaving) { 362 | if (root.direction === 'one-to-one') { 363 | if (!window.isFirstConnectionOpened) { 364 | setTimeout(transmit, 3000); 365 | } 366 | } else { 367 | setTimeout(transmit, 3000); 368 | } 369 | } 370 | })(); 371 | }, 372 | joinRoom: function(_config) { 373 | self.roomToken = _config.roomToken; 374 | isGetNewRoom = false; 375 | 376 | newPrivateSocket({ 377 | channel: self.userToken 378 | }); 379 | 380 | defaultSocket.send({ 381 | participant: true, 382 | userToken: self.userToken, 383 | joinUser: _config.joinUser 384 | }); 385 | }, 386 | send: function(message, _channel) { 387 | var _channels = rtcDataChannels; 388 | var data; 389 | var length = _channels.length; 390 | 391 | if (!length) { 392 | return; 393 | } 394 | 395 | data = JSON.stringify(message); 396 | 397 | if (_channel) { 398 | if (_channel.readyState === 'open') { 399 | _channel.send(data); 400 | } 401 | return; 402 | } 403 | for (var i = 0; i < length; i++) { 404 | if (_channels[i].readyState === 'open') { 405 | _channels[i].send(data); 406 | } 407 | } 408 | }, 409 | leave: function(userid, autoCloseEntireSession) { 410 | if (autoCloseEntireSession) { 411 | root.autoCloseEntireSession = true; 412 | } 413 | leaveChannels(userid); 414 | if (!userid) { 415 | self.joinedARoom = isbroadcaster = false; 416 | isGetNewRoom = true; 417 | } 418 | } 419 | }; 420 | } 421 | -------------------------------------------------------------------------------- /dev/FileConverter.js: -------------------------------------------------------------------------------- 1 | var FileConverter = { 2 | DataURLToBlob: function(dataURL, fileType, callback) { 3 | 4 | function processInWebWorker() { 5 | var blob = URL.createObjectURL(new Blob(['function getBlob(_dataURL, _fileType) {var binary = atob(_dataURL.substr(_dataURL.indexOf(",") + 1)),i = binary.length,view = new Uint8Array(i);while (i--) {view[i] = binary.charCodeAt(i);};postMessage(new Blob([view], {type: _fileType}));};this.onmessage = function (e) {var data = JSON.parse(e.data); getBlob(data.dataURL, data.fileType);}'], { 6 | type: 'application/javascript' 7 | })); 8 | 9 | var worker = new Worker(blob); 10 | URL.revokeObjectURL(blob); 11 | return worker; 12 | } 13 | 14 | if (!!window.Worker && !isMobileDevice) { 15 | var webWorker = processInWebWorker(); 16 | 17 | webWorker.onmessage = function(event) { 18 | callback(event.data); 19 | }; 20 | 21 | webWorker.postMessage(JSON.stringify({ 22 | dataURL: dataURL, 23 | fileType: fileType 24 | })); 25 | } else { 26 | var binary = atob(dataURL.substr(dataURL.indexOf(',') + 1)), 27 | i = binary.length, 28 | view = new Uint8Array(i); 29 | 30 | while (i--) { 31 | view[i] = binary.charCodeAt(i); 32 | } 33 | 34 | callback(new Blob([view])); 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /dev/FileReceiver.js: -------------------------------------------------------------------------------- 1 | function FileReceiver(root) { 2 | var content = {}; 3 | var packets = {}; 4 | var numberOfPackets = {}; 5 | 6 | function receive(data) { 7 | var uuid = data.uuid; 8 | 9 | if (typeof data.packets !== 'undefined') { 10 | numberOfPackets[uuid] = packets[uuid] = parseInt(data.packets); 11 | } 12 | 13 | if (root.onFileProgress) { 14 | root.onFileProgress({ 15 | remaining: packets[uuid]--, 16 | length: numberOfPackets[uuid], 17 | received: numberOfPackets[uuid] - packets[uuid], 18 | 19 | maxChunks: numberOfPackets[uuid], 20 | uuid: uuid, 21 | currentPosition: numberOfPackets[uuid] - packets[uuid] 22 | }, uuid); 23 | } 24 | 25 | if (!content[uuid]) { 26 | content[uuid] = []; 27 | } 28 | 29 | content[uuid].push(data.message); 30 | 31 | if (data.last) { 32 | var dataURL = content[uuid].join(''); 33 | 34 | FileConverter.DataURLToBlob(dataURL, data.fileType, function(blob) { 35 | blob.uuid = uuid; 36 | blob.name = data.name; 37 | // blob.type = data.fileType; 38 | blob.extra = data.extra || {}; 39 | 40 | blob.url = (window.URL || window.webkitURL).createObjectURL(blob); 41 | 42 | if (root.autoSaveToDisk) { 43 | FileSaver.SaveToDisk(blob.url, data.name); 44 | } 45 | 46 | if (root.onFileReceived) { 47 | root.onFileReceived(blob); 48 | } 49 | 50 | delete content[uuid]; 51 | }); 52 | } 53 | } 54 | 55 | return { 56 | receive: receive 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /dev/FileSaver.js: -------------------------------------------------------------------------------- 1 | var FileSaver = { 2 | SaveToDisk: function(fileUrl, fileName) { 3 | var hyperlink = document.createElement('a'); 4 | hyperlink.href = fileUrl; 5 | hyperlink.target = '_blank'; 6 | hyperlink.download = fileName || fileUrl; 7 | 8 | var mouseEvent = new MouseEvent('click', { 9 | view: window, 10 | bubbles: true, 11 | cancelable: true 12 | }); 13 | 14 | hyperlink.dispatchEvent(mouseEvent); 15 | (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /dev/FileSender.js: -------------------------------------------------------------------------------- 1 | var FileSender = { 2 | send: function(config) { 3 | var root = config.root; 4 | var channel = config.channel; 5 | var privateChannel = config._channel; 6 | var file = config.file; 7 | 8 | if (!config.file) { 9 | console.error('You must attach/select a file.'); 10 | return; 11 | } 12 | 13 | // max chunk sending limit on chrome is 64k 14 | // max chunk receiving limit on firefox is 16k 15 | var packetSize = 15 * 1000; 16 | 17 | if (root.chunkSize) { 18 | packetSize = root.chunkSize; 19 | } 20 | 21 | var textToTransfer = ''; 22 | var numberOfPackets = 0; 23 | var packets = 0; 24 | 25 | file.uuid = getRandomString(); 26 | 27 | function processInWebWorker() { 28 | var blob = URL.createObjectURL(new Blob(['function readFile(_file) {postMessage(new FileReaderSync().readAsDataURL(_file));};this.onmessage = function (e) {readFile(e.data);}'], { 29 | type: 'application/javascript' 30 | })); 31 | 32 | var worker = new Worker(blob); 33 | URL.revokeObjectURL(blob); 34 | return worker; 35 | } 36 | 37 | if (!!window.Worker && !isMobileDevice) { 38 | var webWorker = processInWebWorker(); 39 | 40 | webWorker.onmessage = function(event) { 41 | onReadAsDataURL(event.data); 42 | }; 43 | 44 | webWorker.postMessage(file); 45 | } else { 46 | var reader = new FileReader(); 47 | reader.onload = function(e) { 48 | onReadAsDataURL(e.target.result); 49 | }; 50 | reader.readAsDataURL(file); 51 | } 52 | 53 | function onReadAsDataURL(dataURL, text) { 54 | var data = { 55 | type: 'file', 56 | uuid: file.uuid, 57 | maxChunks: numberOfPackets, 58 | currentPosition: numberOfPackets - packets, 59 | name: file.name, 60 | fileType: file.type, 61 | size: file.size 62 | }; 63 | 64 | if (dataURL) { 65 | text = dataURL; 66 | numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); 67 | 68 | file.maxChunks = data.maxChunks = numberOfPackets; 69 | data.currentPosition = numberOfPackets - packets; 70 | 71 | if (root.onFileSent) { 72 | root.onFileSent(file); 73 | } 74 | } 75 | 76 | if (root.onFileProgress) { 77 | root.onFileProgress({ 78 | remaining: packets--, 79 | length: numberOfPackets, 80 | sent: numberOfPackets - packets, 81 | 82 | maxChunks: numberOfPackets, 83 | uuid: file.uuid, 84 | currentPosition: numberOfPackets - packets 85 | }, file.uuid); 86 | } 87 | 88 | if (text.length > packetSize) { 89 | data.message = text.slice(0, packetSize); 90 | } else { 91 | data.message = text; 92 | data.last = true; 93 | data.name = file.name; 94 | 95 | file.url = URL.createObjectURL(file); 96 | root.onFileSent(file, file.uuid); 97 | } 98 | 99 | channel.send(data, privateChannel); 100 | 101 | textToTransfer = text.slice(data.message.length); 102 | if (textToTransfer.length) { 103 | setTimeout(function() { 104 | onReadAsDataURL(null, textToTransfer); 105 | }, root.chunkInterval || 100); 106 | } 107 | } 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /dev/IceServersHandler.js: -------------------------------------------------------------------------------- 1 | // IceServersHandler.js 2 | 3 | var IceServersHandler = (function() { 4 | function getIceServers(connection) { 5 | var iceServers = []; 6 | 7 | iceServers.push(getSTUNObj('stun:stun.l.google.com:19302')); 8 | 9 | iceServers.push(getTURNObj('stun:webrtcweb.com:7788', 'muazkh', 'muazkh')); // coTURN 10 | iceServers.push(getTURNObj('turn:webrtcweb.com:7788', 'muazkh', 'muazkh')); // coTURN 11 | iceServers.push(getTURNObj('turn:webrtcweb.com:8877', 'muazkh', 'muazkh')); // coTURN 12 | 13 | iceServers.push(getTURNObj('turns:webrtcweb.com:7788', 'muazkh', 'muazkh')); // coTURN 14 | iceServers.push(getTURNObj('turns:webrtcweb.com:8877', 'muazkh', 'muazkh')); // coTURN 15 | 16 | // iceServers.push(getTURNObj('turn:webrtcweb.com:3344', 'muazkh', 'muazkh')); // resiprocate 17 | // iceServers.push(getTURNObj('turn:webrtcweb.com:4433', 'muazkh', 'muazkh')); // resiprocate 18 | 19 | // check if restund is still active: http://webrtcweb.com:4050/ 20 | iceServers.push(getTURNObj('stun:webrtcweb.com:4455', 'muazkh', 'muazkh')); // restund 21 | iceServers.push(getTURNObj('turn:webrtcweb.com:4455', 'muazkh', 'muazkh')); // restund 22 | iceServers.push(getTURNObj('turn:webrtcweb.com:5544?transport=tcp', 'muazkh', 'muazkh')); // restund 23 | 24 | return iceServers; 25 | } 26 | 27 | function getSTUNObj(stunStr) { 28 | var urlsParam = 'urls'; 29 | if (typeof isPluginRTC !== 'undefined') { 30 | urlsParam = 'url'; 31 | } 32 | 33 | var obj = {}; 34 | obj[urlsParam] = stunStr; 35 | return obj; 36 | } 37 | 38 | function getTURNObj(turnStr, username, credential) { 39 | var urlsParam = 'urls'; 40 | if (typeof isPluginRTC !== 'undefined') { 41 | urlsParam = 'url'; 42 | } 43 | 44 | var obj = { 45 | username: username, 46 | credential: credential 47 | }; 48 | obj[urlsParam] = turnStr; 49 | return obj; 50 | } 51 | 52 | return { 53 | getIceServers: getIceServers 54 | }; 55 | })(); 56 | -------------------------------------------------------------------------------- /dev/RTCPeerConnection.js: -------------------------------------------------------------------------------- 1 | function RTCPeerConnection(options) { 2 | var w = window; 3 | var PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection; 4 | var SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription; 5 | var IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; 6 | 7 | var iceServers = { 8 | iceServers: IceServersHandler.getIceServers() 9 | }; 10 | 11 | var optional = { 12 | optional: [] 13 | }; 14 | 15 | if (!navigator.onLine) { 16 | iceServers = null; 17 | console.warn('No internet connection detected. No STUN/TURN server is used to make sure local/host candidates are used for peers connection.'); 18 | } 19 | 20 | var peerConnection = new PeerConnection(iceServers, optional); 21 | 22 | openOffererChannel(); 23 | peerConnection.onicecandidate = onicecandidate; 24 | 25 | function onicecandidate(event) { 26 | if (!event.candidate || !peerConnection) { 27 | return; 28 | } 29 | 30 | if (options.onICE) { 31 | options.onICE(event.candidate); 32 | } 33 | } 34 | 35 | var constraints = options.constraints || { 36 | optional: [], 37 | mandatory: { 38 | OfferToReceiveAudio: false, 39 | OfferToReceiveVideo: false 40 | } 41 | }; 42 | 43 | function onSdpError(e) { 44 | var message = JSON.stringify(e, null, '\t'); 45 | 46 | if (message.indexOf('RTP/SAVPF Expects at least 4 fields') !== -1) { 47 | message = 'It seems that you are trying to interop RTP-datachannels with SCTP. It is not supported!'; 48 | } 49 | 50 | console.error('onSdpError:', message); 51 | } 52 | 53 | function onSdpSuccess() {} 54 | 55 | function createOffer() { 56 | if (!options.onOfferSDP) { 57 | return; 58 | } 59 | 60 | peerConnection.createOffer(function(sessionDescription) { 61 | peerConnection.setLocalDescription(sessionDescription); 62 | options.onOfferSDP(sessionDescription); 63 | }, onSdpError, constraints); 64 | } 65 | 66 | function createAnswer() { 67 | if (!options.onAnswerSDP) { 68 | return; 69 | } 70 | 71 | options.offerSDP = new SessionDescription(options.offerSDP); 72 | peerConnection.setRemoteDescription(options.offerSDP, onSdpSuccess, onSdpError); 73 | 74 | peerConnection.createAnswer(function(sessionDescription) { 75 | peerConnection.setLocalDescription(sessionDescription); 76 | options.onAnswerSDP(sessionDescription); 77 | }, onSdpError, constraints); 78 | } 79 | 80 | if (!moz) { 81 | createOffer(); 82 | createAnswer(); 83 | } 84 | 85 | var channel; 86 | 87 | function openOffererChannel() { 88 | if (moz && !options.onOfferSDP) { 89 | return; 90 | } 91 | 92 | if (!moz && !options.onOfferSDP) { 93 | return; 94 | } 95 | 96 | _openOffererChannel(); 97 | if (moz) { 98 | createOffer(); 99 | } 100 | } 101 | 102 | function _openOffererChannel() { 103 | // protocol: 'text/chat', preset: true, stream: 16 104 | // maxRetransmits:0 && ordered:false 105 | var dataChannelDict = {}; 106 | 107 | console.debug('dataChannelDict', dataChannelDict); 108 | 109 | channel = peerConnection.createDataChannel('channel', dataChannelDict); 110 | setChannelEvents(); 111 | } 112 | 113 | function setChannelEvents() { 114 | channel.onmessage = options.onmessage; 115 | channel.onopen = function() { 116 | options.onopen(channel); 117 | }; 118 | channel.onclose = options.onclose; 119 | channel.onerror = options.onerror; 120 | } 121 | 122 | if (options.onAnswerSDP && moz && options.onmessage) { 123 | openAnswererChannel(); 124 | } 125 | 126 | if (!moz && !options.onOfferSDP) { 127 | openAnswererChannel(); 128 | } 129 | 130 | function openAnswererChannel() { 131 | peerConnection.ondatachannel = function(event) { 132 | channel = event.channel; 133 | setChannelEvents(); 134 | }; 135 | 136 | if (moz) { 137 | createAnswer(); 138 | } 139 | } 140 | 141 | function useless() {} 142 | 143 | return { 144 | addAnswerSDP: function(sdp) { 145 | sdp = new SessionDescription(sdp); 146 | peerConnection.setRemoteDescription(sdp, onSdpSuccess, onSdpError); 147 | }, 148 | addICE: function(candidate) { 149 | peerConnection.addIceCandidate(new IceCandidate({ 150 | sdpMLineIndex: candidate.sdpMLineIndex, 151 | candidate: candidate.candidate 152 | })); 153 | }, 154 | 155 | peer: peerConnection, 156 | channel: channel, 157 | sendData: function(message) { 158 | if (!channel) { 159 | return; 160 | } 161 | 162 | channel.send(message); 163 | } 164 | }; 165 | } 166 | -------------------------------------------------------------------------------- /dev/SocketConnector.js: -------------------------------------------------------------------------------- 1 | function SocketConnector(_channel, config) { 2 | var socket = config.openSignalingChannel({ 3 | channel: _channel, 4 | onopen: config.onopen, 5 | onmessage: config.onmessage, 6 | callback: function(_socket) { 7 | socket = _socket; 8 | } 9 | }); 10 | 11 | return { 12 | send: function(message) { 13 | if (!socket) { 14 | return; 15 | } 16 | 17 | socket.send({ 18 | userid: userid, 19 | message: message 20 | }); 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /dev/TextReceiver.js: -------------------------------------------------------------------------------- 1 | function TextReceiver() { 2 | var content = {}; 3 | 4 | function receive(data, onmessage, userid) { 5 | // uuid is used to uniquely identify sending instance 6 | var uuid = data.uuid; 7 | if (!content[uuid]) { 8 | content[uuid] = []; 9 | } 10 | 11 | content[uuid].push(data.message); 12 | if (data.last) { 13 | var message = content[uuid].join(''); 14 | if (data.isobject) { 15 | message = JSON.parse(message); 16 | } 17 | 18 | // latency detection 19 | var receivingTime = new Date().getTime(); 20 | var latency = receivingTime - data.sendingTime; 21 | 22 | onmessage(message, userid, latency); 23 | 24 | delete content[uuid]; 25 | } 26 | } 27 | 28 | return { 29 | receive: receive 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /dev/TextSender.js: -------------------------------------------------------------------------------- 1 | var TextSender = { 2 | send: function(config) { 3 | var root = config.root; 4 | 5 | var channel = config.channel; 6 | var _channel = config._channel; 7 | var initialText = config.text; 8 | var packetSize = root.chunkSize || 1000; 9 | var textToTransfer = ''; 10 | var isobject = false; 11 | 12 | if (typeof initialText !== 'string') { 13 | isobject = true; 14 | initialText = JSON.stringify(initialText); 15 | } 16 | 17 | // uuid is used to uniquely identify sending instance 18 | var uuid = getRandomString(); 19 | var sendingTime = new Date().getTime(); 20 | 21 | sendText(initialText); 22 | 23 | function sendText(textMessage, text) { 24 | var data = { 25 | type: 'text', 26 | uuid: uuid, 27 | sendingTime: sendingTime 28 | }; 29 | 30 | if (textMessage) { 31 | text = textMessage; 32 | data.packets = parseInt(text.length / packetSize); 33 | } 34 | 35 | if (text.length > packetSize) { 36 | data.message = text.slice(0, packetSize); 37 | } else { 38 | data.message = text; 39 | data.last = true; 40 | data.isobject = isobject; 41 | } 42 | 43 | channel.send(data, _channel); 44 | 45 | textToTransfer = text.slice(data.message.length); 46 | 47 | if (textToTransfer.length) { 48 | setTimeout(function() { 49 | sendText(null, textToTransfer); 50 | }, root.chunkInterval || 100); 51 | } 52 | } 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /dev/externalIceServers.js: -------------------------------------------------------------------------------- 1 | var loadedIceFrame; 2 | 3 | function loadIceFrame(callback, skip) { 4 | if (loadedIceFrame) { 5 | return; 6 | } 7 | 8 | if (!skip) { 9 | return loadIceFrame(callback, true); 10 | } 11 | 12 | loadedIceFrame = true; 13 | 14 | var iframe = document.createElement('iframe'); 15 | iframe.onload = function() { 16 | iframe.isLoaded = true; 17 | 18 | listenEventHandler('message', iFrameLoaderCallback); 19 | 20 | function iFrameLoaderCallback(event) { 21 | if (!event.data || !event.data.iceServers) { 22 | return; 23 | } 24 | callback(event.data.iceServers); 25 | 26 | // this event listener is no more needed 27 | window.removeEventListener('message', iFrameLoaderCallback); 28 | } 29 | 30 | iframe.contentWindow.postMessage('get-ice-servers', '*'); 31 | }; 32 | iframe.src = 'https://cdn.webrtc-experiment.com/getIceServers/'; 33 | iframe.style.display = 'none'; 34 | (document.body || document.documentElement).appendChild(iframe); 35 | } 36 | 37 | loadIceFrame(function(iceServers) { 38 | window.iceServers = iceServers; 39 | }); 40 | -------------------------------------------------------------------------------- /dev/globals.js: -------------------------------------------------------------------------------- 1 | var moz = !!navigator.mozGetUserMedia; 2 | var IsDataChannelSupported = !((moz && !navigator.mozGetUserMedia) || (!moz && !navigator.webkitGetUserMedia)); 3 | 4 | function getRandomString() { 5 | return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, '-'); 6 | } 7 | 8 | var userid = getRandomString(); 9 | 10 | var isMobileDevice = navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|IEMobile/i); 11 | var isChrome = !!navigator.webkitGetUserMedia; 12 | var isFirefox = !!navigator.mozGetUserMedia; 13 | 14 | var chromeVersion = 50; 15 | if (isChrome) { 16 | chromeVersion = !!navigator.mozGetUserMedia ? 0 : parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]); 17 | } 18 | 19 | function swap(arr) { 20 | var swapped = []; 21 | var length = arr.length; 22 | 23 | for (var i = 0; i < length; i++) { 24 | if (arr[i]) { 25 | swapped.push(arr[i]); 26 | } 27 | } 28 | 29 | return swapped; 30 | } 31 | 32 | function listenEventHandler(eventName, eventHandler) { 33 | window.removeEventListener(eventName, eventHandler); 34 | window.addEventListener(eventName, eventHandler, false); 35 | } 36 | -------------------------------------------------------------------------------- /dev/head.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | -------------------------------------------------------------------------------- /dev/tail.js: -------------------------------------------------------------------------------- 1 | })(); 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | DataChannel.js » A WebRTC Library for Data Sharing 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 |

58 | DataChannel.js 59 | » A WebRTC Library for Data Sharing 60 |

61 |

62 | HOME 63 | © 64 | Muaz Khan 65 | 66 | . 67 | @WebRTCWeb 68 | 69 | . 70 | Github 71 | 72 | . 73 | Latest issues 74 | 75 | . 76 | What's New? 77 |

78 |
79 | 80 |
81 | 82 | 83 |
84 |
85 |

Open New DataChannel Connection

86 | 87 |
88 | 89 |
90 | 91 | 92 | 98 | 104 | 105 |
93 |

Text Chat

94 | 95 |
96 | 97 |
99 |

Share Files

100 | 101 | 102 |
103 |
106 |
107 | 108 | 243 | 244 |
245 |

DataChannel.js Features:

246 |
    247 |
  1. Direct messages — to any user using his `user-id`
  2. 248 |
  3. Eject/Reject any user — using his `user-id`
  4. 249 |
  5. Leave any room (i.e. data session) or close entire session using `leave` method
  6. 250 |
  7. File size is limitless!
  8. 251 |
  9. Text message length is limitless!
  10. 252 |
  11. Size of data is also limitless!
  12. 253 |
  13. Fallback to socket.io/websockets/etc.
  14. 254 |
  15. Users' presence detection using `onleave`
  16. 255 |
  17. Latency detection
  18. 256 |
  19. Multi-longest strings/files concurrent transmission
  20. 257 |
258 |
259 | 260 |
261 |

How to use DataChannel.js?

262 |
263 | <script src="https://cdn.webrtc-experiment.com/DataChannel.js"> </script>
264 | 
265 | <input type="text" id="chat-input" disabled 
266 |        style="font-size: 2em; width: 98%;"><br />
267 | <div id="chat-output"></div>
268 | 
269 | <script>
270 |     var chatOutput = document.getElementById('chat-output');
271 |     var chatInput = document.getElementById('chat-input');
272 |     chatInput.onkeypress = function(e) {
273 |         if (e.keyCode != 13) return;
274 |         channel.send(this.value);
275 |         chatOutput.innerHTML = 'Me: ' + this.value + '<hr />' 
276 |                              + chatOutput.innerHTML;
277 |         this.value = '';
278 |     };
279 | </script>
280 | 
281 | <script>
282 |     var channel = new DataChannel('Session Unique Identifier');
283 | 
284 |     channel.onopen = function(userid) {
285 |         chatInput.disabled = false;
286 |         chatInput.value = 'Hi, ' + userid;
287 |         chatInput.focus();
288 |     };
289 | 
290 |     channel.onmessage = function(message, userid) {
291 |         chatOutput.innerHTML = userid + ': ' + message + '<hr />' 
292 |                              + chatOutput.innerHTML;
293 |     };
294 | 
295 |     channel.onleave = function(userid) {
296 |         chatOutput.innerHTML = userid + ' Left.<hr />' 
297 |                              + chatOutput.innerHTML;
298 |     };
299 | </script>
300 | 
301 |
302 | 303 |
304 |

Use your own socket.io for signaling

305 |
306 | <script>
307 |     // by default socket.io is used for signaling; you can override it
308 |     channel.openSignalingChannel = function(config) {
309 |         var socket = io.connect('http://your-site:8888');
310 |         socket.channel = config.channel || this.channel || 'default-channel';
311 |         socket.on('message', config.onmessage);
312 | 
313 |         socket.send = function (data) {
314 |             socket.emit('message', data);
315 |         };
316 | 
317 |         if (config.onopen) setTimeout(config.onopen, 1);
318 |         return socket;
319 |     }
320 | </script>
321 | 
322 |
323 | 324 |
325 |

Latest Updates

326 |
327 |
328 | 329 |
330 |

Feedback

331 |
332 | 333 |
334 | Enter your email too; if you want "direct" reply! 335 |
336 |
337 | 338 | 339 | 340 | 347 | 348 | 351 | 352 | 353 | 354 | 355 | 356 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datachannel", 3 | "preferGlobal": false, 4 | "version": "1.0.0", 5 | "author": { 6 | "name": "Muaz Khan", 7 | "email": "muazkh@gmail.com", 8 | "url": "http://www.muazkhan.com/" 9 | }, 10 | "description": "DataChannel.js is a JavaScript library useful to write many-to-many i.e. group file/data sharing or text chat applications. Its syntax is easier to use and understand. It highly simplifies complex tasks like any or all user rejection/ejection; direct messages delivery; and more.", 11 | "scripts": { 12 | "start": "node DataChannel.js" 13 | }, 14 | "main": "./DataChannel.js", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/muaz-khan/DataChannel.git" 18 | }, 19 | "keywords": [ 20 | "webrtc", 21 | "datachannel", 22 | "file-sharing", 23 | "data-sharing", 24 | "text-chat", 25 | "chat", 26 | "p2p-streaming", 27 | "data" 28 | ], 29 | "analyze": false, 30 | "license": "MIT", 31 | "readmeFilename": "README.md", 32 | "bugs": { 33 | "url": "https://github.com/muaz-khan/DataChannel/issues", 34 | "email": "muazkh@gmail.com" 35 | }, 36 | "homepage": "https://www.webrtc-experiment.com/DataChannel/", 37 | "_id": "datachannel@", 38 | "_from": "datachannel@", 39 | "devDependencies": { 40 | "grunt": "latest", 41 | "grunt-cli": "latest", 42 | "load-grunt-tasks": "latest", 43 | "grunt-contrib-concat": "latest", 44 | "grunt-contrib-csslint": "latest", 45 | "grunt-contrib-jshint": "latest", 46 | "grunt-contrib-uglify": "latest", 47 | "grunt-htmlhint": "latest", 48 | "grunt-jsbeautifier": "latest", 49 | "grunt-bump": "latest" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // http://127.0.0.1:9001 2 | // http://localhost:9001 3 | 4 | var port = 9001; 5 | 6 | var server = require('http'), 7 | url = require('url'), 8 | path = require('path'), 9 | fs = require('fs'); 10 | 11 | function serverHandler(request, response) { 12 | var uri = url.parse(request.url).pathname, 13 | filename = path.join(process.cwd(), uri); 14 | 15 | fs.exists(filename, function(exists) { 16 | if (!exists) { 17 | response.writeHead(404, { 18 | 'Content-Type': 'text/plain' 19 | }); 20 | response.write('404 Not Found: ' + filename + '\n'); 21 | response.end(); 22 | return; 23 | } 24 | 25 | if (filename.indexOf('favicon.ico') !== -1) { 26 | return; 27 | } 28 | 29 | var isWin = !!process.platform.match(/^win/); 30 | 31 | if (fs.statSync(filename).isDirectory() && !isWin) { 32 | filename += '/index.html'; 33 | } else if (fs.statSync(filename).isDirectory() && !!isWin) { 34 | filename += '\\index.html'; 35 | } 36 | 37 | fs.readFile(filename, 'binary', function(err, file) { 38 | if (err) { 39 | response.writeHead(500, { 40 | 'Content-Type': 'text/plain' 41 | }); 42 | response.write(err + '\n'); 43 | response.end(); 44 | return; 45 | } 46 | 47 | var contentType; 48 | 49 | if (filename.indexOf('.html') !== -1) { 50 | contentType = 'text/html'; 51 | } 52 | 53 | if (filename.indexOf('.js') !== -1) { 54 | contentType = 'application/javascript'; 55 | } 56 | 57 | if (contentType) { 58 | response.writeHead(200, { 59 | 'Content-Type': contentType 60 | }); 61 | } else response.writeHead(200); 62 | 63 | response.write(file, 'binary'); 64 | response.end(); 65 | }); 66 | }); 67 | } 68 | 69 | var app; 70 | 71 | app = server.createServer(serverHandler); 72 | 73 | app = app.listen(process.env.PORT || port, process.env.IP || "0.0.0.0", function() { 74 | var addr = app.address(); 75 | console.log("Server listening at", addr.address + ":" + addr.port); 76 | }); 77 | -------------------------------------------------------------------------------- /simple.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | File Sharing + Text Chat using WebRTC DataChannel 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 |

File Sharing + Text Chat using WebRTC DataChannel 60 |

61 | 62 |
63 | 64 |

65 | HOME 66 | © 67 | Muaz Khan 68 | 69 | . 70 | @WebRTCWeb 71 | 72 | . 73 | Github 74 | 75 | . 76 | Latest issues 77 | 78 | . 79 | What's New? 80 |

81 | 82 |
83 |

Open Data Channel

84 | 86 | 87 |

or join:

88 | 89 |
90 | 91 | 92 | 93 | 99 | 105 | 106 |
94 |

Text Chat

95 | 96 |
97 | 98 |
100 |

Share Files

101 | 102 | 103 |
104 |
107 | 108 | 111 | 112 | 221 |
222 |
223 | 224 |

Getting started with WebRTC DataChannel

226 |
227 | <script src="https://cdn.webrtc-experiment.com/DataChannel.js"></script>
228 | <script>
229 |     var channel = new DataChannel();
230 | 
231 |     // to create/open a new channel
232 |     channel.open('channel-name');
233 | 
234 |     // to send text/data or file
235 |     channel.send(file || data || 'text');
236 | 	
237 |     // if soemone already created a channel; to join it: use "connect" method
238 |     channel.connect('channel-name');
239 | </script>
240 | 
241 | Remember, A-to-Z, everything is optional! You can set channel-name in constructor or in 242 | open/connect methods. It is your choice! 243 | 244 |
245 |
246 | 247 |

Features:

248 |
    249 |
  1. Send file directly — of any size
  2. 250 |
  3. Send text-message of any length
  4. 251 |
  5. Send data directly
  6. 252 |
  7. Simplest syntax ever! Same like WebSockets.
  8. 253 |
  9. Supports fallback to socket.io/websockets/etc.
  10. 254 |
  11. Auto users' presence detection
  12. 255 |
  13. Allows you eject any user; or close your entire data session
  14. 256 |
257 |
258 |
259 | 260 |

Additional:

261 |
262 | <script>
263 |     // to be alerted on data ports get open
264 |     channel.onopen = function(userid) {}
265 | 	
266 |     // to be alerted on data ports get new message
267 |     channel.onmessage = function(message, userid) {}
268 | 	
269 |     // by default; connection is [many-to-many]; you can use following directions
270 |     channel.direction = 'one-to-one';
271 |     channel.direction = 'one-to-many';
272 |     channel.direction = 'many-to-many';	// --- it is default
273 | 
274 |     // show progress bar!
275 |     channel.onFileProgress = function (packets) {
276 |         // packets.remaining
277 |         // packets.sent
278 |         // packets.received
279 |         // packets.length
280 |     };
281 | 
282 |     // on file successfully sent
283 |     channel.onFileSent = function (file) {
284 |         // file.name
285 |         // file.size
286 |     };
287 | 
288 |     // on file successfully received
289 |     channel.onFileReceived = function (fileName) {};
290 | </script>
291 | 
292 |
293 |
294 | 295 |

Errors Handling

296 |
297 | <script>
298 |     // error to open data ports
299 |     channel.onerror = function(event) {}
300 | 	
301 |     // data ports suddenly dropped
302 |     channel.onclose = function(event) {}
303 | </script>
304 | 
305 |
306 |
307 | 308 |

Use your own socket.io for signaling

309 |
310 | <script>
311 |     // by default socket.io is used for signaling; you can override it
312 |     channel.openSignalingChannel = function(config) {
313 |         var socket = io.connect('http://your-site:8888');
314 |         socket.channel = config.channel || this.channel || 'default-channel';
315 |         socket.on('message', config.onmessage);
316 | 
317 |         socket.send = function (data) {
318 |             socket.emit('message', data);
319 |         };
320 | 
321 |         if (config.onopen) setTimeout(config.onopen, 1);
322 |         return socket;
323 |     }
324 | </script>
325 | 
326 |
327 |
328 |
329 |

Feedback

330 | 331 |
332 | 335 |
336 | 337 |
338 | 339 |
340 |

Latest Updates

341 |
342 |
343 |
344 | 345 | 346 | 347 | 354 | 355 | 356 | 357 | 358 | --------------------------------------------------------------------------------