Files
J621/frontend/src/features/optimize/opfs.ts
T
JakeBreath bd812bb601 Write video output to a real OPFS file instead of assembling chunks
The hand-rolled chunk assembly was verified correct in Node but still
produced a broken MP4 in the browser, so stop relying on it: the muxer now
streams into an Origin Private File System file (random access is exactly
what MP4 needs), the worker returns the OPFS File directly, and the entry
is deleted after a successful apply (stale ones are purged hourly). The
chunk collector remains only as a fallback for browsers without OPFS.

The result is validated before it reaches the UI: MP4s must contain a
moov box and WebM files must start with the EBML magic, so a broken muxer
output surfaces as an error instead of a 0x0 preview.
2026-09-17 20:58:26 -05:00

70 lines
2.1 KiB
TypeScript

/**
* Small helpers around the Origin Private File System, used to stream muxer
* output into a real file with random access instead of hand-assembling
* chunks in memory. Browser-only; every function is a no-op when OPFS is not
* available.
*/
const PREFIX = "j621-optimize-";
const STALE_MS = 60 * 60 * 1000; // purge leftovers after an hour
function directory(): Promise<FileSystemDirectoryHandle> | null {
if (typeof navigator === "undefined") return null;
const storage = navigator.storage as StorageManager | undefined;
if (!storage?.getDirectory) return null;
return storage.getDirectory();
}
export function opfsSupported(): boolean {
return (
typeof navigator !== "undefined" &&
Boolean((navigator.storage as StorageManager | undefined)?.getDirectory)
);
}
export async function createStoredFile(
extension: string,
): Promise<{ name: string; handle: FileSystemFileHandle } | null> {
const root = await directory();
if (!root) return null;
const name = `${PREFIX}${crypto.randomUUID()}.${extension}`;
const handle = await root.getFileHandle(name, { create: true });
return { name, handle };
}
export async function deleteStoredFile(name: string): Promise<void> {
try {
const root = await directory();
if (!root) return;
await root.removeEntry(name);
} catch {
// Best effort: stale entries are purged on the next run anyway.
}
}
export async function purgeStaleStoredFiles(): Promise<void> {
try {
const root = await directory();
if (!root) return;
const now = Date.now();
const entries = (
root as unknown as {
entries(): AsyncIterableIterator<[string, FileSystemHandle]>;
}
).entries();
for await (const [name, handle] of entries) {
if (!name.startsWith(PREFIX) || handle.kind !== "file") continue;
try {
const file = await (handle as FileSystemFileHandle).getFile();
if (now - file.lastModified > STALE_MS) {
await root.removeEntry(name);
}
} catch {
// Ignore entries that vanish underneath us.
}
}
} catch {
// OPFS iteration is best effort.
}
}