summaryrefslogtreecommitdiff
path: root/client-web/source
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2022-10-26 23:54:33 +0200
committermetamuffin <metamuffin@disroot.org>2022-10-26 23:54:33 +0200
commit2a83c8bdbdd5a67b6068420520e83524f4a6f6bd (patch)
tree8638903b93eb929ca74e5027a1816820ff0bc346 /client-web/source
parentd0162d41438c7ee3d9bc5321f73ed33defc443a3 (diff)
downloadkeks-meet-2a83c8bdbdd5a67b6068420520e83524f4a6f6bd.tar
keks-meet-2a83c8bdbdd5a67b6068420520e83524f4a6f6bd.tar.bz2
keks-meet-2a83c8bdbdd5a67b6068420520e83524f4a6f6bd.tar.zst
some code for streamed downloads
Diffstat (limited to 'client-web/source')
-rw-r--r--client-web/source/index.ts4
-rw-r--r--client-web/source/keybinds.ts2
-rw-r--r--client-web/source/logger.ts1
-rw-r--r--client-web/source/resource/file.ts30
-rw-r--r--client-web/source/sw/download_stream.ts57
-rw-r--r--client-web/source/sw/init.ts35
-rw-r--r--client-web/source/sw/worker.ts86
7 files changed, 197 insertions, 18 deletions
diff --git a/client-web/source/index.ts b/client-web/source/index.ts
index c16fa23..277bf00 100644
--- a/client-web/source/index.ts
+++ b/client-web/source/index.ts
@@ -5,6 +5,7 @@
*/
/// <reference lib="dom" />
+import { init_serviceworker } from "./sw/init.ts";
import { ediv, OVERLAYS } from "./helper.ts";
import { setup_keybinds } from "./keybinds.ts";
import { log, LOGGER_CONTAINER } from "./logger.ts"
@@ -56,6 +57,7 @@ export async function main() {
if (!globalThis.RTCPeerConnection) return log({ scope: "webrtc", error: true }, "WebRTC not supported.")
if (!globalThis.isSecureContext) log({ scope: "*", warn: true }, "This page is not in a 'Secure Context'")
if (!globalThis.crypto.subtle) return log({ scope: "crypto", error: true }, "SubtleCrypto not availible")
+ if (!globalThis.navigator.serviceWorker) log({ scope: "*", warn: true }, "Your browser does not support the Service Worker API, some features dont work without it.")
if (room_name.length < 8) log({ scope: "crypto", warn: true }, "Room name is very short. e2ee is insecure!")
if (room_name.length == 0) return window.location.href = "/" // send them back to the start page
if (PREFS.warn_redirect) log({ scope: "crypto", warn: true }, "You were redirected from the old URL format. The server knows the room name now - e2ee is insecure!")
@@ -69,4 +71,6 @@ export async function main() {
new MenuBr().shown = true
}
document.body.append(ROOM_CONTAINER, OVERLAYS, LOGGER_CONTAINER)
+
+ if (globalThis.navigator.serviceWorker) init_serviceworker()
}
diff --git a/client-web/source/keybinds.ts b/client-web/source/keybinds.ts
index ddd1c82..5463e47 100644
--- a/client-web/source/keybinds.ts
+++ b/client-web/source/keybinds.ts
@@ -7,6 +7,7 @@
import { create_camera_res, create_mic_res, create_screencast_res } from "./resource/track.ts";
import { Room } from "./room.ts"
+import { update_serviceworker } from "./sw/init.ts";
export function setup_keybinds(room: Room) {
let command_mode = false
@@ -29,6 +30,7 @@ export function setup_keybinds(room: Room) {
if (ev.code == "KeyS") room.local_user.await_add_resource(create_screencast_res())
if (ev.code == "KeyC" && !ev.ctrlKey) room.local_user.await_add_resource(create_camera_res())
if (ev.code == "KeyC" && ev.ctrlKey) room.local_user.resources.forEach(t => t.destroy())
+ if (ev.code == "KeyU") if (window.confirm("really update?")) update_serviceworker()
}
command_mode = false
})
diff --git a/client-web/source/logger.ts b/client-web/source/logger.ts
index 351cb2e..7d85f5e 100644
--- a/client-web/source/logger.ts
+++ b/client-web/source/logger.ts
@@ -16,6 +16,7 @@ const log_scope_color = {
ws: "#544aff",
media: "#4af5ff",
rnnoise: "#4aff7e",
+ sw: "#4aff7e",
usermodel: "#a6ff4a",
dc: "#4af5ff",
}
diff --git a/client-web/source/resource/file.ts b/client-web/source/resource/file.ts
index 18a1ac7..fedce7b 100644
--- a/client-web/source/resource/file.ts
+++ b/client-web/source/resource/file.ts
@@ -7,6 +7,7 @@
import { ebutton, ediv, espan, sleep } from "../helper.ts";
import { log } from "../logger.ts";
+import { StreamDownload } from "../sw/download_stream.ts";
import { LocalResource, ResourceHandlerDecl } from "./mod.ts";
const MAX_CHUNK_SIZE = 1 << 15;
@@ -30,9 +31,12 @@ export const resource_file: ResourceHandlerDecl = {
on_statechange(_s) { },
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 download = StreamDownload(
+ info.size!, info.label ?? "file",
+ position => {
+ display.status = `${position} / ${info.size}`
+ }
+ );
const display = transfer_status_el()
this.el.appendChild(display.el)
@@ -45,25 +49,15 @@ export const resource_file: ResourceHandlerDecl = {
}
channel.onclose = _ev => {
log("dc", `${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.close()
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);
+ // console.log(ev.data);
+ download.write(ev.data)
}
}
}
@@ -113,9 +107,9 @@ function file_res_inner(file: File): LocalResource {
return channel.close()
}
const feed = async () => {
- const { value: chunk, done }: { value: Uint8Array, done: boolean } = await reader.read()
+ const { value: chunk, done }: { value?: Uint8Array, done: boolean } = await reader.read()
if (done) return await finish()
- if (!chunk) console.warn("no chunk");
+ if (!chunk) return console.warn("no chunk");
position += chunk.length
for (let i = 0; i < chunk.length; i += MAX_CHUNK_SIZE) {
channel.send(chunk.slice(i, Math.min(i + MAX_CHUNK_SIZE, chunk.length)))
diff --git a/client-web/source/sw/download_stream.ts b/client-web/source/sw/download_stream.ts
new file mode 100644
index 0000000..4eaf382
--- /dev/null
+++ b/client-web/source/sw/download_stream.ts
@@ -0,0 +1,57 @@
+import { SW } from "./init.ts"
+
+// export function StreamDownload(size: number, filename?: string, progress?: (position: number) => void) {
+// let position = 0
+// const buffer = new Uint8Array(size)
+// return {
+// close() {
+// const a = document.createElement("a")
+// a.href = URL.createObjectURL(new Blob([buffer], { type: "text/plain" }))
+// a.download = filename ?? "file"
+// a.click()
+// },
+// write(chunk: Blob) {
+// 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]
+// }
+// if (progress) progress(position)
+// };
+// reader.readAsArrayBuffer(chunk);
+// }
+// }
+// }
+
+export function StreamDownload(size: number, filename?: string, progress?: (position: number) => void) {
+ let position = 0
+
+ const path = `/download/${encodeURIComponent(filename ?? "file")}`
+
+ const { port1, port2 } = new MessageChannel()
+ SW!.postMessage({ path, size }, [port2])
+
+ const a = document.createElement("a")
+ a.href = path
+ a.download = filename ?? "file"
+ a.target = "_blank"
+ a.click()
+
+ return {
+ close() {
+ port1.postMessage("end")
+ },
+ write(chunk: Blob) {
+ const reader = new FileReader();
+ reader.onload = function (event) {
+ const arr = new Uint8Array(event.target!.result as ArrayBuffer);
+ console.log("send", arr);
+ port1.postMessage(arr)
+ position += arr.length
+ if (progress) progress(position)
+ };
+ reader.readAsArrayBuffer(chunk);
+ }
+ }
+}
diff --git a/client-web/source/sw/init.ts b/client-web/source/sw/init.ts
new file mode 100644
index 0000000..d082038
--- /dev/null
+++ b/client-web/source/sw/init.ts
@@ -0,0 +1,35 @@
+/*
+ This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet)
+ which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
+ Copyright (C) 2022 metamuffin <metamuffin@disroot.org>
+*/
+/// <reference lib="dom" />
+
+import { log } from "../logger.ts"
+
+export let SW: ServiceWorker | undefined
+export async function init_serviceworker() {
+ let reg = await globalThis.navigator.serviceWorker.getRegistration()
+ if (reg) {
+ log("sw", "service worker already installed")
+ } else {
+ log("sw", "registering service worker")
+ await globalThis.navigator.serviceWorker.register("/sw.js", { scope: "/", type: "module" })
+ log("sw", "worker installed")
+ reg = await globalThis.navigator.serviceWorker.getRegistration();
+ if (!reg) throw new Error("we just registered the sw!?");
+ }
+ const i = setInterval(() => {
+ if (reg!.active) {
+ SW = reg!.active
+ clearInterval(i)
+ }
+ }, 100)
+}
+
+export async function update_serviceworker() {
+ const regs = await globalThis.navigator.serviceWorker.getRegistrations()
+ for (const r of regs) await r.unregister()
+ log("sw", "cleared all workers")
+ setTimeout(() => window.location.reload(), 500)
+}
diff --git a/client-web/source/sw/worker.ts b/client-web/source/sw/worker.ts
new file mode 100644
index 0000000..25f6bab
--- /dev/null
+++ b/client-web/source/sw/worker.ts
@@ -0,0 +1,86 @@
+/*
+ This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet)
+ which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
+ Copyright (C) 2022 metamuffin <metamuffin@disroot.org>
+*/
+/// <reference no-default-lib="true"/>
+
+/// <reference lib="esnext" />
+/// <reference lib="webworker" />
+declare const self: ServiceWorkerGlobalScope; export { };
+
+console.log("hello from the keks-meet service worker");
+console.log(self.origin)
+
+// let cache: Cache;
+
+self.addEventListener("install", event => {
+ console.log("install");
+ self.skipWaiting()
+ event.waitUntil(caches.delete("v1"))
+})
+self.addEventListener("activate", _event => {
+ console.log("activate");
+ self.clients.claim()
+ // event.waitUntil((async () => {
+ // cache = await caches.open("v1")
+ // cache.addAll([
+ // "/assets/bundle.js",
+ // "/assets/sw.js",
+ // ])
+ // })())
+})
+self.addEventListener("unload", () => {
+ console.log("unload")
+})
+
+const streams = new Map<string, { readable: ReadableStream, size: number }>()
+
+self.addEventListener("message", ev => {
+ console.log(ev);
+ const { path, size } = ev.data, port = ev.ports[0]
+ const readable = port_to_readable(port)
+ streams.set(path, { readable, size })
+})
+
+function port_to_readable(port: MessagePort): ReadableStream {
+ return new ReadableStream({
+ start(controller) {
+ console.log("ReadableStream started");
+ port.addEventListener("message", event => {
+ console.log(event.data);
+ if (event.data === "end") { controller.close() }
+ else if (event.data === "abort") controller.error("aborted")
+ else controller.enqueue(event.data)
+ })
+ },
+ cancel() { console.log("ReadableStream cancelled"); port.postMessage({ abort: true }) },
+ })
+}
+
+self.addEventListener("fetch", event => {
+ const { request } = event;
+ if (!request.url.startsWith(self.origin)) return
+ const path = request.url.substring(self.origin.length)
+ console.log(request.method, path);
+
+ const stream = streams.get(path)
+ if (stream) {
+ streams.delete(path)
+ console.log(`-> stream response`);
+ return event.respondWith(
+ new Response(
+ stream.readable,
+ {
+ headers: new Headers({
+ "Content-Type": "application/octet-stream; charset=utf-8", // TODO transmit and set accordingly
+ "Content-Security-Policy": "default-src 'none'",
+ "Content-Length": `${stream.size}`,
+ })
+ }
+ )
+ )
+ }
+
+ event.respondWith(fetch(request))
+})