Extend JamesDSP with convolution and spatial audio

This commit is contained in:
Jonathan Sykes
2026-10-10 19:25:06 +08:00
parent b3f0525a8e
commit ef12468a99
28 changed files with 540 additions and 143 deletions

View File

@@ -3,12 +3,22 @@
'use strict';
const CACHE = 'ytplayer-assets';
const STATE = '/__ytp_asset_state';
const OPT_INS = '/__ytp_offline_opt_ins';
const url = (path, file) => path + '?v=' + file.h;
function paths(manifest) {
return Object.keys(manifest.files).sort();
}
function offlinePaths(manifest, optIns = []) {
const allowed = new Set(optIns || []), optional = new Map();
for (const group of Object.values(manifest.groups || {})) {
if (!group.offlineOptIn || allowed.has(group.offlineOptIn)) continue;
for (const path of group.files || []) optional.set(path, true);
}
return paths(manifest).filter(path => !optional.has(path));
}
function blocking(manifest, previous, activeLayout) {
const names = new Set(Object.entries(manifest.groups)
.filter(([name, group]) => name === 'core' || !group.background ||
@@ -61,13 +71,17 @@
return valid;
}
async function status(manifest, cache, { previous, activeLayout, offline = true } = {}) {
async function readOptIns(cache) {
try { return (await (await cache.match(OPT_INS))?.json()) || []; } catch { return []; }
}
async function status(manifest, cache, { previous, activeLayout, offline = true, optIns } = {}) {
const found = await Promise.all(blocking(manifest, previous, activeLayout).map(async path => {
const response = await cache.match(url(path, manifest.files[path]));
return verifiedProbe(response, manifest.files[path].h);
}));
const missing = found.filter(value => !value).length;
return { ready: missing === 0, missing, version: manifest.buildTag, ...(offline ? await completeness(manifest, cache) : {}) };
return { ready: missing === 0, missing, version: manifest.buildTag, ...(offline ? await completeness(manifest, cache, { optIns }) : {}) };
}
// All concurrent messages/layout jobs in this worker share six download slots.
@@ -89,10 +103,10 @@
finally { pendingDownloads.delete(key); }
}
async function syncAssets(manifest, { cache, fetchFn, concurrency = 6, attempts = 3, activeLayout, groups, all = false }) {
async function syncAssets(manifest, { cache, fetchFn, concurrency = 6, attempts = 3, activeLayout, groups, all = false, optIns } = {}) {
const previous = (await state(cache))?.current;
const missing = [];
const selected = all ? paths(manifest) : groups ? [...new Set(groups.flatMap(name => manifest.groups[name]?.files || []))].sort()
const selected = all ? offlinePaths(manifest, optIns || await readOptIns(cache)) : groups ? [...new Set(groups.flatMap(name => manifest.groups[name]?.files || []))].sort()
: blocking(manifest, previous, activeLayout);
for (const path of selected) {
const key = url(path, manifest.files[path]);
@@ -135,8 +149,8 @@
// Verified cache entries are the durable progress journal. Counters are rebuilt
// after worker termination or browser eviction; never trust a stored ready bit.
async function completeness(manifest, cache) {
const files = paths(manifest);
async function completeness(manifest, cache, { optIns } = {}) {
const files = offlinePaths(manifest, optIns || await readOptIns(cache));
const missingFiles = (await Promise.all(files.map(async path => {
const response = await cache.match(url(path, manifest.files[path]));
return verifiedProbe(response, manifest.files[path].h) ? null : path;
@@ -146,12 +160,13 @@
}
const completionKey = manifest => '/__ytp_completion/' + manifest.buildTag;
async function complete(manifest, { cache, fetchFn, cycles = 4, now = Date.now,
async function complete(manifest, { cache, fetchFn, cycles = 4, now = Date.now, optIns,
sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), baseDelay = 1000,
maxDelay = 30000, notify = () => {}, force = false } = {}) {
const key = completionKey(manifest), saved = await cache.match(key);
let job = saved ? await saved.json() : { failures: 0, nextRetryAt: 0 };
let snapshot = await completeness(manifest, cache);
const selectedOptIns = optIns || await readOptIns(cache);
let snapshot = await completeness(manifest, cache, { optIns: selectedOptIns });
const publish = async running => {
job = { ...job, ...snapshot, running, version: manifest.buildTag };
await cache.put(key, Response.json(job));
@@ -162,9 +177,9 @@
for (let cycle = 0; cycle < cycles; cycle++) {
await publish(true);
let error;
try { await syncAssets(manifest, { cache, fetchFn, all: true }); }
try { await syncAssets(manifest, { cache, fetchFn, all: true, optIns: selectedOptIns }); }
catch (failure) { error = failure; }
snapshot = await completeness(manifest, cache);
snapshot = await completeness(manifest, cache, { optIns: selectedOptIns });
if (snapshot.offlineReady) { job.failures = 0; job.nextRetryAt = 0; job.error = null; await publish(false); return job; }
job.failures++;
const delay = Math.min(maxDelay, baseDelay * 2 ** Math.min(job.failures - 1, 10));
@@ -198,7 +213,7 @@
return next;
}
const api = { CACHE, STATE, url, paths, blocking, plan, retained, fallback, status, completeness, completionKey, complete, syncAssets, state, commit };
const api = { CACHE, STATE, OPT_INS, url, paths, offlinePaths, blocking, plan, retained, fallback, status, completeness, completionKey, complete, syncAssets, readOptIns, state, commit };
if (typeof module !== 'undefined' && module.exports) module.exports = api;
else root.AssetSyncCore = api;
})(typeof globalThis !== 'undefined' ? globalThis : this);