/** * HttpSource - SharedArrayBuffer Streaming with Atomics * * Uses SharedArrayBuffer for zero-copy data sharing. * Atomics for thread-safe concurrent access. */ import type { SourceAdapter } from "./SourceAdapter"; export declare class HttpSource implements SourceAdapter { private url; private headers; private size; private position; private _contentDispositionFilename; private headBuffer; /** * Opening bytes handed over by whoever fetched them first, keyed by URL. * * The pre-play probe used to download ~3MB purely to time the link and throw * every byte away — then this source downloaded the same opening bytes again. * Now the probe reads the head of the rung it is measuring and leaves it * here; `read()` already checks headBuffer before touching the network, so * the demuxer's first reads are served without a request. * * One entry, replaced on each offer: only the rung about to open matters. */ private static warmHead; /** Hand over opening bytes for a URL that is about to be opened. */ static offerWarmHead(url: string, bytes: Uint8Array): void; /** * Size of the FIRST range request, overridable per source. * * 4MB is right for video — it covers the moov and the opening GOP. For a * separate audio stream it is four minutes of AAC nobody needs yet, and it * is fetched during startup where it costs seconds directly. The split-audio * path asks for a smaller opening; the streaming loop after it is unchanged. */ private firstRangeBytes; setFirstRangeBytes(bytes: number): void; private metadataCache; private metadataCacheBytes; private sharedBuffer; private headerView; private dataView; private useSharedBuffer; private fallbackBuffer; private fallbackStart; private fallbackWritePos; private fallbackStreaming; private bufferWaiters; private reader; private abortController; /** * Aborted once on close(), killing every request this source has in flight. * * `abortController` above only covers the sequential stream (it's recycled on * every stream restart), so the ranged reads went out with no signal at all — * a player torn down mid-read (a quality switch, a source error recreate) left * multi-megabyte range requests downloading to nobody, competing for the link * with the replacement that just started. */ private lifetimeAbort; private streamError; /** * A failure this URL will not recover from, remembered for the life of the * source rather than for the life of one stream attempt. * * `streamError` is cleared by startStream(), which is correct for it — each * attempt starts clean. But read() answers a window it cannot cover by * calling startStream(), so a URL that had begun refusing produced: fetch, * fail, set streamError, stop; read; startStream; CLEAR the error; fetch; * fail… Measured against a signed URL expiring mid-playback, that ran 1131 * requests in five seconds, the state stayed "playing", and no error ever * survived long enough for a reader to see one. The viewer got a picture * that had quietly stopped and no reason for it. */ private fatalError; private maxBufferedEnd; private fullyBuffered; private rangeUnsupported; private linearMode; private onLinearMode; private readMax; private recentReads; private consecutiveForceRestarts; private lastForceRestartTime; private readonly MAX_FORCE_RESTARTS; private consecutiveOneOffFetches; private readonly MAX_ONEOFF_BEFORE_RESTART; private seekHinted; private bufferSize; private totalBytesDownloaded; private streamStartTime; private lastSpeedBytes; private lastSpeedTime; private currentSpeed; private lastSpeed; private maxBufferSizeMB; constructor(url: string, headers?: Record, maxBufferSizeMB?: number); /** * Initialize buffer (SharedArrayBuffer if available, fallback otherwise) * Starts with minimum size (2MB), will be resized to 3% of file size when known */ private initBuffer; /** * Override the maximum buffer size cap at runtime. Takes effect on the * next resizeBuffer() call (typically the post-resolveSize pass) — and * immediately re-runs the buffer-sizing logic if a size is already * known, so UI attribute changes reflect without needing a reload. * Pass a number of megabytes. 0 or negative values are ignored. */ setMaxBufferSize(megabytes: number): void; /** * Register a callback fired once when the source falls back to linear * (forward-only, non-seekable) playback because the server has no Range * support and the file is too large to cache whole. The UI uses this to * hide the timeline and disable seeking/thumbnails. */ setOnLinearMode(cb: () => void): void; /** True once the source is in forward-only linear (non-seekable) playback. */ isLinearMode(): boolean; /** * True once we've confirmed the server has no Range support (covers both the * full-cache and the linear fallback). Callers use it to skip features that * need scattered random-access reads (e.g. the thumbnail pipeline). */ isRangeUnsupported(): boolean; /** * Resize buffer based on file size (3% of file, clamped to min/max) */ private resizeBuffer; /** * Atomic operations for SharedArrayBuffer */ private atomicGetWritePos; private atomicSetWritePos; /** * Wake readers parked in waitForData() whose bytes are now buffered. * `force` wakes every waiter regardless of its byte target — used when the * stream stops, so waiters re-evaluate the loop condition and exit rather * than waiting on bytes that are never coming. */ private wakeBufferWaiters; /** * Resolve as soon as bufferEnd reaches `needed`, or after maxWaitMs. * The timer is a safety net for the checks waitForData runs each pass * (deadline, stall, superseded) — not the mechanism for spotting new bytes. */ private waitForBufferAdvance; private atomicGetBufferStart; private atomicSetBufferStart; private _prefetchThrottled; setPrefetchThrottle(throttled: boolean): void; /** * Tell the source a seek is about to happen, so the next read that falls * outside the stream window is treated as the seek landing and restarts the * stream there — instead of being served as a one-off range fetch while the * old stream keeps downloading (and saturating the link with) the region the * user just left. See `seekHinted`. Harmless if the seek lands in-window: * the next read simply clears the hint. */ hintSeek(): void; private awaitPrefetchGate; private atomicIsStreaming; private atomicSetStreaming; private atomicIncrementVersion; /** * Try to acquire lock (non-blocking) */ private tryLock; private unlock; /** * Resolve the total file size. Default implementation issues a HEAD * request and parses Content-Length + Content-Disposition. Override to * source the size from elsewhere (auth token response, database, etc.); * throw on failure. */ protected resolveSize(): Promise; /** * Recover the total file size from a 1-byte ranged GET when HEAD didn't * carry Content-Length. Reads the total out of Content-Range; returns null * if the server gives us nothing usable. The body is cancelled immediately — * we only want headers, and a server that ignores Range would otherwise * start streaming the whole file. */ protected resolveSizeViaRange(): Promise; /** * Last-resort size probe: a plain (un-ranged) GET. Catches servers that strip * Content-Length on HEAD and don't CORS-expose Content-Range on a 206, yet * still send Content-Length on a full 200 response. The body is cancelled the * moment headers are in, so nothing past the headers is downloaded. */ protected resolveSizeViaPlainGet(): Promise; /** Pull a download filename out of a Content-Disposition header, if present. */ private setFilenameFromDisposition; /** * Build the HTTP headers used for every outbound request (HEAD, range * GET, stream GET). Default implementation just returns the static * headers provided to the constructor. Override to inject per-request * auth/signing headers (token, HMAC signature, nonce, timestamp, ...); * note this is called many times across a playback session, so any * expensive work should be cached/memoised by the subclass. * * @param range Optional byte range the caller will request. Subclasses * that sign the range (e.g. HMAC over `offset/length`) need * this; pass-through callers can ignore it. */ protected buildRequestHeaders(range?: { offset: number; length: number; openEnded?: boolean; }): Promise>; getSize(): Promise; getContentDispositionFilename(): string | null; private get bufferEnd(); private isInBuffer; private getBuffer; /** * Read-only peek into the persistent head cache. * Returns a copy if the full range is covered, null otherwise. * Does not mutate position/state, safe for cross-source borrowing. */ peekHead(offset: number, length: number): Uint8Array | null; /** * Record a freshly-served small read into the metadata LRU. * Ignores reads larger than METADATA_CACHE_MAX_CHUNK (those are payload, * not metadata). The caller must pass bytes that will not be mutated — * we keep the reference as-is. read() already hands us fresh Uint8Arrays. */ private cacheMetadataRead; /** * Read-only peek into the metadata LRU. * Returns a fresh copy if any cached chunk fully covers [offset, offset+length). * Bumps matched entry to most-recent. Safe for cross-source borrowing. */ peekMetadata(offset: number, length: number): Uint8Array | null; /** * Read-only peek into the sliding window buffer. * Returns a copy if the full range is present, null otherwise. * Seqlock-guarded via HEADER.VERSION — if the window shifts mid-copy * (new stream started), returns null so the caller can fall back. * Does not mutate position/state, safe for cross-source borrowing. */ peekRange(offset: number, length: number): Uint8Array | null; /** * Start streaming from offset */ private startStream; private readStreamBackground; /** * Consume a single 200 response as the whole file from byte 0, used when the * server has no Range support. Two outcomes: * - File fits the buffer cap → cache it entirely → full random access * (seek/thumbnails keep working once downloaded). * - File exceeds the cap (or size unknown) → bounded forward-only sliding * window (linearMode): playback is linear, seeking impossible. The UI is * notified via onLinearMode so it can hide the timeline. * The response body MUST start at byte 0 (caller guarantees startOffset===0). */ private consumeNonRangeStream; /** * Append a chunk to the buffer for the non-range stream. In full-cache mode * the buffer always has room. In linearMode it slides the window forward by * discarding already-consumed bytes, applying backpressure (waiting for the * demuxer to catch up) when the window can't be advanced yet. */ private writeSequential; /** * Slide the linear-mode window forward to make room for new download. Keeps a * trailing history of up to LINEAR_TRAIL_BYTES behind the playhead so the user * can seek backward into already-played content that's still in RAM — only the * bytes older than that are dropped. Returns false when nothing can be dropped * yet (playhead hasn't advanced past the trailing window), so the caller * applies backpressure. This bounds read-ahead to (bufferSize - trail). */ private slideWindow; private stopStream; private waitForData; read(offset: number, length: number): Promise; private _readInternal; private readFromBuffer; seek(offset: number): number; getPosition(): number; /** * Get the shared buffer for zero-copy access from workers */ getSharedBuffer(): SharedArrayBuffer | null; close(): void; /** True once close() has run. Reads stop trying to recover after that: their * data is not coming, and restarting a stream on a torn-down source only * produces failures that look like the link breaking. */ private closed; getKey(): string; getUrl(): string; /** * Returns true when the entire file has been downloaded and is cached in the buffer. * In this state, all seek/replay operations are served from memory (zero network). */ isFullyCached(): boolean; /** * The file size if it is already known, -1 before it has been resolved. * * getSize() is the way to ASK for it (it will go and fetch it); this is for * callers on a synchronous path — a UI tick reading the buffered range — that * want the answer only if it is already in hand. */ getKnownSize(): number; /** * Get the current buffered end position in bytes * This represents the furthest byte that has been buffered * Uses the maximum of current buffer window and historical max position, * but caps it to not exceed what's actually available */ /** * The first byte the streaming window currently holds. * * The window slides, so "the end is EOF" does NOT mean the file is * downloaded — a container whose index lives at the tail (Matroska cues, a * trailing MP4 moov) sends the window there during open, and for that moment * the window ends at the last byte of a 3.6GB file it has read 4MB of. * Callers that want "everything from here to the end is in hand" have to ask * where the window STARTS as well. */ /** * The failure this source will not come back from, if there has been one. * * Asked at EOF. The demuxer reads through a C callback that can only answer * with a byte count, so a read that FAILED and a read that reached the end of * the file arrive as the same thing: no bytes. FFmpeg calls that EOF, the * packet loop ends the video, and a source that had been refused mid-file * looked exactly like a file that had finished — the picture stopped, the * clock jumped to the duration, and anything watching for "ended" (an * autoplay-next, a playlist) moved on as though nothing had gone wrong. */ getFatalError(): Error | null; getBufferedStart(): number; getBufferedEnd(): number; /** * Get the current buffer start position in bytes */ getBufferStart(): number; /** * Get network stats for nerd stats overlay */ getNetworkStats(): { totalBytes: number; currentSpeed: number; lastSpeed: number; elapsed: number; }; } export declare function createHttpSource(url: string, headers?: Record, maxBufferSizeMB?: number): Promise; //# sourceMappingURL=HttpSource.d.ts.map