network.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. window.URL = window.URL || window.webkitURL;
  2. window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
  3. class ServerConnection {
  4. constructor() {
  5. this._connect();
  6. Events.on('beforeunload', e => this._disconnect());
  7. Events.on('pagehide', e => this._disconnect());
  8. document.addEventListener('visibilitychange', e => this._onVisibilityChange());
  9. }
  10. _connect() {
  11. clearTimeout(this._reconnectTimer);
  12. if (this._isConnected() || this._isConnecting()) return;
  13. const ws = new WebSocket(this._endpoint());
  14. ws.binaryType = 'arraybuffer';
  15. ws.onopen = e => console.log('WS: server connected');
  16. ws.onmessage = e => this._onMessage(e.data);
  17. ws.onclose = e => this._onDisconnect();
  18. ws.onerror = e => console.error(e);
  19. this._socket = ws;
  20. }
  21. _onMessage(msg) {
  22. msg = JSON.parse(msg);
  23. console.log('WS:', msg);
  24. switch (msg.type) {
  25. case 'peers':
  26. Events.fire('peers', msg.peers);
  27. break;
  28. case 'peer-joined':
  29. Events.fire('peer-joined', msg.peer);
  30. break;
  31. case 'peer-left':
  32. Events.fire('peer-left', msg.peerId);
  33. break;
  34. case 'signal':
  35. Events.fire('signal', msg);
  36. break;
  37. case 'ping':
  38. this.send({ type: 'pong' });
  39. break;
  40. default:
  41. console.error('WS: unkown message type', msg);
  42. }
  43. }
  44. send(message) {
  45. if (!this._isConnected()) return;
  46. this._socket.send(JSON.stringify(message));
  47. }
  48. _endpoint() {
  49. // hack to detect if deployment or development environment
  50. const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
  51. const host = location.hostname.startsWith('localhost') ? 'localhost:3000' : (location.host + '/server');
  52. const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
  53. const url = protocol + '://' + host + webrtc;
  54. return url;
  55. }
  56. _disconnect() {
  57. this.send({ type: 'disconnect' });
  58. this._socket.onclose = null;
  59. this._socket.close();
  60. }
  61. _onDisconnect() {
  62. console.log('WS: server disconnected');
  63. Events.fire('notify-user', 'Connection lost. Retry in 5 seconds...');
  64. clearTimeout(this._reconnectTimer);
  65. this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
  66. }
  67. _onVisibilityChange() {
  68. if (document.hidden) return;
  69. this._connect();
  70. }
  71. _isConnected() {
  72. return this._socket && this._socket.readyState === this._socket.OPEN;
  73. }
  74. _isConnecting() {
  75. return this._socket && this._socket.readyState === this._socket.CONNECTING;
  76. }
  77. }
  78. class Peer {
  79. constructor(serverConnection, peerId) {
  80. this._server = serverConnection;
  81. this._peerId = peerId;
  82. this._filesQueue = [];
  83. this._busy = false;
  84. }
  85. sendJSON(message) {
  86. this._send(JSON.stringify(message));
  87. }
  88. sendFiles(files) {
  89. for (let i = 0; i < files.length; i++) {
  90. this._filesQueue.push(files[i]);
  91. }
  92. if (this._busy) return;
  93. this._dequeueFile();
  94. }
  95. _dequeueFile() {
  96. if (!this._filesQueue.length) return;
  97. this._busy = true;
  98. const file = this._filesQueue.shift();
  99. this._sendFile(file);
  100. }
  101. _sendFile(file) {
  102. this.sendJSON({
  103. type: 'header',
  104. name: file.name,
  105. mime: file.type,
  106. size: file.size
  107. });
  108. this._chunker = new FileChunker(file,
  109. chunk => this._send(chunk),
  110. offset => this._onPartitionEnd(offset));
  111. this._chunker.nextPartition();
  112. }
  113. _onPartitionEnd(offset) {
  114. this.sendJSON({ type: 'partition', offset: offset });
  115. }
  116. _onReceivedPartitionEnd(offset) {
  117. this.sendJSON({ type: 'partition-received', offset: offset });
  118. }
  119. _sendNextPartition() {
  120. if (!this._chunker || this._chunker.isFileEnd()) return;
  121. this._chunker.nextPartition();
  122. }
  123. _sendProgress(progress) {
  124. this.sendJSON({ type: 'progress', progress: progress });
  125. }
  126. _onMessage(message) {
  127. if (typeof message !== 'string') {
  128. this._onChunkReceived(message);
  129. return;
  130. }
  131. message = JSON.parse(message);
  132. console.log('RTC:', message);
  133. switch (message.type) {
  134. case 'header':
  135. this._onFileHeader(message);
  136. break;
  137. case 'partition':
  138. this._onReceivedPartitionEnd(message);
  139. break;
  140. case 'partition-received':
  141. this._sendNextPartition();
  142. break;
  143. case 'progress':
  144. this._onDownloadProgress(message.progress);
  145. break;
  146. case 'transfer-complete':
  147. this._onTransferCompleted();
  148. break;
  149. case 'text':
  150. this._onTextReceived(message);
  151. break;
  152. }
  153. }
  154. _onFileHeader(header) {
  155. this._lastProgress = 0;
  156. this._digester = new FileDigester({
  157. name: header.name,
  158. mime: header.mime,
  159. size: header.size
  160. }, file => this._onFileReceived(file));
  161. }
  162. _onChunkReceived(chunk) {
  163. this._digester.unchunk(chunk);
  164. const progress = this._digester.progress;
  165. this._onDownloadProgress(progress);
  166. // occasionally notify sender about our progress
  167. if (progress - this._lastProgress < 0.01) return;
  168. this._lastProgress = progress;
  169. this._sendProgress(progress);
  170. }
  171. _onDownloadProgress(progress) {
  172. Events.fire('file-progress', { sender: this._peerId, progress: progress });
  173. }
  174. _onFileReceived(proxyFile) {
  175. Events.fire('file-received', proxyFile);
  176. this.sendJSON({ type: 'transfer-complete' });
  177. }
  178. _onTransferCompleted() {
  179. this._onDownloadProgress(1);
  180. this._reader = null;
  181. this._busy = false;
  182. this._dequeueFile();
  183. Events.fire('notify-user', 'File transfer completed.');
  184. }
  185. sendText(text) {
  186. const unescaped = btoa(unescape(encodeURIComponent(text)));
  187. this.sendJSON({ type: 'text', text: unescaped });
  188. }
  189. _onTextReceived(message) {
  190. const escaped = decodeURIComponent(escape(atob(message.text)));
  191. Events.fire('text-received', { text: escaped, sender: this._peerId });
  192. }
  193. }
  194. class RTCPeer extends Peer {
  195. constructor(serverConnection, peerId) {
  196. super(serverConnection, peerId);
  197. if (!peerId) return; // we will listen for a caller
  198. this._connect(peerId, true);
  199. }
  200. _connect(peerId, isCaller) {
  201. if (!this._conn) this._openConnection(peerId, isCaller);
  202. if (isCaller) {
  203. this._openChannel();
  204. } else {
  205. this._conn.ondatachannel = e => this._onChannelOpened(e);
  206. }
  207. }
  208. _openConnection(peerId, isCaller) {
  209. this._isCaller = isCaller;
  210. this._peerId = peerId;
  211. this._conn = new RTCPeerConnection(RTCPeer.config);
  212. this._conn.onicecandidate = e => this._onIceCandidate(e);
  213. this._conn.onconnectionstatechange = e => this._onConnectionStateChange(e);
  214. this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
  215. }
  216. _openChannel() {
  217. const channel = this._conn.createDataChannel('data-channel', { reliable: true });
  218. channel.binaryType = 'arraybuffer';
  219. channel.onopen = e => this._onChannelOpened(e);
  220. this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e));
  221. }
  222. _onDescription(description) {
  223. // description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400');
  224. this._conn.setLocalDescription(description)
  225. .then(_ => this._sendSignal({ sdp: description }))
  226. .catch(e => this._onError(e));
  227. }
  228. _onIceCandidate(event) {
  229. if (!event.candidate) return;
  230. this._sendSignal({ ice: event.candidate });
  231. }
  232. onServerMessage(message) {
  233. if (!this._conn) this._connect(message.sender, false);
  234. if (message.sdp) {
  235. this._conn.setRemoteDescription(new RTCSessionDescription(message.sdp))
  236. .then( _ => {
  237. if (message.sdp.type === 'offer') {
  238. return this._conn.createAnswer()
  239. .then(d => this._onDescription(d));
  240. }
  241. })
  242. .catch(e => this._onError(e));
  243. } else if (message.ice) {
  244. this._conn.addIceCandidate(new RTCIceCandidate(message.ice));
  245. }
  246. }
  247. _onChannelOpened(event) {
  248. console.log('RTC: channel opened with', this._peerId);
  249. const channel = event.channel || event.target;
  250. channel.onmessage = e => this._onMessage(e.data);
  251. channel.onclose = e => this._onChannelClosed();
  252. this._channel = channel;
  253. }
  254. _onChannelClosed() {
  255. console.log('RTC: channel closed', this._peerId);
  256. if (!this.isCaller) return;
  257. this._connect(this._peerId, true); // reopen the channel
  258. }
  259. _onConnectionStateChange(e) {
  260. console.log('RTC: state changed:', this._conn.connectionState);
  261. switch (this._conn.connectionState) {
  262. case 'disconnected':
  263. this._onChannelClosed();
  264. break;
  265. case 'failed':
  266. this._conn = null;
  267. this._onChannelClosed();
  268. break;
  269. }
  270. }
  271. _onIceConnectionStateChange() {
  272. switch (this._conn.iceConnectionState) {
  273. case 'failed':
  274. console.error('ICE Gathering failed');
  275. break;
  276. default:
  277. console.log('ICE Gathering', this._conn.iceConnectionState);
  278. }
  279. }
  280. _onError(error) {
  281. console.error(error);
  282. }
  283. _send(message) {
  284. if (!this._channel) return this.refresh();
  285. this._channel.send(message);
  286. }
  287. _sendSignal(signal) {
  288. signal.type = 'signal';
  289. signal.to = this._peerId;
  290. this._server.send(signal);
  291. }
  292. refresh() {
  293. // check if channel is open. otherwise create one
  294. if (this._isConnected() || this._isConnecting()) return;
  295. this._connect(this._peerId, this._isCaller);
  296. }
  297. _isConnected() {
  298. return this._channel && this._channel.readyState === 'open';
  299. }
  300. _isConnecting() {
  301. return this._channel && this._channel.readyState === 'connecting';
  302. }
  303. }
  304. class PeersManager {
  305. constructor(serverConnection) {
  306. this.peers = {};
  307. this._server = serverConnection;
  308. Events.on('signal', e => this._onMessage(e.detail));
  309. Events.on('peers', e => this._onPeers(e.detail));
  310. Events.on('files-selected', e => this._onFilesSelected(e.detail));
  311. Events.on('send-text', e => this._onSendText(e.detail));
  312. Events.on('peer-left', e => this._onPeerLeft(e.detail));
  313. }
  314. _onMessage(message) {
  315. if (!this.peers[message.sender]) {
  316. this.peers[message.sender] = new RTCPeer(this._server);
  317. }
  318. this.peers[message.sender].onServerMessage(message);
  319. }
  320. _onPeers(peers) {
  321. peers.forEach(peer => {
  322. if (this.peers[peer.id]) {
  323. this.peers[peer.id].refresh();
  324. return;
  325. }
  326. if (window.isRtcSupported && peer.rtcSupported) {
  327. this.peers[peer.id] = new RTCPeer(this._server, peer.id);
  328. } else {
  329. this.peers[peer.id] = new WSPeer(this._server, peer.id);
  330. }
  331. })
  332. }
  333. sendTo(peerId, message) {
  334. this.peers[peerId].send(message);
  335. }
  336. _onFilesSelected(message) {
  337. this.peers[message.to].sendFiles(message.files);
  338. }
  339. _onSendText(message) {
  340. this.peers[message.to].sendText(message.text);
  341. }
  342. _onPeerLeft(peerId) {
  343. const peer = this.peers[peerId];
  344. delete this.peers[peerId];
  345. if (!peer || !peer._peer) return;
  346. peer._peer.close();
  347. }
  348. }
  349. class WSPeer {
  350. _send(message) {
  351. message.to = this._peerId;
  352. this._server.send(message);
  353. }
  354. }
  355. class FileChunker {
  356. constructor(file, onChunk, onPartitionEnd) {
  357. this._chunkSize = 64000; // 64 KB
  358. this._maxPartitionSize = 1e6; // 1 MB
  359. this._offset = 0;
  360. this._partitionSize = 0;
  361. this._file = file;
  362. this._onChunk = onChunk;
  363. this._onPartitionEnd = onPartitionEnd;
  364. this._reader = new FileReader();
  365. this._reader.addEventListener('load', e => this._onChunkRead(e.target.result));
  366. }
  367. nextPartition() {
  368. this._partitionSize = 0;
  369. this._readChunk();
  370. }
  371. _readChunk() {
  372. const chunk = this._file.slice(this._offset, this._offset + this._chunkSize);
  373. this._reader.readAsArrayBuffer(chunk);
  374. }
  375. _onChunkRead(chunk) {
  376. this._offset += chunk.byteLength;
  377. this._partitionSize += chunk.byteLength;
  378. this._onChunk(chunk);
  379. if (this._isPartitionEnd() || this.isFileEnd()) {
  380. this._onPartitionEnd(this._offset);
  381. return;
  382. }
  383. this._readChunk();
  384. }
  385. repeatPartition() {
  386. this._offset -= this._partitionSize;
  387. this._nextPartition();
  388. }
  389. _isPartitionEnd() {
  390. return this._partitionSize >= this._maxPartitionSize;
  391. }
  392. isFileEnd() {
  393. return this._offset > this._file.size;
  394. }
  395. get progress() {
  396. return this._offset / this._file.size;
  397. }
  398. }
  399. class FileDigester {
  400. constructor(meta, callback) {
  401. this._buffer = [];
  402. this._bytesReceived = 0;
  403. this._size = meta.size;
  404. this._mime = meta.mime || 'application/octet-stream';
  405. this._name = meta.name;
  406. this._callback = callback;
  407. }
  408. unchunk(chunk) {
  409. this._buffer.push(chunk);
  410. this._bytesReceived += chunk.byteLength || chunk.size;
  411. const totalChunks = this._buffer.length;
  412. this.progress = this._bytesReceived / this._size;
  413. if (this._bytesReceived < this._size) return;
  414. // we are done
  415. let blob = new Blob(this._buffer, { type: this._mime });
  416. this._callback({
  417. name: this._name,
  418. mime: this._mime,
  419. size: this._size,
  420. blob: blob
  421. });
  422. }
  423. }
  424. class Events {
  425. static fire(type, detail) {
  426. window.dispatchEvent(new CustomEvent(type, { detail: detail }));
  427. }
  428. static on(type, callback) {
  429. return window.addEventListener(type, callback, false);
  430. }
  431. }
  432. RTCPeer.config = {
  433. 'iceServers': [{
  434. urls: 'stun:stun.l.google.com:19302'
  435. }, {
  436. urls: 'turn:192.158.29.39:3478?transport=tcp',
  437. credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
  438. username: '28224511:1379330808'
  439. }]
  440. }