diff --git a/CLAUDE.md b/CLAUDE.md index 1153135..b4afb2c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ JSON shapes mirror the Tauri Rust bridge exactly — don't change one side alone sends the `File` itself); cover images go to `PUT /api/admin/uploads/:id/art`. The multipart `POST /api/admin/uploads` parses the whole body in memory — scripts with small files only. - Videos over `transcode.maxSeconds` are never re-encoded (lane would be tied up for hours). -- Deferred ideas (Cloudflare Worker fetch, Android device download): `docs/deferred-ideas.md`. +- Deferred ideas (Cloudflare Worker fetch, Android device download, ffmpeg.wasm client editing): `docs/deferred-ideas.md`. ## Save to device + "On other devices" - `exportToDevice(video)` (app.js, helpers in `frontend/export.js`): puts the file in Photos / Files / Downloads. @@ -76,6 +76,15 @@ JSON shapes mirror the Tauri Rust bridge exactly — don't change one side alone `ignoreSearch` against the plain-URL precache. A new build = new URLs, so old ones simply get evicted. `/fonts` and `/icons` are cached 30 days. +## SHA-256 in WebAssembly (`frontend/sha256.js`) +- `Sha256.create()` runs its block function as WebAssembly (`frontend/wasm/sha256.c` → `scripts/build-sha256-wasm.sh` + → generated `frontend/sha256-wasm.js`, base64, ~2 KB) and falls back to pure JS if WebAssembly is missing, the CSP + refuses it, or the load-time self-test ("abc") fails. ~150 MB/s vs ~65 MB/s in JS (Node/Chromium; phones are slower + but the ratio holds). It is synchronous on purpose (callers aren't async) — fine because the module is under 4 KB. +- Needs `'wasm-unsafe-eval'` in index.html's CSP `script-src`. Workers pull the blob in with `importScripts('/sha256-wasm.js')` + from inside sha256.js; the file is in the SW `SHELL` list. After editing the C file, re-run the build script and commit both. + `Sha256.engine()` says which one is active; `create({js:true})` forces JS (tests). + ## Server media cache (`server/media-cache.js`) - Every played (`/api/streams`, LOW priority, ≤ `MEDIA_AUTO_MAX_SECONDS`, default 3 h) or saved (`/api/download`, HIGH, ≤ 3 h) video gets ONE copy: `$MEDIA_DIR/..mp4` diff --git a/docs/deferred-ideas.md b/docs/deferred-ideas.md index 08cdd8c..0b4d836 100644 --- a/docs/deferred-ideas.md +++ b/docs/deferred-ideas.md @@ -32,3 +32,13 @@ yt-dlp (or an equivalent extractor) on the phone's own IP, checks the server cache first, then uploads the finished file in the background through the device intake (`POST /api/p2p/intake`, `server/p2p-intake.js`). The server's yt-dlp stays as the fallback when the device fails. + +## Client-side video editing with ffmpeg.wasm (deferred 2026-10-02) + +Skipped on purpose. ffmpeg.wasm is about 25–30 MB, and fast (multi-threaded) +use needs cross-origin isolation (COOP/COEP). That would break the YouTube +thumbnails and avatars unless every image host sends CORP headers, and we do not +control `i.ytimg.com` / `ggpht.com`. The server already trims video (edit & download +runs ffmpeg there), so the browser gains nothing it needs. Revisit only if offline +editing becomes a requirement; then load it lazily from a worker and measure the +isolation fallout first. diff --git a/frontend/hash-worker.js b/frontend/hash-worker.js index 56a3e62..455dab6 100644 --- a/frontend/hash-worker.js +++ b/frontend/hash-worker.js @@ -3,7 +3,7 @@ * thread, read in 4 MiB slices (never the whole video in memory). * * In: { name } file name under OPFS videos/ (e.g. "abc.mp4") - * Out: { ok: true, sha256, size } | { ok: false, error } +^ * Out: { ok: true, sha256, size, engine } | { ok: false, error } * ========================================================================== */ 'use strict'; importScripts('/sha256.js'); @@ -19,7 +19,7 @@ self.onmessage = async (e) => { for (let pos = 0; pos < file.size; pos += STEP) { h.update(new Uint8Array(await file.slice(pos, pos + STEP).arrayBuffer())); } - self.postMessage({ ok: true, sha256: h.hex(), size: file.size }); + self.postMessage({ ok: true, sha256: h.hex(), size: file.size, engine: self.Sha256.engine() }); } catch (err) { self.postMessage({ ok: false, error: err && err.message ? err.message : String(err) }); } diff --git a/frontend/index.html b/frontend/index.html index 15705c1..811308c 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,7 +13,7 @@ YT Player @@ -622,6 +622,7 @@ + diff --git a/frontend/sha256-wasm.js b/frontend/sha256-wasm.js new file mode 100644 index 0000000..6fd19e6 --- /dev/null +++ b/frontend/sha256-wasm.js @@ -0,0 +1,7 @@ +/* GENERATED by scripts/build-sha256-wasm.sh from frontend/wasm/sha256.c — do not edit. + * WebAssembly SHA-256 block compression, embedded as base64 so it loads with the + * page / worker that already loads sha256.js (no extra request, no MIME or SW change). */ +(function (root) { + root.SHA256_WASM_B64 = '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'; + if (typeof module !== 'undefined' && module.exports) module.exports = root.SHA256_WASM_B64; +})(typeof globalThis !== 'undefined' ? globalThis : this); diff --git a/frontend/sha256.js b/frontend/sha256.js index 69b88f7..931d248 100644 --- a/frontend/sha256.js +++ b/frontend/sha256.js @@ -1,6 +1,10 @@ /* ============================================================================ * sha256.js — incremental SHA-256 (pure JS; window.Sha256 / worker / node) * + * The block function runs as WebAssembly (wasm/sha256.c → sha256-wasm.js) when the + * browser allows it, with the pure-JS version below as the fallback — a failed + * load or a failed self-test silently keeps hashing in JS. + * * WebCrypto's digest() needs the whole input at once, which would pull a * multi-hundred-MB video into memory. This hasher takes chunks as they stream * past (downloads, peer transfers, OPFS reads) and keeps ~100 bytes of state. @@ -22,7 +26,41 @@ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, ]); - function create() { + // ---- WebAssembly engine (optional) ------------------------------------- + // Memory layout shared with wasm/sha256.c: state at STATE, input blocks at DATA. + const STATE = 16384, DATA = 65536, CHUNK = 1 << 20; // 1 MiB staging window + const IV = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]; + let wasm; // undefined = not tried yet, null = unavailable, else { compress, state, data } + function loadWasm() { + if (wasm !== undefined) return wasm; + wasm = null; + try { + if (typeof WebAssembly === 'undefined') return wasm; + if (!root.SHA256_WASM_B64 && typeof importScripts === 'function') importScripts('/sha256-wasm.js'); + let b64 = root.SHA256_WASM_B64; + if (!b64 && typeof require === 'function') b64 = require('./sha256-wasm.js'); + if (!b64) return wasm; + const bin = typeof Buffer !== 'undefined' ? Uint8Array.from(Buffer.from(b64, 'base64')) + : Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); + const memory = new WebAssembly.Memory({ initial: (DATA + CHUNK) / 65536, maximum: (DATA + CHUNK) / 65536 }); + const inst = new WebAssembly.Instance(new WebAssembly.Module(bin), { env: { memory } }); + const e = { + compress: inst.exports.compress, + state: new Uint32Array(memory.buffer, STATE, 8), + data: new Uint8Array(memory.buffer, DATA, CHUNK), + }; + // Self-test: one padded block of "abc" must give the well-known digest. + e.state.set(IV); + e.data.fill(0, 0, 64); e.data.set([0x61, 0x62, 0x63, 0x80], 0); e.data[63] = 24; + e.compress(STATE, DATA, 1); + if (e.state[0] !== 0xba7816bf || e.state[7] !== 0xf20015ad) return wasm; + wasm = e; + } catch { wasm = null; } + return wasm; + } + + function create(opts) { + const useWasm = !(opts && opts.js) && loadWasm(); const H = new Uint32Array([ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, ]); @@ -58,6 +96,21 @@ H[4] = (H[4] + e) | 0; H[5] = (H[5] + f) | 0; H[6] = (H[6] + g) | 0; H[7] = (H[7] + h) | 0; } + // Hash n whole 64-byte blocks of buf starting at off — wasm in 1 MiB windows, else JS. + function blocks(buf, off, n) { + if (!useWasm) { for (let k = 0; k < n; k++) compress(buf, off + k * 64); return; } + const w = useWasm; + w.state.set(H); + const per = CHUNK / 64; + for (let done = 0; done < n;) { + const m = Math.min(per, n - done); + w.data.set(buf.subarray(off + done * 64, off + (done + m) * 64)); + w.compress(STATE, DATA, m); + done += m; + } + H.set(w.state); + } + function update(data) { if (done) throw new Error('sha256: update() after digest'); const u8 = data instanceof Uint8Array ? data : new Uint8Array(data); @@ -68,9 +121,10 @@ block.set(u8.subarray(0, take), blockLen); blockLen += take; i = take; - if (blockLen === 64) { compress(block, 0); blockLen = 0; } + if (blockLen === 64) { blocks(block, 0, 1); blockLen = 0; } } - for (; i + 64 <= u8.length; i += 64) compress(u8, i); + const whole = Math.floor((u8.length - i) / 64); + if (whole) { blocks(u8, i, whole); i += whole * 64; } if (i < u8.length) { block.set(u8.subarray(i), 0); blockLen = u8.length - i; } return api; } @@ -80,12 +134,12 @@ done = true; const bits = total * 8; block[blockLen++] = 0x80; - if (blockLen > 56) { block.fill(0, blockLen); compress(block, 0); blockLen = 0; } + if (blockLen > 56) { block.fill(0, blockLen); blocks(block, 0, 1); blockLen = 0; } block.fill(0, blockLen, 56); const hi = Math.floor(bits / 0x100000000), lo = bits >>> 0; block[56] = hi >>> 24; block[57] = hi >>> 16; block[58] = hi >>> 8; block[59] = hi; block[60] = lo >>> 24; block[61] = lo >>> 16; block[62] = lo >>> 8; block[63] = lo; - compress(block, 0); + blocks(block, 0, 1); } const out = new Uint8Array(32); for (let i = 0; i < 8; i++) { @@ -107,6 +161,8 @@ const Sha256 = { create, hex: (data) => create().update(data).hex(), + // Which engine create() will use: 'wasm' or 'js'. + engine: () => (loadWasm() ? 'wasm' : 'js'), isHex: (s) => typeof s === 'string' && /^[0-9a-f]{64}$/.test(s), }; if (typeof module !== 'undefined' && module.exports) module.exports = Sha256; diff --git a/frontend/sha256.test.js b/frontend/sha256.test.js index ac6ffb3..4d1aee9 100644 --- a/frontend/sha256.test.js +++ b/frontend/sha256.test.js @@ -43,3 +43,32 @@ test('update after digest throws; isHex', () => { assert.ok(Sha256.isHex(ref(Buffer.from('x')))); assert.ok(!Sha256.isHex('ABC')); }); + +// ---- WebAssembly engine ---- +test('wasm engine is available in Node and matches the JS engine and node:crypto', () => { + const crypto = require('node:crypto'); + assert.equal(Sha256.engine(), 'wasm'); + const sizes = [0, 1, 55, 56, 63, 64, 65, 1000, (1 << 20) - 1, 1 << 20, (1 << 20) + 1, 3_000_001]; + for (const n of sizes) { + const buf = Buffer.alloc(n).map((_, i) => (i * 7 + n) & 255); + const want = crypto.createHash('sha256').update(buf).digest('hex'); + assert.equal(Sha256.create().update(buf).hex(), want, `wasm ${n}`); + assert.equal(Sha256.create({ js: true }).update(buf).hex(), want, `js ${n}`); + } +}); + +test('wasm engine hashes streamed odd-sized chunks like one-shot', () => { + const crypto = require('node:crypto'); + const buf = Buffer.alloc(2_500_003).map((_, i) => (i * 13) & 255); + const h = Sha256.create(); + for (let o = 0; o < buf.length; o += 77_777) h.update(buf.subarray(o, o + 77_777)); + assert.equal(h.hex(), crypto.createHash('sha256').update(buf).digest('hex')); +}); + +test('falls back to the JS engine when WebAssembly is unavailable', () => { + const { execFileSync } = require('node:child_process'); + const out = execFileSync(process.execPath, ['-e', + "delete globalThis.WebAssembly; const S=require('./frontend/sha256.js');" + + "console.log(S.engine(), S.hex(new TextEncoder().encode('abc')))"], { cwd: require('node:path').join(__dirname, '..') }).toString().trim(); + assert.equal(out, 'js ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'); +}); diff --git a/frontend/sw.js b/frontend/sw.js index 1c8dd95..0ef5e8f 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -70,6 +70,7 @@ const SHELL = [ '/lyrics-core.js', '/stats-core.js', '/sha256.js', + '/sha256-wasm.js', '/device-db.js', '/hash-worker.js', '/p2p-client.js', diff --git a/frontend/wasm/sha256.c b/frontend/wasm/sha256.c new file mode 100644 index 0000000..238cf87 --- /dev/null +++ b/frontend/wasm/sha256.c @@ -0,0 +1,46 @@ +// SHA-256 block compression for frontend/sha256.js (built to WebAssembly). +// No libc, no allocation: JS owns the memory layout. +// state (8 x u32) at STATE +// input (N x 64 B) at DATA +// Build: scripts/build-sha256-wasm.sh (writes frontend/sha256-wasm.js) +typedef unsigned int u32; +typedef unsigned char u8; + +static const u32 K[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 }; + +#define ROR(x,n) (((x) >> (n)) | ((x) << (32 - (n)))) + +__attribute__((export_name("compress"))) +void compress(u32 *state, const u8 *data, u32 blocks) { + u32 W[64]; + while (blocks--) { + for (int i = 0; i < 16; i++, data += 4) + W[i] = ((u32)data[0] << 24) | ((u32)data[1] << 16) | ((u32)data[2] << 8) | data[3]; + for (int i = 16; i < 64; i++) { + u32 s0 = ROR(W[i-15],7) ^ ROR(W[i-15],18) ^ (W[i-15] >> 3); + u32 s1 = ROR(W[i-2],17) ^ ROR(W[i-2],19) ^ (W[i-2] >> 10); + W[i] = W[i-16] + s0 + W[i-7] + s1; + } + u32 a=state[0],b=state[1],c=state[2],d=state[3],e=state[4],f=state[5],g=state[6],h=state[7]; + // 8 rounds per iteration with the variables renamed instead of shuffled. +#define RND(a,b,c,d,e,f,g,h,i) do { \ + u32 t1 = h + (ROR(e,6) ^ ROR(e,11) ^ ROR(e,25)) + (g ^ (e & (f ^ g))) + K[i] + W[i]; \ + u32 t2 = (ROR(a,2) ^ ROR(a,13) ^ ROR(a,22)) + ((a & b) | (c & (a | b))); \ + d += t1; h = t1 + t2; } while (0) + for (int i = 0; i < 64; i += 8) { + RND(a,b,c,d,e,f,g,h,i); RND(h,a,b,c,d,e,f,g,i+1); + RND(g,h,a,b,c,d,e,f,i+2); RND(f,g,h,a,b,c,d,e,i+3); + RND(e,f,g,h,a,b,c,d,i+4); RND(d,e,f,g,h,a,b,c,i+5); + RND(c,d,e,f,g,h,a,b,i+6); RND(b,c,d,e,f,g,h,a,i+7); + } + state[0]+=a; state[1]+=b; state[2]+=c; state[3]+=d; state[4]+=e; state[5]+=f; state[6]+=g; state[7]+=h; + } +} diff --git a/scripts/build-sha256-wasm.sh b/scripts/build-sha256-wasm.sh new file mode 100755 index 0000000..6886d26 --- /dev/null +++ b/scripts/build-sha256-wasm.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Rebuild frontend/sha256-wasm.js (the base64-embedded WebAssembly SHA-256 block +# function) from frontend/wasm/sha256.c. Needs clang with wasm32 + wasm-ld. +set -euo pipefail +cd "$(dirname "$0")/.." +tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT +clang --target=wasm32 -O3 -nostdlib -fno-builtin -ffreestanding \ + -Wl,--no-entry -Wl,--export=compress -Wl,--import-memory -Wl,--initial-memory=65536 -Wl,--max-memory=1114112 \ + -Wl,-z,stack-size=4096 -Wl,--strip-all \ + frontend/wasm/sha256.c -o "$tmp/sha256.wasm" +b64=$(base64 -w0 "$tmp/sha256.wasm") +cat > frontend/sha256-wasm.js <