123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- window.URL = window.URL || window.webkitURL;
- window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
- class ServerConnection {
- constructor() {
- this._connect();
- Events.on('beforeunload', e => this._disconnect());
- Events.on('pagehide', e => this._disconnect());
- document.addEventListener('visibilitychange', e => this._onVisibilityChange());
- }
- _connect() {
- clearTimeout(this._reconnectTimer);
- if (this._isConnected() || this._isConnecting()) return;
- const ws = new WebSocket(this._endpoint());
- ws.binaryType = 'arraybuffer';
- ws.onopen = e => console.log('WS: server connected');
- ws.onmessage = e => this._onMessage(e.data);
- ws.onclose = e => this._onDisconnect();
- ws.onerror = e => console.error(e);
- this._socket = ws;
- }
- _onMessage(msg) {
- msg = JSON.parse(msg);
- console.log('WS:', msg);
- switch (msg.type) {
- case 'peers':
- Events.fire('peers', msg.peers);
- break;
- case 'peer-joined':
- Events.fire('peer-joined', msg.peer);
- break;
- case 'peer-left':
- Events.fire('peer-left', msg.peerId);
- break;
- case 'signal':
- Events.fire('signal', msg);
- break;
- case 'ping':
- this.send({ type: 'pong' });
- break;
- case 'display-name':
- Events.fire('display-name', msg);
- break;
- default:
- console.error('WS: unkown message type', msg);
- }
- }
- send(message) {
- if (!this._isConnected()) return;
- this._socket.send(JSON.stringify(message));
- }
- _endpoint() {
- // hack to detect if deployment or development environment
- const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
- const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
- const url = protocol + '://' + location.host + location.pathname + '/server' + webrtc;
- return url;
- }
- _disconnect() {
- this.send({ type: 'disconnect' });
- this._socket.onclose = null;
- this._socket.close();
- }
- _onDisconnect() {
- console.log('WS: server disconnected');
- Events.fire('notify-user', 'Connection lost. Retry in 5 seconds...');
- clearTimeout(this._reconnectTimer);
- this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
- }
- _onVisibilityChange() {
- if (document.hidden) return;
- this._connect();
- }
- _isConnected() {
- return this._socket && this._socket.readyState === this._socket.OPEN;
- }
- _isConnecting() {
- return this._socket && this._socket.readyState === this._socket.CONNECTING;
- }
- }
- class Peer {
- constructor(serverConnection, peerId) {
- this._server = serverConnection;
- this._peerId = peerId;
- this._filesQueue = [];
- this._busy = false;
- }
- sendJSON(message) {
- this._send(JSON.stringify(message));
- }
- sendFiles(files) {
- for (let i = 0; i < files.length; i++) {
- this._filesQueue.push(files[i]);
- }
- if (this._busy) return;
- this._dequeueFile();
- }
- _dequeueFile() {
- if (!this._filesQueue.length) return;
- this._busy = true;
- const file = this._filesQueue.shift();
- this._sendFile(file);
- }
- _sendFile(file) {
- this.sendJSON({
- type: 'header',
- name: file.name,
- mime: file.type,
- size: file.size
- });
- this._chunker = new FileChunker(file,
- chunk => this._send(chunk),
- offset => this._onPartitionEnd(offset));
- this._chunker.nextPartition();
- }
- _onPartitionEnd(offset) {
- this.sendJSON({ type: 'partition', offset: offset });
- }
- _onReceivedPartitionEnd(offset) {
- this.sendJSON({ type: 'partition-received', offset: offset });
- }
- _sendNextPartition() {
- if (!this._chunker || this._chunker.isFileEnd()) return;
- this._chunker.nextPartition();
- }
- _sendProgress(progress) {
- this.sendJSON({ type: 'progress', progress: progress });
- }
- _onMessage(message) {
- if (typeof message !== 'string') {
- this._onChunkReceived(message);
- return;
- }
- message = JSON.parse(message);
- console.log('RTC:', message);
- switch (message.type) {
- case 'header':
- this._onFileHeader(message);
- break;
- case 'partition':
- this._onReceivedPartitionEnd(message);
- break;
- case 'partition-received':
- this._sendNextPartition();
- break;
- case 'progress':
- this._onDownloadProgress(message.progress);
- break;
- case 'transfer-complete':
- this._onTransferCompleted();
- break;
- case 'text':
- this._onTextReceived(message);
- break;
- }
- }
- _onFileHeader(header) {
- this._lastProgress = 0;
- this._digester = new FileDigester({
- name: header.name,
- mime: header.mime,
- size: header.size
- }, file => this._onFileReceived(file));
- }
- _onChunkReceived(chunk) {
- this._digester.unchunk(chunk);
- const progress = this._digester.progress;
- this._onDownloadProgress(progress);
- // occasionally notify sender about our progress
- if (progress - this._lastProgress < 0.01) return;
- this._lastProgress = progress;
- this._sendProgress(progress);
- }
- _onDownloadProgress(progress) {
- Events.fire('file-progress', { sender: this._peerId, progress: progress });
- }
- _onFileReceived(proxyFile) {
- Events.fire('file-received', proxyFile);
- this.sendJSON({ type: 'transfer-complete' });
- }
- _onTransferCompleted() {
- this._onDownloadProgress(1);
- this._reader = null;
- this._busy = false;
- this._dequeueFile();
- Events.fire('notify-user', 'File transfer completed.');
- }
- sendText(text) {
- const unescaped = btoa(unescape(encodeURIComponent(text)));
- this.sendJSON({ type: 'text', text: unescaped });
- }
- _onTextReceived(message) {
- const escaped = decodeURIComponent(escape(atob(message.text)));
- Events.fire('text-received', { text: escaped, sender: this._peerId });
- }
- }
- class RTCPeer extends Peer {
- constructor(serverConnection, peerId) {
- super(serverConnection, peerId);
- if (!peerId) return; // we will listen for a caller
- this._connect(peerId, true);
- }
- _connect(peerId, isCaller) {
- if (!this._conn) this._openConnection(peerId, isCaller);
- if (isCaller) {
- this._openChannel();
- } else {
- this._conn.ondatachannel = e => this._onChannelOpened(e);
- }
- }
- _openConnection(peerId, isCaller) {
- this._isCaller = isCaller;
- this._peerId = peerId;
- this._conn = new RTCPeerConnection(RTCPeer.config);
- this._conn.onicecandidate = e => this._onIceCandidate(e);
- this._conn.onconnectionstatechange = e => this._onConnectionStateChange(e);
- this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
- }
- _openChannel() {
- const channel = this._conn.createDataChannel('data-channel', {
- ordered: true,
- reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
- });
- channel.binaryType = 'arraybuffer';
- channel.onopen = e => this._onChannelOpened(e);
- this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e));
- }
- _onDescription(description) {
- // description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400');
- this._conn.setLocalDescription(description)
- .then(_ => this._sendSignal({ sdp: description }))
- .catch(e => this._onError(e));
- }
- _onIceCandidate(event) {
- if (!event.candidate) return;
- this._sendSignal({ ice: event.candidate });
- }
- onServerMessage(message) {
- if (!this._conn) this._connect(message.sender, false);
- if (message.sdp) {
- this._conn.setRemoteDescription(new RTCSessionDescription(message.sdp))
- .then( _ => {
- if (message.sdp.type === 'offer') {
- return this._conn.createAnswer()
- .then(d => this._onDescription(d));
- }
- })
- .catch(e => this._onError(e));
- } else if (message.ice) {
- this._conn.addIceCandidate(new RTCIceCandidate(message.ice));
- }
- }
- _onChannelOpened(event) {
- console.log('RTC: channel opened with', this._peerId);
- const channel = event.channel || event.target;
- channel.onmessage = e => this._onMessage(e.data);
- channel.onclose = e => this._onChannelClosed();
- this._channel = channel;
- }
- _onChannelClosed() {
- console.log('RTC: channel closed', this._peerId);
- if (!this.isCaller) return;
- this._connect(this._peerId, true); // reopen the channel
- }
- _onConnectionStateChange(e) {
- console.log('RTC: state changed:', this._conn.connectionState);
- switch (this._conn.connectionState) {
- case 'disconnected':
- this._onChannelClosed();
- break;
- case 'failed':
- this._conn = null;
- this._onChannelClosed();
- break;
- }
- }
- _onIceConnectionStateChange() {
- switch (this._conn.iceConnectionState) {
- case 'failed':
- console.error('ICE Gathering failed');
- break;
- default:
- console.log('ICE Gathering', this._conn.iceConnectionState);
- }
- }
- _onError(error) {
- console.error(error);
- }
- _send(message) {
- if (!this._channel) return this.refresh();
- this._channel.send(message);
- }
- _sendSignal(signal) {
- signal.type = 'signal';
- signal.to = this._peerId;
- this._server.send(signal);
- }
- refresh() {
- // check if channel is open. otherwise create one
- if (this._isConnected() || this._isConnecting()) return;
- this._connect(this._peerId, this._isCaller);
- }
- _isConnected() {
- return this._channel && this._channel.readyState === 'open';
- }
- _isConnecting() {
- return this._channel && this._channel.readyState === 'connecting';
- }
- }
- class PeersManager {
- constructor(serverConnection) {
- this.peers = {};
- this._server = serverConnection;
- Events.on('signal', e => this._onMessage(e.detail));
- Events.on('peers', e => this._onPeers(e.detail));
- Events.on('files-selected', e => this._onFilesSelected(e.detail));
- Events.on('send-text', e => this._onSendText(e.detail));
- Events.on('peer-left', e => this._onPeerLeft(e.detail));
- }
- _onMessage(message) {
- if (!this.peers[message.sender]) {
- this.peers[message.sender] = new RTCPeer(this._server);
- }
- this.peers[message.sender].onServerMessage(message);
- }
- _onPeers(peers) {
- peers.forEach(peer => {
- if (this.peers[peer.id]) {
- this.peers[peer.id].refresh();
- return;
- }
- if (window.isRtcSupported && peer.rtcSupported) {
- this.peers[peer.id] = new RTCPeer(this._server, peer.id);
- } else {
- this.peers[peer.id] = new WSPeer(this._server, peer.id);
- }
- })
- }
- sendTo(peerId, message) {
- this.peers[peerId].send(message);
- }
- _onFilesSelected(message) {
- this.peers[message.to].sendFiles(message.files);
- }
- _onSendText(message) {
- this.peers[message.to].sendText(message.text);
- }
- _onPeerLeft(peerId) {
- const peer = this.peers[peerId];
- delete this.peers[peerId];
- if (!peer || !peer._peer) return;
- peer._peer.close();
- }
- }
- class WSPeer {
- _send(message) {
- message.to = this._peerId;
- this._server.send(message);
- }
- }
- class FileChunker {
- constructor(file, onChunk, onPartitionEnd) {
- this._chunkSize = 64000; // 64 KB
- this._maxPartitionSize = 1e6; // 1 MB
- this._offset = 0;
- this._partitionSize = 0;
- this._file = file;
- this._onChunk = onChunk;
- this._onPartitionEnd = onPartitionEnd;
- this._reader = new FileReader();
- this._reader.addEventListener('load', e => this._onChunkRead(e.target.result));
- }
- nextPartition() {
- this._partitionSize = 0;
- this._readChunk();
- }
- _readChunk() {
- const chunk = this._file.slice(this._offset, this._offset + this._chunkSize);
- this._reader.readAsArrayBuffer(chunk);
- }
- _onChunkRead(chunk) {
- this._offset += chunk.byteLength;
- this._partitionSize += chunk.byteLength;
- this._onChunk(chunk);
- if (this._isPartitionEnd() || this.isFileEnd()) {
- this._onPartitionEnd(this._offset);
- return;
- }
- this._readChunk();
- }
- repeatPartition() {
- this._offset -= this._partitionSize;
- this._nextPartition();
- }
- _isPartitionEnd() {
- return this._partitionSize >= this._maxPartitionSize;
- }
- isFileEnd() {
- return this._offset > this._file.size;
- }
- get progress() {
- return this._offset / this._file.size;
- }
- }
- class FileDigester {
- constructor(meta, callback) {
- this._buffer = [];
- this._bytesReceived = 0;
- this._size = meta.size;
- this._mime = meta.mime || 'application/octet-stream';
- this._name = meta.name;
- this._callback = callback;
- }
- unchunk(chunk) {
- this._buffer.push(chunk);
- this._bytesReceived += chunk.byteLength || chunk.size;
- const totalChunks = this._buffer.length;
- this.progress = this._bytesReceived / this._size;
- if (this._bytesReceived < this._size) return;
- // we are done
- let blob = new Blob(this._buffer, { type: this._mime });
- this._callback({
- name: this._name,
- mime: this._mime,
- size: this._size,
- blob: blob
- });
- }
- }
- class Events {
- static fire(type, detail) {
- window.dispatchEvent(new CustomEvent(type, { detail: detail }));
- }
- static on(type, callback) {
- return window.addEventListener(type, callback, false);
- }
- }
- RTCPeer.config = {
- 'sdpSemantics': 'unified-plan',
- 'iceServers': [{
- urls: 'stun:stun.l.google.com:19302'
- }]
- }
|