155 lines
5.2 KiB
JavaScript
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);
|
|
}
|
|
}
|