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.
70 lines
2.1 KiB
TypeScript
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.
|
|
}
|
|
}
|