window.URL = window.URL || window.webkitURL; window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection); window.hiddenProperty = 'hidden' in document ? 'hidden' : 'webkitHidden' in document ? 'webkitHidden' : 'mozHidden' in document ? 'mozHidden' : null; window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' : 'webkitvisibilitychange' in document ? 'webkitvisibilitychange' : 'mozvisibilitychange' in document ? 'mozvisibilitychange' : null; class ServerConnection { constructor() { this._connect(); Events.on('pagehide', _ => this._disconnect()); document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange()); if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail })); Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'})); Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail})); Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail})); Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate()); Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail)); Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' })); Events.on('create-public-room', _ => this._onCreatePublicRoom()); Events.on('join-public-room', e => this._onJoinPublicRoom(e.detail.roomId, e.detail.createIfInvalid)); Events.on('leave-public-room', _ => this._onLeavePublicRoom()); Events.on('offline', _ => clearTimeout(this._reconnectTimer)); Events.on('online', _ => this._connect()); } _connect() { clearTimeout(this._reconnectTimer); if (this._isConnected() || this._isConnecting() || this._isOffline()) return; if (this._isReconnect) { Events.fire('notify-user', { message: Localization.getTranslation("notifications.connecting"), persistent: true }); } const ws = new WebSocket(this._endpoint()); ws.binaryType = 'arraybuffer'; ws.onopen = _ => this._onOpen(); ws.onmessage = e => this._onMessage(e.data); ws.onclose = _ => this._onDisconnect(); ws.onerror = e => this._onError(e); this._socket = ws; } _onOpen() { console.log('WS: server connected'); Events.fire('ws-connected'); if (this._isReconnect) Events.fire('notify-user', Localization.getTranslation("notifications.connected")); } _onPairDeviceInitiate() { if (!this._isConnected()) { Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-pairing")); return; } this.send({ type: 'pair-device-initiate' }); } _onPairDeviceJoin(pairKey) { if (!this._isConnected()) { setTimeout(_ => this._onPairDeviceJoin(pairKey), 1000); return; } this.send({ type: 'pair-device-join', pairKey: pairKey }); } _onCreatePublicRoom() { if (!this._isConnected()) { Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement-public-room")); return; } this.send({ type: 'create-public-room' }); } _onJoinPublicRoom(roomId, createIfInvalid) { if (!this._isConnected()) { setTimeout(_ => this._onJoinPublicRoom(roomId), 1000); return; } this.send({ type: 'join-public-room', publicRoomId: roomId, createIfInvalid: createIfInvalid }); } _onLeavePublicRoom() { if (!this._isConnected()) { setTimeout(_ => this._onLeavePublicRoom(), 1000); return; } this.send({ type: 'leave-public-room' }); } _setRtcConfig(config) { window.rtcConfig = config; } _onMessage(msg) { msg = JSON.parse(msg); if (msg.type !== 'ping') console.log('WS receive:', msg); switch (msg.type) { case 'rtc-config': this._setRtcConfig(msg.config); break; case 'peers': this._onPeers(msg); break; case 'peer-joined': Events.fire('peer-joined', msg); break; case 'peer-left': Events.fire('peer-left', msg); break; case 'signal': Events.fire('signal', msg); break; case 'ping': this.send({ type: 'pong' }); break; case 'display-name': this._onDisplayName(msg); break; case 'pair-device-initiated': Events.fire('pair-device-initiated', msg); break; case 'pair-device-joined': Events.fire('pair-device-joined', msg); break; case 'pair-device-join-key-invalid': Events.fire('pair-device-join-key-invalid'); break; case 'pair-device-canceled': Events.fire('pair-device-canceled', msg.pairKey); break; case 'join-key-rate-limit': Events.fire('notify-user', Localization.getTranslation("notifications.rate-limit-join-key")); break; case 'secret-room-deleted': Events.fire('secret-room-deleted', msg.roomSecret); break; case 'room-secret-regenerated': Events.fire('room-secret-regenerated', msg); break; case 'public-room-id-invalid': Events.fire('public-room-id-invalid', msg.publicRoomId); break; case 'public-room-created': Events.fire('public-room-created', msg.roomId); break; case 'public-room-left': Events.fire('public-room-left'); break; case 'request': case 'header': case 'progress': case 'files-transfer-response': case 'file-transfer-complete': case 'message-transfer-complete': case 'text': case 'display-name-changed': case 'ws-chunk': Events.fire('ws-relay', JSON.stringify(msg)); break; default: console.error('WS receive: unknown message type', msg); } } send(msg) { if (!this._isConnected()) return; if (msg.type !== 'pong') console.log("WS send:", msg) this._socket.send(JSON.stringify(msg)); } _onPeers(msg) { Events.fire('peers', msg); } _onDisplayName(msg) { // Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload sessionStorage.setItem('peer_id', msg.message.peerId); sessionStorage.setItem('peer_id_hash', msg.message.peerIdHash); // Add peerId to localStorage to mark it for other PairDrop tabs on the same browser BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => { if (!peerId) return; console.log("successfully added peerId to localStorage"); // Only now join rooms Events.fire('join-ip-room'); PersistentStorage.getAllRoomSecrets().then(roomSecrets => { Events.fire('room-secrets', roomSecrets); }); }); Events.fire('display-name', msg); } _endpoint() { // hack to detect if deployment or development environment const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws'; const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback'; let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc); const peerId = sessionStorage.getItem('peer_id'); const peerIdHash = sessionStorage.getItem('peer_id_hash'); if (peerId && peerIdHash) { ws_url.searchParams.append('peer_id', peerId); ws_url.searchParams.append('peer_id_hash', peerIdHash); } return ws_url.toString(); } _disconnect() { this.send({ type: 'disconnect' }); const peerId = sessionStorage.getItem('peer_id'); BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => { console.log("successfully removed peerId from localStorage"); }); if (!this._socket) return; this._socket.onclose = null; this._socket.close(); this._socket = null; Events.fire('ws-disconnected'); this._isReconnect = true; } _onDisconnect() { console.log('WS: server disconnected'); setTimeout(() => { this._isReconnect = true; Events.fire('ws-disconnected'); this._reconnectTimer = setTimeout(_ => this._connect(), 1000); }, 100); //delay for 100ms to prevent flickering on page reload } _onVisibilityChange() { if (window.hiddenProperty) return; this._connect(); } _isConnected() { return this._socket && this._socket.readyState === this._socket.OPEN; } _isConnecting() { return this._socket && this._socket.readyState === this._socket.CONNECTING; } _isOffline() { return !navigator.onLine; } _onError(e) { console.error(e); } _reconnect() { this._disconnect(); this._connect(); } } class Peer { constructor(serverConnection, isCaller, peerId, roomType, roomId) { this._server = serverConnection; this._isCaller = isCaller; this._peerId = peerId; this._roomIds = {}; this._updateRoomIds(roomType, roomId); this._chunkSize = 262144; this._lowWaterMark = 2* this._chunkSize; this._filesQueue = []; this._busy = false; // evaluate auto accept this._evaluateAutoAccept(); } sendJSON(message) { this._send(JSON.stringify(message)); } sendDisplayName(displayName) { this.sendJSON({type: 'display-name-changed', displayName: displayName}); } _isSameBrowser() { return BrowserTabsConnector.peerIsSameBrowser(this._peerId); } _isPaired() { return !!this._roomIds['secret']; } _getPairSecret() { return this._roomIds['secret']; } _getRoomTypes() { return Object.keys(this._roomIds); } _updateRoomIds(roomType, roomId) { // if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets // -> do not delete duplicates and do not regenerate room secrets if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret() !== roomId) { // multiple roomSecrets with same peer -> delete old roomSecret PersistentStorage.deleteRoomSecret(this._getPairSecret()) .then(deletedRoomSecret => { if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret); }); } this._roomIds[roomType] = roomId; if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret().length !== 256 && this._isCaller) { // increase security by initiating the increase of the roomSecret length from 64 chars ( { const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false; this._setAutoAccept(autoAccept); }) .catch(_ => { this._setAutoAccept(false); }); } _setAutoAccept(autoAccept) { this._autoAccept = !this._isSameBrowser() ? autoAccept : false; } getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) { return new Promise((resolve, reject) => { let image = new Image(); image.src = URL.createObjectURL(file); image.onload = _ => { let imageWidth = image.width; let imageHeight = image.height; let canvas = document.createElement('canvas'); // resize the canvas and draw the image data into it if (width && height) { canvas.width = width; canvas.height = height; } else if (width) { canvas.width = width; canvas.height = Math.floor(imageHeight * width / imageWidth) } else if (height) { canvas.width = Math.floor(imageWidth * height / imageHeight); canvas.height = height; } else { canvas.width = imageWidth; canvas.height = imageHeight } var ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, canvas.width, canvas.height); let dataUrl = canvas.toDataURL("image/jpeg", quality); resolve(dataUrl); } image.onerror = _ => reject(`Could not create an image thumbnail from type ${file.type}`); }).then(dataUrl => { return dataUrl; }).catch(e => console.error(e)); } async requestFileTransfer(files) { let header = []; let totalSize = 0; let imagesOnly = true for (let i=0; i this._send(chunk)); this._chunker._readChunksIntoBuffer(this._channel ? this._channel.bufferedAmount : 0); } _sendProgress(progress) { this.sendJSON({ type: 'progress', progress: progress }); } _onMessage(message) { if (typeof message !== 'string') { this._onChunkReceived(message); return; } const messageJSON = JSON.parse(message); switch (messageJSON.type) { case 'request': this._onFilesTransferRequest(messageJSON); break; case 'header': this._onFileHeader(messageJSON); break; case 'progress': this._onDownloadProgress(messageJSON.progress); break; case 'files-transfer-response': this._onFileTransferRequestResponded(messageJSON); break; case 'file-transfer-complete': this._onFileTransferCompleted(messageJSON); break; case 'message-transfer-complete': this._onMessageTransferCompleted(); break; case 'text': this._onTextReceived(messageJSON); break; case 'display-name-changed': this._onDisplayNameChanged(messageJSON); break; } } _onFilesTransferRequest(request) { if (this._requestPending) { // Only accept one request at a time per peer this.sendJSON({type: 'files-transfer-response', accepted: false}); return; } if (window.iOS && request.totalSize >= 200*1024*1024) { // iOS Safari can only put 400MB at once to memory. // Request to send them in chunks of 200MB instead: this.sendJSON({type: 'files-transfer-response', accepted: false, reason: 'ios-memory-limit'}); return; } this._requestPending = request; if (this._autoAccept) { // auto accept if set via Edit Paired Devices Dialog this._respondToFileTransferRequest(true); return; } // default behavior: show user transfer request Events.fire('files-transfer-request', { request: request, peerId: this._peerId }); } _respondToFileTransferRequest(accepted) { this.sendJSON({type: 'files-transfer-response', accepted: accepted}); if (accepted) { this._requestAccepted = this._requestPending; this._totalBytesReceived = 0; this._busy = true; this._filesReceived = []; } this._requestPending = null; } _onFileHeader(header) { if (this._requestAccepted && this._requestAccepted.header.length) { this._lastProgress = 0; this._timeStart = Date.now(); this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime}, this._requestAccepted.totalSize, this._totalBytesReceived, fileBlob => this._onFileReceived(fileBlob) ); } } _abortTransfer() { Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect")); this._filesReceived = []; this._requestAccepted = null; this._digester = null; throw new Error("Received files differ from requested files. Abort!"); } _onChunkReceived(chunk) { if(!this._digester || !(chunk.byteLength || chunk.size)) return; this._digester.unchunk(chunk); const progress = this._digester.progress; if (progress > 1) { this._abortTransfer(); } this._onDownloadProgress(progress); // occasionally notify sender about our progress if (progress - this._lastProgress < 0.005 && progress !== 1) return; this._lastProgress = progress; this._sendProgress(progress); } _onDownloadProgress(progress) { Events.fire('set-progress', {peerId: this._peerId, progress: progress, status: 'transfer'}); } async _onFileReceived(fileBlob) { const acceptedHeader = this._requestAccepted.header.shift(); this._totalBytesReceived += fileBlob.size; let duration = (Date.now() - this._timeStart) / 1000; let size = Math.round(10 * fileBlob.size / 1000000) / 10; let speed = Math.round(100 * fileBlob.size / 1000000 / duration) / 100; console.log(`File received.\n\nSize: ${size} MB\tDuration: ${duration} s\tSpeed: ${speed} MB/s`); this.sendJSON({type: 'file-transfer-complete', size: size, duration: duration, speed: speed}); const sameSize = fileBlob.size === acceptedHeader.size; const sameName = fileBlob.name === acceptedHeader.name if (!sameSize || !sameName) { this._abortTransfer(); } // include for compatibility with 'Snapdrop & PairDrop for Android' app Events.fire('file-received', fileBlob); this._filesReceived.push(fileBlob); if (!this._requestAccepted.header.length) { this._busy = false; Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'}); Events.fire('files-received', {peerId: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize}); this._filesReceived = []; this._requestAccepted = null; } } _onFileTransferCompleted(message) { console.log(`File sent.\n\nSize: ${message.size} MB\tDuration: ${message.duration} s\tSpeed: ${message.speed} MB/s`); if (!this._filesQueue.length) { this._busy = false; Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed")); Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app } else { this._dequeueFile(); } this._chunker._removeEventListener(); this._chunker = null; } _onFileTransferRequestResponded(message) { if (!message.accepted) { Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); this._filesRequested = null; if (message.reason === 'ios-memory-limit') { Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit")); } return; } Events.fire('file-transfer-accepted'); Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'transfer'}); this.sendFiles(); } _onMessageTransferCompleted() { Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed")); } sendText(text) { const unescaped = btoa(unescape(encodeURIComponent(text))); this.sendJSON({ type: 'text', text: unescaped }); } _onTextReceived(message) { if (!message.text) return; const escaped = decodeURIComponent(escape(atob(message.text))); Events.fire('text-received', { text: escaped, peerId: this._peerId }); this.sendJSON({ type: 'message-transfer-complete' }); } _onDisplayNameChanged(message) { const displayNameHasChanged = this._displayName !== message.displayName if (message.displayName && displayNameHasChanged) { this._displayName = message.displayName; } Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName}); if (!displayNameHasChanged) return; Events.fire('notify-peer-display-name-changed', this._peerId); } } class RTCPeer extends Peer { constructor(serverConnection, isCaller, peerId, roomType, roomId) { super(serverConnection, isCaller, peerId, roomType, roomId); this.rtcSupported = true; if (!this._isCaller) return; // we will listen for a caller this._connect(); } _connect() { if (!this._conn || this._conn.signalingState === "closed") this._openConnection(); if (this._isCaller) { this._openChannel(); } else { this._conn.ondatachannel = e => this._onChannelOpened(e); } } _openConnection() { this._conn = new RTCPeerConnection(window.rtcConfig); this._conn.onicecandidate = e => this._onIceCandidate(e); this._conn.onicecandidateerror = e => this._onError(e); this._conn.onconnectionstatechange = _ => this._onConnectionStateChange(); this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e); } _openChannel() { if (!this._conn) return; const channel = this._conn.createDataChannel('data-channel', { ordered: false, }); channel.onopen = e => this._onChannelOpened(e); channel.onerror = e => this._onError(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(); if (message.sdp) { this._conn.setRemoteDescription(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)) .catch(e => this._onError(e)); } } _onChannelOpened(event) { console.log('RTC: channel opened with', this._peerId); const channel = event.channel || event.target; channel.binaryType = 'arraybuffer'; channel.onmessage = e => this._onMessage(e.data); channel.onclose = _ => this._onChannelClosed(); this._chunkSize = Math.min(this._conn.sctp.maxMessageSize, 262144); // max chunk size: 256 KB this._lowWaterMark = 2 * this._chunkSize; channel.bufferedAmountLowThreshold = this._lowWaterMark; channel.onbufferedamountlow = () => Events.fire("bufferedamountlow", channel.bufferedAmount); this._channel = channel; Events.on('beforeunload', e => this._onBeforeUnload(e)); Events.on('pagehide', _ => this._onPageHide()); Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()}); } _onMessage(message) { if (typeof message === 'string') { console.log('RTC:', JSON.parse(message)); } super._onMessage(message); } getConnectionHash() { const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n"); const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n"); let localConnectionFingerprint, remoteConnectionFingerprint; for (let i=0; i this._onMessage(e.detail)); Events.on('peers', e => this._onPeers(e.detail)); Events.on('files-selected', e => this._onFilesSelected(e.detail)); Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail)) Events.on('send-text', e => this._onSendText(e.detail)); Events.on('peer-left', e => this._onPeerLeft(e.detail)); Events.on('peer-joined', e => this._onPeerJoined(e.detail)); Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId)); Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); // this device closes connection Events.on('room-secrets-deleted', e => this._onRoomSecretsDeleted(e.detail)); Events.on('leave-public-room', e => this._onLeavePublicRoom(e.detail)); // peer closes connection Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail)); Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName)); Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail)); Events.on('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail)); Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept)); Events.on('ws-disconnected', _ => this._onWsDisconnected()); Events.on('ws-relay', e => this._onWsRelay(e.detail)); } _onMessage(message) { const peerId = message.sender.id; this.peers[peerId].onServerMessage(message); } _refreshPeer(peer, roomType, roomId) { if (!peer) return false; const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType; const roomIdsDiffer = peer._roomIds[roomType] !== roomId; // if roomType or roomId for roomType differs peer is already connected // -> only update roomSecret and reevaluate auto accept if (roomTypesDiffer || roomIdsDiffer) { peer._updateRoomIds(roomType, roomId); peer._evaluateAutoAccept(); return true; } return true; } _createOrRefreshPeer(isCaller, peerId, roomType, roomId, rtcSupported) { const peer = this.peers[peerId]; if (peer) { this._refreshPeer(peer, roomType, roomId); return; } if (window.isRtcSupported && rtcSupported) { this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId); } else { this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomId); } } _onPeerJoined(message) { this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId, message.peer.rtcSupported); } _onPeers(message) { message.peers.forEach(peer => { this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId, peer.rtcSupported); }) } _onWsRelay(message) { const messageJSON = JSON.parse(message); if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk); this.peers[messageJSON.sender.id]._onMessage(message); } _onRespondToFileTransferRequest(detail) { this.peers[detail.to]._respondToFileTransferRequest(detail.accepted); } _onFilesSelected(message) { let inputFiles = Array.from(message.files); delete message.files; let files = []; const l = inputFiles.length; for (let i=0; i { if (!peerIds) return; console.log("successfully removed other peerIds from localStorage"); }); } } } _onPeerConnected(peerId) { this._notifyPeerDisplayNameChanged(peerId); } _onWsDisconnected() { for (const peerId in this.peers) { if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) { Events.fire('peer-disconnected', peerId); } } } _onPeerDisconnected(peerId) { const peer = this.peers[peerId]; delete this.peers[peerId]; if (!peer || !peer._conn) return; if (peer._channel) peer._channel.onclose = null; peer._conn.close(); peer._busy = false; peer._roomIds = {}; } _onRoomSecretsDeleted(roomSecrets) { for (let i=0; i 1) { peer._removeRoomType(roomType); } else { Events.fire('peer-disconnected', peerId); } } _onRoomSecretRegenerated(message) { PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => { console.log("successfully regenerated room secret"); Events.fire("room-secrets", [message.newRoomSecret]); }) } _notifyPeersDisplayNameChanged(newDisplayName) { this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName; for (const peerId in this.peers) { this._notifyPeerDisplayNameChanged(peerId); } } _notifyPeerDisplayNameChanged(peerId) { const peer = this.peers[peerId]; if (!peer) return; this.peers[peerId].sendDisplayName(this._displayName); } _onDisplayName(displayName) { this._originalDisplayName = displayName; // if the displayName has not been changed (yet) set the displayName to the original displayName if (!this._displayName) this._displayName = displayName; } _onAutoAcceptUpdated(roomSecret, autoAccept) { const peerId = this._getPeerIdsFromRoomId(roomSecret)[0]; if (!peerId) return; this.peers[peerId]._setAutoAccept(autoAccept); } _getPeerIdsFromRoomId(roomId) { if (!roomId) return []; let peerIds = [] for (const peerId in this.peers) { const peer = this.peers[peerId]; // peer must have same roomId. if (Object.values(peer._roomIds).includes(roomId)) { peerIds.push(peer._peerId); } } return peerIds; } } class FileChunker { constructor(file, chunkSize, onChunk) { this._file = file; this._chunkSize = chunkSize; this._highWaterMark = 8 * chunkSize; this._bytesToSend = file.size; this._sendProgress = 0; this._onChunk = onChunk; this._reader = new FileReader(); this._reader.addEventListener('error', err => console.error('Error reading file:', err)); this._reader.addEventListener('abort', e => console.log('File reading aborted:', e)); this._reader.addEventListener('load', e => this._onChunkRead(e.target.result)); this.bufferedAmountCallback = e => this._readChunksIntoBuffer(e.detail); Events.on('bufferedamountlow', this.bufferedAmountCallback); } _removeEventListener() { Events.off('bufferedamountlow', this.bufferedAmountCallback); } _readChunksIntoBuffer(bufferedAmount) { this._bufferedAmount = bufferedAmount; this._readChunk(); } _readChunk() { const chunk = this._file.slice(this._sendProgress, this._sendProgress + this._chunkSize); this._reader.readAsArrayBuffer(chunk); } _onChunkRead(chunk) { this._onChunk(chunk); this._bufferedAmount += this._chunkSize; this._sendProgress += this._chunkSize; if (this._isBufferFull() || this._isFileEnd()) return; this._readChunk(); } _isBufferFull() { return this._bufferedAmount >= this._highWaterMark; } _isFileEnd() { return this._sendProgress >= this._bytesToSend; } } class FileDigester { constructor(meta, totalSize, totalBytesReceived, callback) { this._buffer = []; this._bytesReceived = 0; this._size = meta.size; this._name = meta.name; this._mime = meta.mime; this._totalSize = totalSize; this._totalBytesReceived = totalBytesReceived; this._callback = callback; } unchunk(chunk) { this._buffer.push(chunk); this._bytesReceived += chunk.byteLength || chunk.size; this.progress = (this._totalBytesReceived + this._bytesReceived) / this._totalSize; if (isNaN(this.progress)) this.progress = 1 if (this._bytesReceived < this._size) return; // we are done const blob = new Blob(this._buffer) this._buffer = null; this._callback(new File([blob], this._name, { type: this._mime, lastModified: new Date().getTime() })); } } class Events { static fire(type, detail = {}) { window.dispatchEvent(new CustomEvent(type, { detail: detail })); } static on(type, callback, options = false) { return window.addEventListener(type, callback, options); } static off(type, callback, options = false) { return window.removeEventListener(type, callback, options); } }