network.js 14 KB

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