network.js 15 KB

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