fix: stop playback stutter and save failures via async yt-dlp, per-download OPFS workers, lazy playback blobs, GPU composite trims, and landscape pane scrolling
This commit is contained in:
110
frontend/opfs-worker.js
Normal file
110
frontend/opfs-worker.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/* ============================================================================
|
||||
* opfs-worker.js — off-main-thread video download → OPFS
|
||||
*
|
||||
* One dedicated Worker per download (spawned by OPFS.downloadVideo in
|
||||
* opfs.js, terminated when finished). The worker does the whole job itself —
|
||||
* fetch from /api/download plus streaming writes via createSyncAccessHandle —
|
||||
* so a multi-hundred-MB save never allocates buffers or runs stream pumps on
|
||||
* the main thread. createSyncAccessHandle is worker-only but has wider
|
||||
* support than createWritable (Safari 15.2+ vs 18.2+), which also removes
|
||||
* the whole-file ArrayBuffer fallback the main-thread path needs on WebKit.
|
||||
*
|
||||
* In message: { videoId, url }
|
||||
* Out messages:
|
||||
* { type: 'unsupported' } → caller falls back to main thread
|
||||
* { type: 'progress', received } → bytes written so far
|
||||
* { type: 'done', ext } → file stored as <videoId>.<ext>
|
||||
* { type: 'error', error } → failed; .part cleaned up
|
||||
* ========================================================================== */
|
||||
|
||||
'use strict';
|
||||
|
||||
async function getVideosDir() {
|
||||
const root = await navigator.storage.getDirectory();
|
||||
return root.getDirectoryHandle('videos', { create: true });
|
||||
}
|
||||
|
||||
function extFromContentType(ct) {
|
||||
ct = ct || 'video/mp4';
|
||||
return ct.includes('webm') ? 'webm' : ct.includes('ogg') ? 'ogg' : 'mp4';
|
||||
}
|
||||
|
||||
self.onmessage = async (e) => {
|
||||
const { videoId, url } = e.data || {};
|
||||
|
||||
if (
|
||||
typeof navigator === 'undefined' ||
|
||||
!navigator.storage ||
|
||||
typeof navigator.storage.getDirectory !== 'function' ||
|
||||
typeof FileSystemFileHandle === 'undefined' ||
|
||||
typeof FileSystemFileHandle.prototype.createSyncAccessHandle !== 'function'
|
||||
) {
|
||||
self.postMessage({ type: 'unsupported' });
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = null;
|
||||
let partName = null;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
let msg = 'HTTP ' + res.status;
|
||||
try { msg = (await res.json()).error || msg; } catch { /* non-JSON */ }
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
const ext = extFromContentType(res.headers.get('content-type'));
|
||||
const filename = videoId + '.' + ext;
|
||||
partName = filename + '.part';
|
||||
|
||||
dir = await getVideosDir();
|
||||
const partHandle = await dir.getFileHandle(partName, { create: true });
|
||||
const access = await partHandle.createSyncAccessHandle();
|
||||
let offset = 0;
|
||||
try {
|
||||
const reader = res.body.getReader();
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
access.write(value, { at: offset });
|
||||
offset += value.byteLength;
|
||||
self.postMessage({ type: 'progress', received: offset });
|
||||
}
|
||||
access.truncate(offset);
|
||||
access.flush();
|
||||
} finally {
|
||||
access.close();
|
||||
}
|
||||
|
||||
// Finalize: .part → permanent name. Prefer the native rename; fall back
|
||||
// to a chunked copy (still fully inside the worker, small fixed buffers).
|
||||
try { await dir.removeEntry(filename); } catch { /* no previous copy */ }
|
||||
if (typeof partHandle.move === 'function') {
|
||||
await partHandle.move(filename);
|
||||
} else {
|
||||
const finalHandle = await dir.getFileHandle(filename, { create: true });
|
||||
const out = await finalHandle.createSyncAccessHandle();
|
||||
try {
|
||||
const file = await partHandle.getFile();
|
||||
const CHUNK = 8 * 1024 * 1024;
|
||||
let pos = 0;
|
||||
while (pos < file.size) {
|
||||
const buf = await file.slice(pos, pos + CHUNK).arrayBuffer();
|
||||
out.write(new Uint8Array(buf), { at: pos });
|
||||
pos += buf.byteLength;
|
||||
}
|
||||
out.truncate(file.size);
|
||||
out.flush();
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
await dir.removeEntry(partName);
|
||||
}
|
||||
|
||||
self.postMessage({ type: 'done', ext });
|
||||
} catch (err) {
|
||||
// Never leave a corrupt partial behind
|
||||
try { if (dir && partName) await dir.removeEntry(partName); } catch { /* gone */ }
|
||||
self.postMessage({ type: 'error', error: err && err.message ? err.message : String(err) });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user