diff options
-rw-r--r-- | client-web/source/helper.ts | 2 | ||||
-rw-r--r-- | client-web/source/resource/file.ts | 92 | ||||
-rw-r--r-- | client-web/source/user/remote.ts | 12 | ||||
-rw-r--r-- | readme.md | 2 |
4 files changed, 90 insertions, 18 deletions
diff --git a/client-web/source/helper.ts b/client-web/source/helper.ts index d43fc3e..adb5f08 100644 --- a/client-web/source/helper.ts +++ b/client-web/source/helper.ts @@ -84,3 +84,5 @@ export function notify(body: string, author?: string) { else new Notification(`keks-meet`, { body }) } + +export function sleep(delay: number) { return new Promise(r => setTimeout(r, delay)) }
\ No newline at end of file diff --git a/client-web/source/resource/file.ts b/client-web/source/resource/file.ts index c54abc8..d76961d 100644 --- a/client-web/source/resource/file.ts +++ b/client-web/source/resource/file.ts @@ -1,23 +1,32 @@ -import { ebutton, ediv, espan } from "../helper.ts"; +import { ebutton, ediv, espan, sleep } from "../helper.ts"; import { LocalResource, ResourceHandlerDecl } from "./mod.ts"; export const resource_file: ResourceHandlerDecl = { kind: "file", new_remote(info, user, enable) { + const download_button = ebutton("Download", { + onclick: self => { + enable() + self.textContent = "Downloading…" + self.disabled = true + } + }) return { info, el: ediv({}, espan(`File: ${JSON.stringify(info.label)}`), - ebutton("Download", { - onclick: self => { - enable() - self.disabled = true - } - }) + download_button, ), on_statechange(_s) { }, - on_enable(channel, _disable) { + on_enable(channel, disable) { if (!(channel instanceof RTCDataChannel)) throw new Error("not a data channel"); + // TODO stream + let position = 0 + const buffer = new Uint8Array(info.size!) + + const display = transfer_status_el() + this.el.appendChild(display.el) + channel.onopen = _ev => { console.log(`${user.display_name}: channel open`); } @@ -26,6 +35,25 @@ export const resource_file: ResourceHandlerDecl = { } channel.onclose = _ev => { console.log(`${user.display_name}: channel closed`); + const a = document.createElement("a") + a.href = URL.createObjectURL(new Blob([buffer], { type: "text/plain" })) + a.download = info.label ?? "file" + a.click() + this.el.removeChild(display.el) + download_button.disabled = false + download_button.textContent = "Download" + disable() + } + channel.onmessage = ev => { + const reader = new FileReader(); + reader.onload = function (event) { + const arr = new Uint8Array(event.target!.result as ArrayBuffer); + for (let i = 0; i < arr.length; i++, position++) { + buffer[position] = arr[i] + } + display.status = `${position} / ${info.size}` + }; + reader.readAsArrayBuffer(ev.data); } } } @@ -57,24 +85,64 @@ function file_res_inner(file: File): LocalResource { ), on_request(user, create_channel) { const channel = create_channel() + channel.bufferedAmountLowThreshold = 1 << 16 // this appears to be the buffer size in firefox for reading files const reader = file.stream().getReader() - console.log(`${user.display_name} started requested file`); - channel.onbufferedamountlow = async () => { + + console.log(`${user.display_name} started transfer`); + const display = transfer_status_el() + transfers_el.appendChild(display.el) + display.status = "Waiting for data channel to open…" + let position = 0 + + const finish = async () => { + while (channel.bufferedAmount) { + display.status = `Draining buffers… (buffer: ${channel.bufferedAmount})` + await sleep(10) + } + return channel.close() + } + const feed = async () => { const { value: chunk, done } = await reader.read() - console.log(chunk, done); + if (!chunk) console.warn("no chunk"); + if (done) return await finish() + position += chunk.length channel.send(chunk) - if (!done) console.log("transfer done"); + display.status = `${position} / ${file.size} (buffer: ${channel.bufferedAmount})` } + const feed_until_full = async () => { + // this has to do with a bad browser implementation + // https://github.com/w3c/webrtc-pc/issues/1979 + while (channel.bufferedAmount < channel.bufferedAmountLowThreshold * 2 && channel.readyState == "open") { + await feed() + } + } + channel.onbufferedamountlow = () => feed_until_full() channel.onopen = _ev => { + display.status = "Buffering…" console.log(`${user.display_name}: channel open`); + feed_until_full() } channel.onerror = _ev => { console.log(`${user.display_name}: channel error`); } + channel.onclosing = _ev => { + display.status = "Channel closing…" + } channel.onclose = _ev => { console.log(`${user.display_name}: channel closed`); + transfers_el.removeChild(display.el) } return channel } } +} + +function transfer_status_el() { + const status = espan("…") + return { + el: ediv({ class: "progress" }, status), + set status(s: string) { + status.textContent = s + } + } }
\ No newline at end of file diff --git a/client-web/source/user/remote.ts b/client-web/source/user/remote.ts index 2c3fd58..5517dbe 100644 --- a/client-web/source/user/remote.ts +++ b/client-web/source/user/remote.ts @@ -61,7 +61,7 @@ export class RemoteUser extends User { } this.pc.onnegotiationneeded = () => { log("webrtc", `negotiation needed: ${this.display_name}`) - if (this.negotiation_busy && this.pc.signalingState == "stable") return + // if (this.pc.signalingState != "stable") return this.offer() this.update_stats() } @@ -119,8 +119,9 @@ export class RemoteUser extends User { } if (message.request_stop) { const sender = this.senders.get(message.request_stop.id) - if (!sender) return log({ scope: "*", warn: true }, "somebody requested us to stop transmitting an unknown resource") - this.pc.removeTrack(sender) + if (sender) this.pc.removeTrack(sender) + const dc = this.data_channels.get(message.request_stop.id) + if (dc) dc.close() } } send_to(message: RelayMessage) { @@ -168,14 +169,14 @@ export class RemoteUser extends User { let stuff = ""; stuff += `ice-conn=${this.pc.iceConnectionState}; ice-gathering=${this.pc.iceGatheringState}; ice-trickle=${this.pc.canTrickleIceCandidates}; signaling=${this.pc.signalingState};\n` stats.forEach(s => { - console.log("stat", s); + // console.log("stat", s); if (s.type == "candidate-pair" && s.selected) { //@ts-ignore trust me, this works if (!stats.get) return console.warn("no RTCStatsReport.get"); //@ts-ignore trust me, this works const cpstat = stats.get(s.localCandidateId) if (!cpstat) return console.warn("no stats"); - console.log("cp", cpstat); + // console.log("cp", cpstat); stuff += `via ${cpstat.candidateType}:${cpstat.protocol}:${cpstat.address}\n` } else if (s.type == "codec") { stuff += `using ${s.codecType ?? "dec/enc"}:${s.mimeType}(${s.sdpFmtpLine})\n` @@ -186,5 +187,4 @@ export class RemoteUser extends User { console.warn(e); } } - }
\ No newline at end of file @@ -132,6 +132,8 @@ system works as follows: - Pin js by bookmarking data:text/html loader page - convert protocol enums to `A | B | C` - add "contributing" stuff to readme +- download files in a streaming manner. + - workaround using service worker ## Protocol |