mediabunny
Version:
Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.
154 lines (153 loc) • 6.05 kB
JavaScript
/*!
* 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;
}
}