MWSE/sdk/P2PFileSender.js

155 lines
5.2 KiB
JavaScript

// 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);
}
}