UNPKG

mediabunny

Version:

Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.

154 lines (153 loc) 6.05 kB
/*! * Copyright (c) 2025-present, Vanilagy and contributors * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { assert, binarySearchLessOrEqual, removeItem } from './misc.js'; export class Reader { constructor(source, maxStorableBytes = Infinity) { this.source = source; this.maxStorableBytes = maxStorableBytes; this.loadedSegments = []; this.loadingSegments = []; this.sourceSizePromise = null; this.nextAge = 0; this.totalStoredBytes = 0; } async loadRange(start, end) { end = Math.min(end, await this.source.getSize()); if (start >= end) { return; } const matchingLoadingSegment = this.loadingSegments.find(x => x.start <= start && x.end >= end); if (matchingLoadingSegment) { // Simply wait for the existing promise to finish to avoid loading the same range twice await matchingLoadingSegment.promise; return; } const index = binarySearchLessOrEqual(this.loadedSegments, start, x => x.start); if (index !== -1) { for (let i = index; i < this.loadedSegments.length; i++) { const segment = this.loadedSegments[i]; if (segment.start > start) { break; } const segmentEncasesRequestedRange = segment.end >= end; if (segmentEncasesRequestedRange) { // Nothing to load return; } } } this.source.onread?.(start, end); const bytesPromise = this.source._read(start, end); const loadingSegment = { start, end, promise: bytesPromise }; this.loadingSegments.push(loadingSegment); const bytes = await bytesPromise; removeItem(this.loadingSegments, loadingSegment); this.insertIntoLoadedSegments(start, bytes); } rangeIsLoaded(start, end) { if (end <= start) { return true; } const index = binarySearchLessOrEqual(this.loadedSegments, start, x => x.start); if (index === -1) { return false; } for (let i = index; i < this.loadedSegments.length; i++) { const segment = this.loadedSegments[i]; if (segment.start > start) { break; } const segmentEncasesRequestedRange = segment.end >= end; if (segmentEncasesRequestedRange) { return true; } } return false; } insertIntoLoadedSegments(start, bytes) { const segment = { start, end: start + bytes.byteLength, bytes, view: new DataView(bytes.buffer), age: this.nextAge++, }; let index = binarySearchLessOrEqual(this.loadedSegments, start, x => x.start); if (index === -1 || this.loadedSegments[index].start < segment.start) { index++; } // Insert the segment at the right place so that the array remains sorted by start offset this.loadedSegments.splice(index, 0, segment); this.totalStoredBytes += bytes.byteLength; // Remove all other segments from the array that are completely covered by the newly-inserted segment for (let i = index + 1; i < this.loadedSegments.length; i++) { const otherSegment = this.loadedSegments[i]; if (otherSegment.start >= segment.end) { break; } if (segment.start <= otherSegment.start && otherSegment.end <= segment.end) { this.loadedSegments.splice(i, 1); i--; } } // If we overshoot the max amount of permitted bytes, let's start evicting the oldest segments while (this.totalStoredBytes > this.maxStorableBytes && this.loadedSegments.length > 1) { let oldestSegment = null; let oldestSegmentIndex = -1; for (let i = 0; i < this.loadedSegments.length; i++) { const candidate = this.loadedSegments[i]; if (!oldestSegment || candidate.age < oldestSegment.age) { oldestSegment = candidate; oldestSegmentIndex = i; } } assert(oldestSegment); this.totalStoredBytes -= oldestSegment.bytes.byteLength; this.loadedSegments.splice(oldestSegmentIndex, 1); } } getViewAndOffset(start, end) { const startIndex = binarySearchLessOrEqual(this.loadedSegments, start, x => x.start); let segment = null; if (startIndex !== -1) { for (let i = startIndex; i < this.loadedSegments.length; i++) { const candidate = this.loadedSegments[i]; if (candidate.start > start) { break; } if (end <= candidate.end) { segment = candidate; break; } } } if (!segment) { throw new Error(`No segment loaded for range [${start}, ${end}).`); } segment.age = this.nextAge++; return { view: segment.view, offset: segment.bytes.byteOffset + start - segment.start, }; } forgetRange(start, end) { if (end <= start) { return; } const startIndex = binarySearchLessOrEqual(this.loadedSegments, start, x => x.start); if (startIndex === -1) { return; } const segment = this.loadedSegments[startIndex]; if (segment.start !== start || segment.end !== end) { return; } this.loadedSegments.splice(startIndex, 1); this.totalStoredBytes -= segment.bytes.byteLength; } }