mirror of
https://github.com/jech/galene.git
synced 2024-11-22 16:45:58 +01:00
Rework file transfer.
Split into the protocol (in protocol.js) and the user interface (in galene.js). Make the state automaton explicit, and improve error-handling. The new protocol is incompatible with the old one.
This commit is contained in:
parent
7d4133d1c3
commit
3bdd82f06d
2 changed files with 647 additions and 494 deletions
546
static/galene.js
546
static/galene.js
|
@ -2328,96 +2328,11 @@ async function gotJoined(kind, group, perms, status, data, message) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {Object<string,TransferredFile>} */
|
|
||||||
let transferredFiles = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A file in the process of being transferred.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
function TransferredFile(id, userid, up, username, name, type, size) {
|
|
||||||
/** @type {string} */
|
|
||||||
this.id = id;
|
|
||||||
/** @type {string} */
|
|
||||||
this.userid = userid;
|
|
||||||
/** @type {boolean} */
|
|
||||||
this.up = up;
|
|
||||||
/** @type {string} */
|
|
||||||
this.username = username;
|
|
||||||
/** @type {string} */
|
|
||||||
this.name = name;
|
|
||||||
/** @type {string} */
|
|
||||||
this.type = type;
|
|
||||||
/** @type {number} */
|
|
||||||
this.size = size;
|
|
||||||
/** @type {File} */
|
|
||||||
this.file = null;
|
|
||||||
/** @type {RTCPeerConnection} */
|
|
||||||
this.pc = null;
|
|
||||||
/** @type {RTCDataChannel} */
|
|
||||||
this.dc = null;
|
|
||||||
/** @type {Array<RTCIceCandidateInit>} */
|
|
||||||
this.candidates = [];
|
|
||||||
/** @type {Array<Blob|ArrayBuffer>} */
|
|
||||||
this.data = [];
|
|
||||||
/** @type {number} */
|
|
||||||
this.datalen = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
TransferredFile.prototype.fullid = function() {
|
|
||||||
return this.userid + (this.up ? '+' : '-') + this.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean} up
|
|
||||||
* @param {string} userid
|
|
||||||
* @param {string} fileid
|
|
||||||
* @returns {TransferredFile}
|
|
||||||
*/
|
|
||||||
TransferredFile.get = function(up, userid, fileid) {
|
|
||||||
return transferredFiles[userid + (up ? '+' : '-') + fileid];
|
|
||||||
};
|
|
||||||
|
|
||||||
TransferredFile.prototype.close = function() {
|
|
||||||
if(this.dc) {
|
|
||||||
this.dc.onclose = null;
|
|
||||||
this.dc.onerror = null;
|
|
||||||
this.dc.onmessage = null;
|
|
||||||
}
|
|
||||||
if(this.pc)
|
|
||||||
this.pc.close();
|
|
||||||
this.dc = null;
|
|
||||||
this.pc = null;
|
|
||||||
this.data = [];
|
|
||||||
this.datalen = 0;
|
|
||||||
delete(transferredFiles[this.fullid()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
TransferredFile.prototype.pushData = function(data) {
|
|
||||||
if(data instanceof Blob) {
|
|
||||||
this.datalen += data.size;
|
|
||||||
} else if(data instanceof ArrayBuffer) {
|
|
||||||
this.datalen += data.byteLength;
|
|
||||||
} else {
|
|
||||||
throw new Error('unexpected type for received data');
|
|
||||||
}
|
|
||||||
this.data.push(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
TransferredFile.prototype.getData = function() {
|
|
||||||
let blob = new Blob(this.data, {type: this.type});
|
|
||||||
if(blob.size != this.datalen)
|
|
||||||
throw new Error('Inconsistent data size');
|
|
||||||
this.data = [];
|
|
||||||
this.datalen = 0;
|
|
||||||
return blob;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {TransferredFile} f
|
* @param {TransferredFile} f
|
||||||
*/
|
*/
|
||||||
function fileTransferBox(f) {
|
function gotFileTransfer(f) {
|
||||||
|
f.onevent = gotFileTransferEvent;
|
||||||
let p = document.createElement('p');
|
let p = document.createElement('p');
|
||||||
if(f.up)
|
if(f.up)
|
||||||
p.textContent =
|
p.textContent =
|
||||||
|
@ -2428,27 +2343,20 @@ function fileTransferBox(f) {
|
||||||
`User ${f.username} offered to send us a file ` +
|
`User ${f.username} offered to send us a file ` +
|
||||||
`called "${f.name}" of size ${f.size}.`
|
`called "${f.name}" of size ${f.size}.`
|
||||||
let bno = null, byes = null;
|
let bno = null, byes = null;
|
||||||
if(f.up) {
|
if(!f.up) {
|
||||||
bno = document.createElement('button');
|
|
||||||
bno.textContent = 'Cancel';
|
|
||||||
bno.onclick = function(e) {
|
|
||||||
cancelFile(f);
|
|
||||||
};
|
|
||||||
bno.id = "bno-" + f.fullid();
|
|
||||||
} else {
|
|
||||||
byes = document.createElement('button');
|
byes = document.createElement('button');
|
||||||
byes.textContent = 'Accept';
|
byes.textContent = 'Accept';
|
||||||
byes.onclick = function(e) {
|
byes.onclick = function(e) {
|
||||||
getFile(f);
|
f.receive();
|
||||||
};
|
};
|
||||||
byes.id = "byes-" + f.fullid();
|
byes.id = "byes-" + f.fullid();
|
||||||
bno = document.createElement('button');
|
|
||||||
bno.textContent = 'Decline';
|
|
||||||
bno.onclick = function(e) {
|
|
||||||
rejectFile(f);
|
|
||||||
};
|
|
||||||
bno.id = "bno-" + f.fullid();
|
|
||||||
}
|
}
|
||||||
|
bno = document.createElement('button');
|
||||||
|
bno.textContent = f.up ? 'Cancel' : 'Reject';
|
||||||
|
bno.onclick = function(e) {
|
||||||
|
f.cancel();
|
||||||
|
};
|
||||||
|
bno.id = "bno-" + f.fullid();
|
||||||
let status = document.createElement('div');
|
let status = document.createElement('div');
|
||||||
status.id = 'status-' + f.fullid();
|
status.id = 'status-' + f.fullid();
|
||||||
if(!f.up) {
|
if(!f.up) {
|
||||||
|
@ -2500,334 +2408,49 @@ function setFileStatus(f, status, delyes, delno) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {TransferredFile} f
|
* @this {TransferredFile}
|
||||||
* @param {any} message
|
* @param {string} state
|
||||||
|
* @param {any} [data]
|
||||||
*/
|
*/
|
||||||
function failFile(f, message) {
|
function gotFileTransferEvent(state, data) {
|
||||||
if(!f.dc)
|
let f = this;
|
||||||
return;
|
switch(state) {
|
||||||
console.error('File transfer failed:', message);
|
case 'inviting':
|
||||||
setFileStatus(f, message ? `Failed: ${message}` : 'Failed.');
|
break;
|
||||||
f.close();
|
case 'connecting':
|
||||||
}
|
setFileStatus(f, 'Connecting...', true);
|
||||||
|
break;
|
||||||
/**
|
case 'connected':
|
||||||
* @param {string} id
|
if(f.up)
|
||||||
* @param {File} file
|
setFileStatus(f, `Sending... ${f.datalen}/${f.size}`);
|
||||||
*/
|
else
|
||||||
function offerFile(id, file) {
|
setFileStatus(f, `Receiving... ${f.datalen}/${f.size}`);
|
||||||
let fileid = newRandomId();
|
break;
|
||||||
let username = serverConnection.users[id].username;
|
case 'done':
|
||||||
let f = new TransferredFile(
|
|
||||||
fileid, id, true, username, file.name, file.type, file.size,
|
|
||||||
);
|
|
||||||
f.file = file;
|
|
||||||
transferredFiles[f.fullid()] = f;
|
|
||||||
try {
|
|
||||||
fileTransferBox(f);
|
|
||||||
serverConnection.userMessage('offerfile', id, {
|
|
||||||
id: fileid,
|
|
||||||
name: f.name,
|
|
||||||
size: f.size,
|
|
||||||
type: f.type,
|
|
||||||
});
|
|
||||||
} catch(e) {
|
|
||||||
displayError(e);
|
|
||||||
f.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {TransferredFile} f
|
|
||||||
*/
|
|
||||||
function cancelFile(f) {
|
|
||||||
serverConnection.userMessage('cancelfile', f.userid, {
|
|
||||||
id: f.id,
|
|
||||||
});
|
|
||||||
f.close();
|
|
||||||
setFileStatus(f, 'Cancelled.', true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {TransferredFile} f
|
|
||||||
*/
|
|
||||||
async function getFile(f) {
|
|
||||||
if(f.pc)
|
|
||||||
throw new Error("Download already in progress");
|
|
||||||
setFileStatus(f, 'Connecting...', true);
|
|
||||||
let pc = new RTCPeerConnection(serverConnection.getRTCConfiguration());
|
|
||||||
if(!pc)
|
|
||||||
throw new Error("Couldn't create peer connection");
|
|
||||||
f.pc = pc;
|
|
||||||
f.candidates = [];
|
|
||||||
pc.onsignalingstatechange = function(e) {
|
|
||||||
if(pc.signalingState === 'stable') {
|
|
||||||
f.candidates.forEach(c => pc.addIceCandidate(c).catch(console.warn));
|
|
||||||
f.candidates = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
pc.onicecandidate = function(e) {
|
|
||||||
serverConnection.userMessage('filedownice', f.userid, {
|
|
||||||
id: f.id,
|
|
||||||
candidate: e.candidate,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
f.dc = pc.createDataChannel('file');
|
|
||||||
f.data = [];
|
|
||||||
f.datalen = 0;
|
|
||||||
f.dc.onclose = function(e) {
|
|
||||||
try {
|
|
||||||
closeReceiveFileData(f);
|
|
||||||
} catch(e) {
|
|
||||||
failFile(f, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
f.dc.onmessage = function(e) {
|
|
||||||
try {
|
|
||||||
receiveFileData(f, e.data);
|
|
||||||
} catch(e) {
|
|
||||||
failFile(f, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
f.dc.onerror = function(e) {
|
|
||||||
/** @ts-ignore */
|
|
||||||
let err = e.error;
|
|
||||||
failFile(f, err);
|
|
||||||
};
|
|
||||||
let offer = await pc.createOffer();
|
|
||||||
if(!offer)
|
|
||||||
throw new Error("Couldn't create offer");
|
|
||||||
await pc.setLocalDescription(offer);
|
|
||||||
serverConnection.userMessage('getfile', f.userid, {
|
|
||||||
id: f.id,
|
|
||||||
offer: pc.localDescription.sdp,
|
|
||||||
});
|
|
||||||
setFileStatus(f, 'Negotiating...', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {TransferredFile} f
|
|
||||||
*/
|
|
||||||
async function rejectFile(f) {
|
|
||||||
serverConnection.userMessage('rejectfile', f.userid, {
|
|
||||||
id: f.id,
|
|
||||||
});
|
|
||||||
setFileStatus(f, 'Rejected.', true, true);
|
|
||||||
f.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {TransferredFile} f
|
|
||||||
* @param {string} sdp
|
|
||||||
*/
|
|
||||||
async function sendOfferedFile(f, sdp) {
|
|
||||||
if(f.pc)
|
|
||||||
throw new Error('Transfer already in progress');
|
|
||||||
|
|
||||||
setFileStatus(f, 'Negotiating...', true);
|
|
||||||
let pc = new RTCPeerConnection(serverConnection.getRTCConfiguration());
|
|
||||||
if(!pc)
|
|
||||||
throw new Error("Couldn't create peer connection");
|
|
||||||
f.pc = pc;
|
|
||||||
f.candidates = [];
|
|
||||||
pc.onicecandidate = function(e) {
|
|
||||||
serverConnection.userMessage('fileupice', f.userid, {
|
|
||||||
id: f.id,
|
|
||||||
candidate: e.candidate,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
pc.onsignalingstatechange = function(e) {
|
|
||||||
if(pc.signalingState === 'stable') {
|
|
||||||
f.candidates.forEach(c => pc.addIceCandidate(c).catch(console.warn));
|
|
||||||
f.candidates = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
pc.ondatachannel = function(e) {
|
|
||||||
if(f.dc)
|
|
||||||
throw new Error('Duplicate datachannel');
|
|
||||||
f.dc = /** @type{RTCDataChannel} */(e.channel);
|
|
||||||
f.dc.onclose = function(e) {
|
|
||||||
try {
|
|
||||||
closeSendFileData(f);
|
|
||||||
} catch(e) {
|
|
||||||
failFile(f, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
f.dc.onerror = function(e) {
|
|
||||||
/** @ts-ignore */
|
|
||||||
let err = e.error;
|
|
||||||
failFile(f, err);
|
|
||||||
}
|
|
||||||
f.dc.onmessage = function(e) {
|
|
||||||
try {
|
|
||||||
ackSendFileData(f, e.data);
|
|
||||||
} catch(e) {
|
|
||||||
failFile(f, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sendFileData(f).catch(e => failFile(f, e));
|
|
||||||
};
|
|
||||||
|
|
||||||
await pc.setRemoteDescription({
|
|
||||||
type: 'offer',
|
|
||||||
sdp: sdp,
|
|
||||||
});
|
|
||||||
|
|
||||||
let answer = await pc.createAnswer();
|
|
||||||
if(!answer)
|
|
||||||
throw new Error("Couldn't create answer");
|
|
||||||
await pc.setLocalDescription(answer);
|
|
||||||
serverConnection.userMessage('sendfile', f.userid, {
|
|
||||||
id: f.id,
|
|
||||||
answer: pc.localDescription.sdp,
|
|
||||||
});
|
|
||||||
setFileStatus(f, 'Uploading...', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {TransferredFile} f
|
|
||||||
* @param {string} sdp
|
|
||||||
*/
|
|
||||||
async function receiveFile(f, sdp) {
|
|
||||||
await f.pc.setRemoteDescription({
|
|
||||||
type: 'answer',
|
|
||||||
sdp: sdp,
|
|
||||||
});
|
|
||||||
setFileStatus(f, 'Downloading...', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {TransferredFile} f
|
|
||||||
*/
|
|
||||||
async function sendFileData(f) {
|
|
||||||
let r = f.file.stream().getReader();
|
|
||||||
|
|
||||||
f.dc.bufferedAmountLowThreshold = 65536;
|
|
||||||
|
|
||||||
async function write(a) {
|
|
||||||
while(f.dc.bufferedAmount > f.dc.bufferedAmountLowThreshold) {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
if(!f.dc) {
|
|
||||||
reject(new Error('File is closed.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
f.dc.onbufferedamountlow = function(e) {
|
|
||||||
if(!f.dc) {
|
|
||||||
reject(new Error('File is closed.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
f.dc.onbufferedamountlow = null;
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
f.dc.send(a);
|
|
||||||
f.datalen += a.length;
|
|
||||||
setFileStatus(f, `Uploading... ${f.datalen}/${f.size}`, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
while(true) {
|
|
||||||
let v = await r.read();
|
|
||||||
if(v.done)
|
|
||||||
break;
|
|
||||||
if(!(v.value instanceof Uint8Array))
|
|
||||||
throw new Error('Unexpected type for chunk');
|
|
||||||
if(v.value.length <= 16384) {
|
|
||||||
await write(v.value);
|
|
||||||
} else {
|
|
||||||
for(let i = 0; i < v.value.length; i += 16384) {
|
|
||||||
let a = new Uint8Array(
|
|
||||||
v.value.buffer, i, Math.min(16384, v.value.length - i),
|
|
||||||
);
|
|
||||||
await write(a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {TransferredFile} f
|
|
||||||
*/
|
|
||||||
function ackSendFileData(f, data) {
|
|
||||||
if(data === 'done' && f.datalen == f.size)
|
|
||||||
setFileStatus(f, 'Done.', true, true);
|
setFileStatus(f, 'Done.', true, true);
|
||||||
else
|
if(!f.up) {
|
||||||
setFileStatus(f, 'Failed.', true, true);
|
let url = URL.createObjectURL(data);
|
||||||
f.dc.onclose = null;
|
let a = document.createElement('a');
|
||||||
f.dc.onerror = null;
|
a.href = url;
|
||||||
f.close();
|
a.textContent = f.name;
|
||||||
}
|
a.download = f.name;
|
||||||
|
a.type = f.mimetype;
|
||||||
/**
|
a.click();
|
||||||
* @param {TransferredFile} f
|
URL.revokeObjectURL(url);
|
||||||
*/
|
}
|
||||||
function closeSendFileData(f) {
|
break;
|
||||||
setFileStatus(f, 'Failed.', true, true);
|
case 'cancelled':
|
||||||
f.close();
|
if(data)
|
||||||
}
|
setFileStatus(f, `Cancelled: ${data.toString()}.`, true, true);
|
||||||
|
else
|
||||||
/**
|
setFileStatus(f, 'Cancelled.', true, true);
|
||||||
* @param {TransferredFile} f
|
break;
|
||||||
* @param {Blob|ArrayBuffer} data
|
case 'closed':
|
||||||
*/
|
break;
|
||||||
function receiveFileData(f, data) {
|
default:
|
||||||
f.pushData(data);
|
console.error(`Unexpected state "${state}"`);
|
||||||
setFileStatus(f, `Downloading... ${f.datalen}/${f.size}`, true);
|
f.cancel(`unexpected state "${state}" (this shouldn't happen)`);
|
||||||
|
break;
|
||||||
if(f.datalen < f.size)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if(f.datalen != f.size) {
|
|
||||||
setFileStatus(f, 'Failed.', true, true);
|
|
||||||
f.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
f.dc.onmessage = null;
|
|
||||||
doneReceiveFileData(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {TransferredFile} f
|
|
||||||
*/
|
|
||||||
async function doneReceiveFileData(f) {
|
|
||||||
setFileStatus(f, 'Done.', true, true);
|
|
||||||
let blob = f.getData();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
let timer = setTimeout(function(e) { resolve(); }, 2000);
|
|
||||||
f.dc.onclose = function(e) {
|
|
||||||
clearTimeout(timer);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
f.dc.onerror = function(e) {
|
|
||||||
clearTimeout(timer);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
f.dc.send('done');
|
|
||||||
});
|
|
||||||
|
|
||||||
f.dc.onclose = null;
|
|
||||||
f.dc.onerror = null;
|
|
||||||
f.close();
|
|
||||||
|
|
||||||
let url = URL.createObjectURL(blob);
|
|
||||||
let a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.textContent = f.name;
|
|
||||||
a.download = f.name;
|
|
||||||
a.type = f.type;
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {TransferredFile} f
|
|
||||||
*/
|
|
||||||
function closeReceiveFileData(f) {
|
|
||||||
if(f.datalen !== f.size) {
|
|
||||||
setFileStatus(f, 'Failed.', true, true)
|
|
||||||
f.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2868,70 +2491,6 @@ function gotUserMessage(id, dest, username, time, privileged, kind, message) {
|
||||||
console.error(`Got unprivileged message of kind ${kind}`);
|
console.error(`Got unprivileged message of kind ${kind}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'offerfile': {
|
|
||||||
let f = new TransferredFile(
|
|
||||||
message.id, id, false, username,
|
|
||||||
message.name, message.type, message.size,
|
|
||||||
);
|
|
||||||
transferredFiles[f.fullid()] = f;
|
|
||||||
fileTransferBox(f);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'cancelfile': {
|
|
||||||
let f = TransferredFile.get(false, id, message.id);
|
|
||||||
if(!f)
|
|
||||||
throw new Error('unexpected cancelfile');
|
|
||||||
setFileStatus(f, 'Cancelled.', true, true);
|
|
||||||
f.close();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'getfile': {
|
|
||||||
let f = TransferredFile.get(true, id, message.id);
|
|
||||||
if(!f)
|
|
||||||
throw new Error('unexpected getfile');
|
|
||||||
sendOfferedFile(f, message.offer);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'rejectfile': {
|
|
||||||
let f = TransferredFile.get(true, id, message.id);
|
|
||||||
if(!f)
|
|
||||||
throw new Error('unexpected rejectfile');
|
|
||||||
setFileStatus(f, 'Rejected.', true, true);
|
|
||||||
f.close();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'sendfile': {
|
|
||||||
let f = TransferredFile.get(false, id, message.id);
|
|
||||||
if(!f)
|
|
||||||
throw new Error('unexpected sendfile');
|
|
||||||
receiveFile(f, message.answer);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'filedownice': {
|
|
||||||
let f = TransferredFile.get(true, id, message.id);
|
|
||||||
if(!f.pc) {
|
|
||||||
console.warn('Unexpected filedownice');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(f.pc.signalingState === 'stable')
|
|
||||||
f.pc.addIceCandidate(message.candidate).catch(console.warn);
|
|
||||||
else
|
|
||||||
f.candidates.push(message.candidate);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'fileupice': {
|
|
||||||
let f = TransferredFile.get(false, id, message.id);
|
|
||||||
if(!f.pc) {
|
|
||||||
console.warn('Unexpected fileupice');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(f.pc.signalingState === 'stable')
|
|
||||||
f.pc.addIceCandidate(message.candidate).catch(console.warn);
|
|
||||||
else
|
|
||||||
f.candidates.push(message.candidate);
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
console.warn(`Got unknown user message ${kind}`);
|
console.warn(`Got unknown user message ${kind}`);
|
||||||
break;
|
break;
|
||||||
|
@ -3515,7 +3074,7 @@ function sendFile(id) {
|
||||||
let files = this.files;
|
let files = this.files;
|
||||||
for(let i = 0; i < files.length; i++) {
|
for(let i = 0; i < files.length; i++) {
|
||||||
try {
|
try {
|
||||||
offerFile(id, files[i]);
|
serverConnection.sendFile(id, files[i]);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
displayError(e);
|
displayError(e);
|
||||||
|
@ -3870,6 +3429,7 @@ async function serverConnect() {
|
||||||
serverConnection.onjoined = gotJoined;
|
serverConnection.onjoined = gotJoined;
|
||||||
serverConnection.onchat = addToChatbox;
|
serverConnection.onchat = addToChatbox;
|
||||||
serverConnection.onusermessage = gotUserMessage;
|
serverConnection.onusermessage = gotUserMessage;
|
||||||
|
serverConnection.onfiletransfer = gotFileTransfer;
|
||||||
|
|
||||||
let url = `ws${location.protocol === 'https:' ? 's' : ''}://${location.host}/ws`;
|
let url = `ws${location.protocol === 'https:' ? 's' : ''}://${location.host}/ws`;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -202,6 +202,14 @@ function ServerConnection() {
|
||||||
* @type {(this: ServerConnection, id: string, dest: string, username: string, time: number, privileged: boolean, kind: string, message: unknown) => void}
|
* @type {(this: ServerConnection, id: string, dest: string, username: string, time: number, privileged: boolean, kind: string, message: unknown) => void}
|
||||||
*/
|
*/
|
||||||
this.onusermessage = null;
|
this.onusermessage = null;
|
||||||
|
/**
|
||||||
|
* @type {Object<string,TransferredFile>}
|
||||||
|
*/
|
||||||
|
this.transferredFiles = {};
|
||||||
|
/**
|
||||||
|
* @type {(this: ServerConnection, f: TransferredFile) => void}
|
||||||
|
*/
|
||||||
|
this.onfiletransfer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -380,6 +388,11 @@ ServerConnection.prototype.connect = async function(url) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
if(!(m.id in sc.users))
|
if(!(m.id in sc.users))
|
||||||
console.warn(`Unknown user ${m.id} ${m.username}`);
|
console.warn(`Unknown user ${m.id} ${m.username}`);
|
||||||
|
for(let t in sc.transferredFiles) {
|
||||||
|
let f = sc.transferredFiles[t];
|
||||||
|
if(f.userid === m.id)
|
||||||
|
f.fail('user has gone away');
|
||||||
|
}
|
||||||
delete(sc.users[m.id]);
|
delete(sc.users[m.id]);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -398,7 +411,9 @@ ServerConnection.prototype.connect = async function(url) {
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'usermessage':
|
case 'usermessage':
|
||||||
if(sc.onusermessage)
|
if(m.kind === 'filetransfer')
|
||||||
|
sc.fileTransfer(m.source, m.username, m.value);
|
||||||
|
else if(sc.onusermessage)
|
||||||
sc.onusermessage.call(
|
sc.onusermessage.call(
|
||||||
sc, m.source, m.dest, m.username, m.time,
|
sc, m.source, m.dest, m.username, m.time,
|
||||||
m.privileged, m.kind, m.value,
|
m.privileged, m.kind, m.value,
|
||||||
|
@ -1419,3 +1434,581 @@ Stream.prototype.setStatsInterval = function(ms) {
|
||||||
c.updateStats();
|
c.updateStats();
|
||||||
}, ms);
|
}, ms);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A file in the process of being transferred.
|
||||||
|
* These are stored in the ServerConnection.transferredFiles dictionary.
|
||||||
|
*
|
||||||
|
* @parm {ServerConnection} sc
|
||||||
|
* @parm {string} userid
|
||||||
|
* @parm {string} rid
|
||||||
|
* @parm {boolean} up
|
||||||
|
* @parm {string} username
|
||||||
|
* @parm {string} mimetype
|
||||||
|
* @parm {number} size
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
* State transitions:
|
||||||
|
*
|
||||||
|
* '' -> inviting -> connecting -> connected -> done -> closed
|
||||||
|
* any -> cancelled -> closed
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function TransferredFile(sc, userid, id, up, username, name, mimetype, size) {
|
||||||
|
/** @type {ServerConnection} */
|
||||||
|
this.sc = sc;
|
||||||
|
/** @type {string} */
|
||||||
|
this.userid = userid;
|
||||||
|
/** @type {string} */
|
||||||
|
this.id = id;
|
||||||
|
/** @type {boolean} */
|
||||||
|
this.up = up;
|
||||||
|
/** @type {string} */
|
||||||
|
this.state = '';
|
||||||
|
/** @type {string} */
|
||||||
|
this.username = username;
|
||||||
|
/** @type {string} */
|
||||||
|
this.name = name;
|
||||||
|
/** @type {string} */
|
||||||
|
this.mimetype = mimetype;
|
||||||
|
/** @type {number} */
|
||||||
|
this.size = size;
|
||||||
|
/** @type {File} */
|
||||||
|
this.file = null;
|
||||||
|
/** @type {boolean} */
|
||||||
|
this.closed = false;
|
||||||
|
/** @type {RTCPeerConnection} */
|
||||||
|
this.pc = null;
|
||||||
|
/** @type {RTCDataChannel} */
|
||||||
|
this.dc = null;
|
||||||
|
/** @type {Array<RTCIceCandidateInit>} */
|
||||||
|
this.candidates = [];
|
||||||
|
/** @type {Array<Blob|ArrayBuffer>} */
|
||||||
|
this.data = [];
|
||||||
|
/** @type {number} */
|
||||||
|
this.datalen = 0;
|
||||||
|
/** @type {(this: TransferredFile, type: string, [data]: string) => void} */
|
||||||
|
this.onevent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full id of this file transfer, used as a key in the transferredFiles
|
||||||
|
* dictionary.
|
||||||
|
*/
|
||||||
|
TransferredFile.prototype.fullid = function() {
|
||||||
|
return this.userid + (this.up ? '+' : '-') + this.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a transferred file from the transferredFiles dictionary.
|
||||||
|
*
|
||||||
|
* @param {string} userid
|
||||||
|
* @param {string} fileid
|
||||||
|
* @param {boolean} up
|
||||||
|
* @returns {TransferredFile}
|
||||||
|
*/
|
||||||
|
ServerConnection.prototype.getTransferredFile = function(userid, fileid, up) {
|
||||||
|
return this.transferredFiles[userid + (up ? '+' : '-') + fileid];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a file transfer and remove it from the transferredFiles dictionary.
|
||||||
|
* Do not call this, call 'cancel' instead.
|
||||||
|
*/
|
||||||
|
TransferredFile.prototype.close = function() {
|
||||||
|
let f = this;
|
||||||
|
if(f.state === 'closed')
|
||||||
|
return;
|
||||||
|
if(f.state !== 'done' && f.state !== 'cancelled')
|
||||||
|
console.warn(
|
||||||
|
`TransferredFile.close called in unexpected state ${f.state}`,
|
||||||
|
);
|
||||||
|
if(f.dc) {
|
||||||
|
f.dc.onclose = null;
|
||||||
|
f.dc.onerror = null;
|
||||||
|
f.dc.onmessage = null;
|
||||||
|
}
|
||||||
|
if(f.pc)
|
||||||
|
f.pc.close();
|
||||||
|
f.dc = null;
|
||||||
|
f.pc = null;
|
||||||
|
f.data = [];
|
||||||
|
f.datalen = 0;
|
||||||
|
delete(f.sc.transferredFiles[f.fullid()]);
|
||||||
|
f.event('closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buffer a chunk of data received during a file transfer. Do not call this.
|
||||||
|
*
|
||||||
|
* @param {Blob|ArrayBuffer} data
|
||||||
|
*/
|
||||||
|
TransferredFile.prototype.bufferData = function(data) {
|
||||||
|
let f = this;
|
||||||
|
if(f.up)
|
||||||
|
throw new Error('buffering data in the wrong direction');
|
||||||
|
if(data instanceof Blob) {
|
||||||
|
f.datalen += data.size;
|
||||||
|
} else if(data instanceof ArrayBuffer) {
|
||||||
|
f.datalen += data.byteLength;
|
||||||
|
} else {
|
||||||
|
throw new Error('unexpected type for received data');
|
||||||
|
}
|
||||||
|
f.data.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retreive the data buffered during a file transfer. Don't call this.
|
||||||
|
*
|
||||||
|
* @returns {Blob}
|
||||||
|
*/
|
||||||
|
TransferredFile.prototype.getBufferedData = function() {
|
||||||
|
let f = this;
|
||||||
|
if(f.up)
|
||||||
|
throw new Error('buffering data in wrong direction');
|
||||||
|
let blob = new Blob(f.data, {type: f.mimetype});
|
||||||
|
if(blob.size != f.datalen)
|
||||||
|
throw new Error('Inconsistent data size');
|
||||||
|
f.data = [];
|
||||||
|
f.datalen = 0;
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the file's state, and call the onevent callback.
|
||||||
|
*
|
||||||
|
* This calls the callback even if the state didn't change, which is
|
||||||
|
* useful if the client needs to display a progress bar.
|
||||||
|
*
|
||||||
|
* @param {string} state
|
||||||
|
* @param {any} [data]
|
||||||
|
*/
|
||||||
|
TransferredFile.prototype.event = function(state, data) {
|
||||||
|
let f = this;
|
||||||
|
f.state = state;
|
||||||
|
if(f.onevent)
|
||||||
|
f.onevent.call(f, state, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a file transfer.
|
||||||
|
*
|
||||||
|
* Depending on the state, this will either forcibly close the connection,
|
||||||
|
* send a handshake, or do nothing. It will set the state to cancelled.
|
||||||
|
*
|
||||||
|
* @param {string|Error} [data]
|
||||||
|
*/
|
||||||
|
TransferredFile.prototype.cancel = function(data) {
|
||||||
|
let f = this;
|
||||||
|
if(f.state === 'closed')
|
||||||
|
return;
|
||||||
|
if(f.state !== '' && f.state !== 'done' && f.state !== 'cancelled') {
|
||||||
|
let m = {
|
||||||
|
type: f.up ? 'cancel' : 'reject',
|
||||||
|
id: f.id,
|
||||||
|
};
|
||||||
|
if(data)
|
||||||
|
m.message = data.toString();
|
||||||
|
f.sc.userMessage('filetransfer', f.userid, m);
|
||||||
|
}
|
||||||
|
if(f.state !== 'done' && f.state !== 'cancelled')
|
||||||
|
f.event('cancelled', data);
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcibly terminate a file transfer.
|
||||||
|
*
|
||||||
|
* This is like cancel, but will not attempt to handshake.
|
||||||
|
* Use cancel instead of this, unless you know what you are doing.
|
||||||
|
*
|
||||||
|
* @param {string|Error} [data]
|
||||||
|
*/
|
||||||
|
TransferredFile.prototype.fail = function(data) {
|
||||||
|
let f = this;
|
||||||
|
if(f.state === 'done' || f.state === 'cancelled' || f.state === 'closed')
|
||||||
|
return;
|
||||||
|
f.event('cancelled', data);
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate a file upload.
|
||||||
|
*
|
||||||
|
* This will cause the onfiletransfer callback to be called, at which
|
||||||
|
* point you should set up the onevent callback.
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @param {File} file
|
||||||
|
*/
|
||||||
|
ServerConnection.prototype.sendFile = function(id, file) {
|
||||||
|
let sc = this;
|
||||||
|
let fileid = newRandomId();
|
||||||
|
let user = sc.users[id];
|
||||||
|
if(!user)
|
||||||
|
throw new Error('offering upload to unknown user');
|
||||||
|
let f = new TransferredFile(
|
||||||
|
sc, id, fileid, true, user.username, file.name, file.type, file.size,
|
||||||
|
);
|
||||||
|
f.file = file;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(sc.onfiletransfer)
|
||||||
|
sc.onfiletransfer.call(sc, f);
|
||||||
|
else
|
||||||
|
throw new Error('this client does not implement file transfer');
|
||||||
|
} catch(e) {
|
||||||
|
f.cancel(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sc.transferredFiles[f.fullid()] = f;
|
||||||
|
sc.userMessage('filetransfer', id, {
|
||||||
|
type: 'invite',
|
||||||
|
id: fileid,
|
||||||
|
name: f.name,
|
||||||
|
size: f.size,
|
||||||
|
mimetype: f.mimetype,
|
||||||
|
});
|
||||||
|
f.event('inviting');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive a file.
|
||||||
|
*
|
||||||
|
* Call this after the onfiletransfer callback has yielded an incoming
|
||||||
|
* file (up field set to false). If you wish to reject the file transfer,
|
||||||
|
* call cancel instead.
|
||||||
|
*/
|
||||||
|
TransferredFile.prototype.receive = async function() {
|
||||||
|
let f = this;
|
||||||
|
if(f.up)
|
||||||
|
throw new Error('Receiving in wrong direction');
|
||||||
|
if(f.pc)
|
||||||
|
throw new Error('Download already in progress');
|
||||||
|
let pc = new RTCPeerConnection(serverConnection.getRTCConfiguration());
|
||||||
|
if(!pc) {
|
||||||
|
let err = new Error("Couldn't create peer connection");
|
||||||
|
f.fail(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
f.pc = pc;
|
||||||
|
f.event('connecting');
|
||||||
|
|
||||||
|
f.candidates = [];
|
||||||
|
pc.onsignalingstatechange = function(e) {
|
||||||
|
if(pc.signalingState === 'stable') {
|
||||||
|
f.candidates.forEach(c => pc.addIceCandidate(c).catch(console.warn));
|
||||||
|
f.candidates = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pc.onicecandidate = function(e) {
|
||||||
|
serverConnection.userMessage('filetransfer', f.userid, {
|
||||||
|
type: 'downice',
|
||||||
|
id: f.id,
|
||||||
|
candidate: e.candidate,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
f.dc = pc.createDataChannel('file');
|
||||||
|
f.data = [];
|
||||||
|
f.datalen = 0;
|
||||||
|
f.dc.onclose = function(e) {
|
||||||
|
f.cancel('remote peer closed connection');
|
||||||
|
};
|
||||||
|
f.dc.onmessage = function(e) {
|
||||||
|
f.receiveData(e.data).catch(e => f.cancel(e));
|
||||||
|
};
|
||||||
|
f.dc.onerror = function(e) {
|
||||||
|
/** @ts-ignore */
|
||||||
|
let err = e.error;
|
||||||
|
f.cancel(err)
|
||||||
|
};
|
||||||
|
let offer = await pc.createOffer();
|
||||||
|
if(!offer) {
|
||||||
|
f.cancel(new Error("Couldn't create offer"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await pc.setLocalDescription(offer);
|
||||||
|
f.sc.userMessage('filetransfer', f.userid, {
|
||||||
|
type: 'offer',
|
||||||
|
id: f.id,
|
||||||
|
sdp: pc.localDescription.sdp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Negotiate a file transfer on the sender side. Don't call this.
|
||||||
|
*
|
||||||
|
* @param {string} sdp
|
||||||
|
*/
|
||||||
|
TransferredFile.prototype.answer = async function(sdp) {
|
||||||
|
let f = this;
|
||||||
|
if(!f.up)
|
||||||
|
throw new Error('Sending file in wrong direction');
|
||||||
|
if(f.pc)
|
||||||
|
throw new Error('Transfer already in progress');
|
||||||
|
let pc = new RTCPeerConnection(serverConnection.getRTCConfiguration());
|
||||||
|
if(!pc) {
|
||||||
|
let err = new Error("Couldn't create peer connection");
|
||||||
|
f.fail(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
f.pc = pc;
|
||||||
|
f.event('connecting');
|
||||||
|
|
||||||
|
f.candidates = [];
|
||||||
|
pc.onicecandidate = function(e) {
|
||||||
|
serverConnection.userMessage('filetransfer', f.userid, {
|
||||||
|
type: 'upice',
|
||||||
|
id: f.id,
|
||||||
|
candidate: e.candidate,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
pc.onsignalingstatechange = function(e) {
|
||||||
|
if(pc.signalingState === 'stable') {
|
||||||
|
f.candidates.forEach(c => pc.addIceCandidate(c).catch(console.warn));
|
||||||
|
f.candidates = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pc.ondatachannel = function(e) {
|
||||||
|
if(f.dc) {
|
||||||
|
f.cancel(new Error('Duplicate datachannel'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
f.dc = /** @type{RTCDataChannel} */(e.channel);
|
||||||
|
f.dc.onclose = function(e) {
|
||||||
|
f.cancel('remote peer closed connection');
|
||||||
|
};
|
||||||
|
f.dc.onerror = function(e) {
|
||||||
|
/** @ts-ignore */
|
||||||
|
let err = e.error;
|
||||||
|
f.cancel(err);
|
||||||
|
}
|
||||||
|
f.dc.onmessage = function(e) {
|
||||||
|
if(e.data === 'done' && f.datalen === f.size) {
|
||||||
|
f.event('done');
|
||||||
|
f.dc.onclose = null;
|
||||||
|
f.dc.onerror = null;
|
||||||
|
f.close();
|
||||||
|
} else {
|
||||||
|
f.cancel(new Error('unexpected data from receiver'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.send().catch(e => f.cancel(e));
|
||||||
|
};
|
||||||
|
|
||||||
|
await pc.setRemoteDescription({
|
||||||
|
type: 'offer',
|
||||||
|
sdp: sdp,
|
||||||
|
});
|
||||||
|
|
||||||
|
let answer = await pc.createAnswer();
|
||||||
|
if(!answer)
|
||||||
|
throw new Error("Couldn't create answer");
|
||||||
|
await pc.setLocalDescription(answer);
|
||||||
|
serverConnection.userMessage('filetransfer', f.userid, {
|
||||||
|
type: 'answer',
|
||||||
|
id: f.id,
|
||||||
|
sdp: pc.localDescription.sdp,
|
||||||
|
});
|
||||||
|
|
||||||
|
f.event('connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transfer file data. Don't call this, it is called automatically
|
||||||
|
* after negotiation completes.
|
||||||
|
*/
|
||||||
|
TransferredFile.prototype.send = async function() {
|
||||||
|
let f = this;
|
||||||
|
if(!f.up)
|
||||||
|
throw new Error('sending in wrong direction');
|
||||||
|
let r = f.file.stream().getReader();
|
||||||
|
|
||||||
|
f.dc.bufferedAmountLowThreshold = 65536;
|
||||||
|
|
||||||
|
async function write(a) {
|
||||||
|
while(f.dc.bufferedAmount > f.dc.bufferedAmountLowThreshold) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
if(!f.dc) {
|
||||||
|
reject(new Error('File is closed.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
f.dc.onbufferedamountlow = function(e) {
|
||||||
|
if(!f.dc) {
|
||||||
|
reject(new Error('File is closed.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
f.dc.onbufferedamountlow = null;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
f.dc.send(a);
|
||||||
|
f.datalen += a.length;
|
||||||
|
// we're already in the connected state, but invoke callbacks to
|
||||||
|
// that the application can display progress
|
||||||
|
f.event('connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
let v = await r.read();
|
||||||
|
if(v.done)
|
||||||
|
break;
|
||||||
|
let data = v.value;
|
||||||
|
if(!(data instanceof Uint8Array))
|
||||||
|
throw new Error('Unexpected type for chunk');
|
||||||
|
/* Base SCTP only supports up to 16kB data chunks. There are
|
||||||
|
extensions to handle larger chunks, but they don't interoperate
|
||||||
|
between browsers, so we chop the file into small pieces. */
|
||||||
|
if(data.length <= 16384) {
|
||||||
|
await write(data);
|
||||||
|
} else {
|
||||||
|
for(let i = 0; i < v.value.length; i += 16384) {
|
||||||
|
let d = new Uint8Array(
|
||||||
|
data.buffer, i, Math.min(16384, data.length - i),
|
||||||
|
);
|
||||||
|
await write(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} sdp
|
||||||
|
*/
|
||||||
|
TransferredFile.prototype.receiveFile = async function(sdp) {
|
||||||
|
let f = this;
|
||||||
|
if(f.up)
|
||||||
|
throw new Error('Receiving in wrong direction');
|
||||||
|
await f.pc.setRemoteDescription({
|
||||||
|
type: 'answer',
|
||||||
|
sdp: sdp,
|
||||||
|
});
|
||||||
|
f.event('connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Blob|ArrayBuffer} data
|
||||||
|
*/
|
||||||
|
TransferredFile.prototype.receiveData = async function(data) {
|
||||||
|
let f = this;
|
||||||
|
if(f.up)
|
||||||
|
throw new Error('Receiving in wrong direction');
|
||||||
|
f.bufferData(data);
|
||||||
|
|
||||||
|
if(f.datalen < f.size) {
|
||||||
|
f.event('connected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.dc.onmessage = null;
|
||||||
|
|
||||||
|
if(f.datalen != f.size) {
|
||||||
|
f.cancel('unexpected file size');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let blob = f.getBufferedData();
|
||||||
|
f.event('done', blob);
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
let timer = setTimeout(function(e) { resolve(); }, 2000);
|
||||||
|
f.dc.onclose = function(e) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
f.dc.onerror = function(e) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
f.dc.send('done');
|
||||||
|
});
|
||||||
|
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {string} username
|
||||||
|
* @param {object} message
|
||||||
|
*/
|
||||||
|
ServerConnection.prototype.fileTransfer = function(id, username, message) {
|
||||||
|
let sc = this;
|
||||||
|
switch(message.type) {
|
||||||
|
case 'invite': {
|
||||||
|
let f = new TransferredFile(
|
||||||
|
sc, id, message.id, false, username,
|
||||||
|
message.name, message.mimetype, message.size,
|
||||||
|
);
|
||||||
|
f.state = 'inviting';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(sc.onfiletransfer)
|
||||||
|
sc.onfiletransfer.call(sc, f);
|
||||||
|
else
|
||||||
|
f.cancel('this client does not implement file transfer');
|
||||||
|
} catch(e) {
|
||||||
|
f.cancel(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(f.fullid() in sc.transferredFiles) {
|
||||||
|
console.error('Duplicate id for file transfer');
|
||||||
|
f.cancel("duplicate id (this shouldn't happen)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sc.transferredFiles[f.fullid()] = f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'offer': {
|
||||||
|
let f = sc.getTransferredFile(id, message.id, true);
|
||||||
|
if(!f) {
|
||||||
|
console.error('Unexpected offer for file transfer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
f.answer(message.sdp).catch(e => f.cancel(e));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'answer': {
|
||||||
|
let f = sc.getTransferredFile(id, message.id, false);
|
||||||
|
if(!f) {
|
||||||
|
console.error('Unexpected answer for file transfer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
f.receiveFile(message.sdp).catch(e => f.cancel(e));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'downice':
|
||||||
|
case 'upice': {
|
||||||
|
let f = sc.getTransferredFile(
|
||||||
|
id, message.id, message.type === 'downice',
|
||||||
|
);
|
||||||
|
if(!f || !f.pc) {
|
||||||
|
console.warn(`Unexpected ${message.type} for file transfer`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(f.pc.signalingState === 'stable')
|
||||||
|
f.pc.addIceCandidate(message.candidate).catch(console.warn);
|
||||||
|
else
|
||||||
|
f.candidates.push(message.candidate);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'cancel':
|
||||||
|
case 'reject': {
|
||||||
|
let f = sc.getTransferredFile(id, message.id, message.type === 'reject');
|
||||||
|
if(!f) {
|
||||||
|
console.error(`Unexpected ${message.type} for file transfer`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
f.event('cancelled');
|
||||||
|
f.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.error(`Unknown filetransfer message ${message.type}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue