// P2P file transfer over WebRTC data channels. Experimental — do not use in // production without testing. Requires a live WebRTC peer connection. export default class P2PFileSender { constructor(webrtc, peer) { this.webrtc = webrtc; this.rtc = webrtc.rtc; this.peer = peer; this.totalSize = 0; this.isReady = false; this.isStarted = false; this.isSending = false; this.isRecaiving = false; this.processedSize = 0; this.bufferSizePerChannel = 10e6; this.bufferSizePerPack = 10e3; this.safeBufferSizePerPack = 10e3 - 1; } async RecaiveFile(fileMetadata, channelCount, _totalSize, onEnded) { let parts = []; this.webrtc.on('datachannel', datachannel => { let current = 0; let totalSize = 0; let currentPart = 0; let bufferAmount = []; datachannel.onmessage = ({ data }) => { if (totalSize === 0) { const { size, part } = JSON.parse(data); totalSize = size; currentPart = part; datachannel.send('READY'); } else { current += data.byteLength; bufferAmount.push(data); if (current === totalSize) { parts[currentPart] = new Blob(bufferAmount); bufferAmount = []; totalSize = 0; currentPart = 0; current = 0; datachannel.send('TOTAL_RECAIVED'); } } }; datachannel.onclose = () => { channelCount--; if (channelCount === 0) { const file = new File(parts, fileMetadata.name, { type: fileMetadata.type, lastModified: Date.now() }); onEnded(file); } }; }); } async SendFile(file, metadata) { this.isSending = true; this.isStarted = true; const buffer = await file.arrayBuffer(); const partCount = Math.ceil(buffer.byteLength / 10e6); const channelCount = Math.min(5, partCount); if (this.webrtc.iceStatus !== 'connected') { throw new Error('WebRTC is not ready'); } this.peer.send({ type: 'file', name: file.name, size: file.size, mimetype: file.type, partCount, channelCount, metadata }); const channels = []; for (let i = 0; i < channelCount; i++) { const channel = this.rtc.createDataChannel('\\?\\file_' + i); channel.binaryType = 'arraybuffer'; await new Promise(ok => { channel.onopen = () => ok(); }); channels.push(channel); } let currentPart = 0; const next = () => { if (currentPart < partCount) { const part = buffer.slice(currentPart * 10e6, (currentPart + 1) * 10e6); return [part, currentPart++]; } return [false, 0]; }; let pending = channels.length; await new Promise(ok => { for (let i = 0; i < channels.length; i++) { this._sendPartition(channels[i], next, () => { if (--pending === 0) { this.isSending = false; this.isStarted = false; ok(); } }); } }); } _sendPartition(channel, nextblob, onEnded) { let [currentBuffer, currentPartition] = nextblob(); let currentPart = 0; const nextChunk = () => { if (!(currentBuffer instanceof ArrayBuffer)) return; const chunk = currentBuffer.slice(currentPart * 16e3, (currentPart + 1) * 16e3); currentPart++; return chunk.byteLength ? chunk : undefined; }; channel.addEventListener('message', ({ data }) => { if (data === 'READY') { this._sendChannel(channel, nextChunk); } else if (data === 'TOTAL_RECAIVED') { [currentBuffer, currentPartition] = nextblob(); currentPart = 0; if (currentBuffer !== false) { channel.send(JSON.stringify({ size: currentBuffer.byteLength, part: currentPartition })); } else { channel.close(); onEnded(); } } }); channel.send(JSON.stringify({ size: currentBuffer.byteLength, part: currentPartition })); } _sendChannel(channel, getNextChunk) { channel.addEventListener('bufferedamountlow', () => { const chunk = getNextChunk(); if (chunk) channel.send(chunk); }); channel.bufferedAmountLowThreshold = 16e3 - 1; const first = getNextChunk(); if (first) channel.send(first); } }