network.js 14 KB

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