diff options
Diffstat (limited to 'client-web/source')
-rw-r--r-- | client-web/source/index.ts | 4 | ||||
-rw-r--r-- | client-web/source/keybinds.ts | 2 | ||||
-rw-r--r-- | client-web/source/logger.ts | 1 | ||||
-rw-r--r-- | client-web/source/resource/file.ts | 30 | ||||
-rw-r--r-- | client-web/source/sw/download_stream.ts | 57 | ||||
-rw-r--r-- | client-web/source/sw/init.ts | 35 | ||||
-rw-r--r-- | client-web/source/sw/worker.ts | 86 |
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)) +}) |