/** * FileSource - Uses LRU cache for chunked file access with preloading * * For local File objects, we read the file in chunks and cache them * using an LRU cache. Chunks are preloaded sequentially to fill the cache. */ import type { SourceAdapter } from "./SourceAdapter"; import { LRUCache } from "../cache/LRUCache"; export declare class FileSource implements SourceAdapter { private file; private cache; private size; private position; private preloadPromise; private sourceKey; private currentTime; private duration; private preloadOffset; private preloadAbort; private fullFilePreload; private secondary; private totalBytesRead; private lastSpeedBytes; private lastSpeedTime; private currentReadSpeed; private readStartTime; private onRevokedCallback; private revokedFired; private preloadComplete; private onPreloadCompleteCallback; constructor(file: File, cache?: LRUCache | null); /** * Register a callback fired the first time a file read times out (likely * revocation by mobile browser). Lets the host surface a re-pick UI. */ setOnRevoked(cb: (info: { offset: number; length: number; reason: string; }) => void): void; /** * Disable whole-file preloading (bounded read-ahead only). Set by the host on * low-end mobile so the initial sequential disk read doesn't compete with a * strained 4K decode or fill RAM. Small files fall in the read-ahead window * anyway, so they still cache fully. */ setFullFilePreload(enabled: boolean): void; /** * Mark this as a SECOND reader over a file another source already owns — the * thumbnail pipeline builds one of these for file sources. * * Both readers share one LRU under one key (the key is name/size/mtime, so * it is the same string), which is what makes the arrangement work at all. * What did not work was leaving the second one behaving like an owner: * * - It ran its own whole-file preload sweep. Each pass checks the cache * before reading, but the two run CONCURRENTLY, so both miss on the same * not-yet-finished chunk and both read it — the whole file pulled twice. * Invisible on an SSD; on a Google Drive / OneDrive virtual file, where * every chunk is a network fetch through the filesystem shim, it doubles * the download and makes the two readers queue behind each other. * - Its close() cleared the cache. Not its own entries — the whole LRU, * including everything the main source had spent that download filling. * A thumbnail init that gets superseded (a quick second source change) * therefore threw away the file mid-playback and it was fetched again. * * Read-through still caches every chunk it touches, so previews stay warm. */ markSecondary(): void; /** * Register a one-shot callback fired when the initial preload pass settles. * If preload is already complete, fires synchronously. */ setOnPreloadComplete(cb: () => void): void; /** * True once the initial preload pass has settled (success, error, or abort). */ isPreloadComplete(): boolean; /** * Get the total size of the source in bytes */ getSize(): Promise; /** * Update preload position based on current playback time * @param currentTime Current playback time in seconds * @param duration Total duration in seconds */ updatePreloadPosition(currentTime: number, duration: number): void; /** * Read data from the source at the given offset * @param offset Byte offset to start reading from * @param length Number of bytes to read * @returns ArrayBuffer containing the requested data */ read(offset: number, length: number): Promise; /** * Seek to a position (for sources that need state) * @param offset The byte offset to seek to * @returns The actual offset seeked to */ seek(offset: number): number; /** * Get the current read position */ getPosition(): number; /** * Get disk read stats for nerd stats overlay */ getDiskStats(): { totalBytes: number; currentSpeed: number; elapsed: number; }; /** * Close the source and release resources */ close(): void; /** * Get a unique identifier for this source (used for caching) */ getKey(): string; /** * Start preloading chunks into cache * This method is idempotent - multiple calls will share the same preload promise */ private startPreload; /** * Preload chunks around current position (ahead for playback, behind for seeking) */ private preloadChunks; /** * Check if preloading should stop (cache full) */ /** * Hold the preload sweep while somebody is waiting on bytes of their own. * * The sweep walks the file from the front. A seek needs bytes from wherever * the playhead landed, which on a long file is far ahead of wherever the * sweep has got to — so the two ask the filesystem for different parts of * the same file at the same time, and the one nobody is waiting for gets * served alongside the one everybody is. Captured on a 94MB 4K AV1 file: a * seek to 110s needs the chunk at 58MB, and what actually arrived in the * next second and a half were the sweep's chunks at 37MB and 39MB. No frame * decoded, the seek hit its deadline, black-frame recovery seeked again. * * So the sweep stands down for a moment after every demand read. It is a * read-ahead: being late costs nothing, and being in the way costs a seek. */ private waitForDemandReads; private shouldStopPreload; /** * Read a chunk from file and cache it * * Mobile browsers (iOS Safari, Android Chrome) can revoke the underlying File * handle after long backgrounding or under memory pressure. The Blob then * silently hangs on read instead of rejecting, which stalls the demuxer * forever. Race against a timeout and surface a clear error so the UI can * prompt the user to re-pick the file. */ private readChunkFromFile; /** * Read from file (not cached) and optionally cache it */ /** * Read from file (not cached) and optionally cache it * Enforces strict CHUNK_SIZE alignment to prevent cache duplication */ private readFromFile; /** * Construct result from overlapping cached chunks */ private constructFromOverlapping; } /** * Factory function to create a FileSource */ export declare function createFileSource(file: File, cache?: LRUCache | null): Promise; //# sourceMappingURL=FileSource.d.ts.map