feat: batch video upscaling with folder upload, directory save, and result comparison (#1051)

* feat: support batch video upscaling with folder upload and directory save

- Add multi-file selection and entire folder upload (webkitdirectory)
- Support drag & drop of both files and folders via FileSystemEntry API
- Add save directory picker using File System Access API (showDirectoryPicker)
- Process videos sequentially with per-file progress and status tracking
- Add right-side result list with click-to-compare functionality
- Add per-video duration and total batch duration display
- Add retry button for failed items with error detail display
- Remove auto-play on comparison viewer load
- Fix worker error handling: handle "error" command to reject stale promises
- Fix event listener accumulation in video comparison (AbortController)
- Fix XSS risk by escaping HTML in all innerHTML injections
- Fix URL revocation timing in download to prevent large file truncation
- Extract writeToSaveDirectory and truncateName helpers to reduce duplication
- Add Usage Guide panel explaining single/multiple/folder upload options

Files changed:
  docs/upscaler/index.html  (+1110/-181)
  docs/upscaler/upscaler.js (+110)

* fix: prevent auto-play and resource consumption during upscaling

- Set preload="metadata" on preview and comparison video elements
- Explicitly pause preview video after setting src in showVideoPreview
- Remove preview video seek sync from batch processing progress callbacks
  (was causing continuous decode during upscaling, wasting GPU resources)
- Remove preview video seek sync from retry progress callback
- All videos now require manual play; no background resource usage

* fix: improve upscaled video quality with higher bitrate and codec support

- Raise base bitrate from 2.5 Mbps (25e5) to 8 Mbps (8e6) for standard and
  firefox processors, preventing quality loss when source videos have high
  bitrates (e.g. 720p @ 16.7 Mbps was producing 1440p @ 10 Mbps, smaller
  than the original file)
- Double the bitrate for mediabunny processor (X.n1 * 2) to account for the
  4x pixel increase from 2x upscaling
- Add H.264 Level 5.1 (avc1.640033) and Level 5.2 (avc1.640034) codecs to
  the fallback lists, since Level 4.0 (avc1.640028, ~24.6 Mbps) cannot
  support the higher bitrates for 1440p+ outputs
- Add latencyMode: "quality" to VideoEncoder configs for both standard and
  firefox processors, prioritizing encoding quality over latency (suitable
  for offline processing)

* feat: add video data comparison panel for original vs upscaled metrics

- Add a comparison data table below the file info bar showing 6 metrics:
  Resolution, File Size, Duration, Est. Bitrate, Pixels, Process Time
- Display per-metric change percentage with color-coded indicators:
  green (↑) for improvements, red (↓) for regressions, gray for neutral
- Estimated bitrate calculated from file size × 8 / video duration
- Table renders after both original and upscaled video metadata loads
- Auto-hide panel on reset

* fix: make video data comparison panel full-width and responsive

- Move comparison data table from inside the left column to the full-width
  area below the left/right layout, so it is no longer constrained by the
  comparison viewer column width
- Remove table-fixed colgroup and overflow-x-auto that caused horizontal
  scrolling on smaller screens
- Table now uses w-full auto-layout, displaying all 4 columns (Metric,
  Original, Upscaled, Change) without requiring horizontal scrolling

---------

Co-authored-by: rionzhao <rionzhao@tencent.com>
This commit is contained in:
糖猫猫andrionzhao authored and GitHub committed 2026-09-10 17:19:38 +08:00
commit 0896841d23
538 files changed
+379715

No files matched your search

+2219
View File
File diff suppressed because it is too large. Load diff
+723
View File
@@ -0,0 +1,723 @@
class Upscaler {
static DEFAULT_TIMEOUTS = { IMAGE: 300000, VIDEO: 3600000, METADATA: 10000 };
static DEFAULT_DELAYS = { INIT: 500, NETWORK: 300 };
static isSupported() {
try {
const hasWorker = typeof Worker !== "undefined";
const hasOffscreen =
typeof OffscreenCanvas !== "undefined" ||
(typeof document !== "undefined" &&
!!document.createElement("canvas").transferControlToOffscreen);
const hasBlob = typeof Blob !== "undefined";
return hasWorker && hasOffscreen && hasBlob;
} catch {
return false;
}
}
static isVideoSupported() {
try {
return (
typeof VideoEncoder !== "undefined" &&
typeof VideoDecoder !== "undefined"
);
} catch {
return false;
}
}
constructor(options = {}) {
const weightsBaseUrl = options.weightsBaseUrl || "/weights";
this.networks = options.networks || {
small: {
name: "anime4k/cnn-2x-l",
weightsUrl: `${weightsBaseUrl}/cnn-8.json`,
},
medium: {
name: "anime4k/cnn-2x-16",
weightsUrl: `${weightsBaseUrl}/cnn-16.json`,
},
large: {
name: "anime4k/cnn-2x-28",
weightsUrl: `${weightsBaseUrl}/cnn-28.json`,
},
};
this.weightsBaseUrl = weightsBaseUrl;
this.networkSize = options.networkSize || "medium";
this.workerUrl = options.workerUrl || "/worker/main.js";
this.timeouts = {
...Upscaler.DEFAULT_TIMEOUTS,
...(options.timeouts || {}),
};
this.delays = { ...Upscaler.DEFAULT_DELAYS, ...(options.delays || {}) };
this.imageScale =
typeof options.imageScale === "number" && options.imageScale > 0
? options.imageScale
: 2;
this.videoScale =
typeof options.videoScale === "number" && options.videoScale > 0
? options.videoScale
: 2;
this.weightsCache = new Map();
this.workerInstance = null;
this.messageHandlers = {};
this.progressCallback = null;
this.processingType = null;
this.currentReject = null;
this.currentTimeoutId = null;
this.realtimeLoopId = null;
this.realtimeState = null;
this.init();
}
init({ prewarm = true } = {}) {
if (!Upscaler.isSupported()) {
throw new Error("Upscaler is not supported in this environment");
}
if (prewarm) {
this.getWorker().postMessage({ cmd: "isSupported" });
}
return this;
}
getWorker() {
if (!this.workerInstance) {
this.workerInstance = new Worker(this.workerUrl);
this.workerInstance.onmessage = (event) =>
this.handleWorkerMessage(event);
this.workerInstance.onerror = (error) => {
console.error("[Upscaler] Worker error:", error);
this.rejectCurrent(
new Error(
error.message || "Worker crashed unexpectedly",
),
);
};
this.workerInstance.onmessageerror = (error) => {
console.error("[Upscaler] Worker message error:", error);
this.rejectCurrent(
new Error("Worker message serialization error"),
);
};
}
return this.workerInstance;
}
rejectCurrent(error) {
if (this.currentReject) {
const reject = this.currentReject;
this.currentReject = null;
if (this.currentTimeoutId) {
clearTimeout(this.currentTimeoutId);
this.currentTimeoutId = null;
}
// Clean up whichever blob handler is currently registered
delete this.messageHandlers.videoBlob;
delete this.messageHandlers.imageBlob;
reject(error);
}
}
static extractProgressValue(data) {
const value = data.data ?? data.progress ?? data.value ?? data.percentage;
return typeof value === "number"
? Math.min(100, Math.max(0, Math.round(value)))
: null;
}
handleBlobResponse(data) {
const blobType =
this.processingType === "video" ? "videoBlob" : "imageBlob";
const handler = this.messageHandlers[blobType];
if (data.data instanceof Blob && handler) {
handler({ [blobType]: data.data });
}
}
requestBlob() {
const cmd =
this.processingType === "video" ? "getVideoBlob" : "getImageBlob";
this.getWorker().postMessage({ cmd });
}
handleWorkerMessage(event) {
const { data } = event;
console.debug("Worker message:", data);
if (!data.cmd) return;
const { cmd } = data;
if (cmd === "error") {
const errorMsg =
data.data || data.message || "Unknown worker error";
console.error("[Upscaler] Worker reported error:", errorMsg);
this.rejectCurrent(new Error(errorMsg));
} else if (cmd === "progress") {
const progress = Upscaler.extractProgressValue(data);
if (progress !== null && this.progressCallback) {
this.progressCallback(progress);
}
if (this.messageHandlers.progress) this.messageHandlers.progress(data);
} else if (cmd === "finished") {
if (data.data instanceof Blob) {
this.handleBlobResponse(data);
} else {
this.requestBlob();
}
} else if (this.messageHandlers[cmd]) {
this.messageHandlers[cmd](data);
}
}
async loadWeights(networkSize) {
this.validateNetworkSize(networkSize);
if (this.weightsCache.has(networkSize)) {
return this.weightsCache.get(networkSize);
}
const network = this.networks[networkSize];
const response = await fetch(network.weightsUrl);
if (!response.ok)
throw new Error(`Failed to fetch weights: ${response.statusText}`);
const weights = await response.json();
this.weightsCache.set(networkSize, weights);
return weights;
}
static loadImageMetadata(arrayBuffer, mimeType) {
return new Promise((resolve, reject) => {
const blob = new Blob([arrayBuffer], { type: mimeType });
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(url);
resolve({ width: img.width, height: img.height });
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error("Failed to load image"));
};
img.src = url;
});
}
static loadVideoMetadata(file, timeoutMs) {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const video = document.createElement("video");
video.src = url;
const timeout = setTimeout(() => reject(new Error("Timeout")), timeoutMs);
video.onloadedmetadata = () => {
clearTimeout(timeout);
URL.revokeObjectURL(url);
resolve({
width: video.videoWidth,
height: video.videoHeight,
duration: video.duration,
});
};
video.onerror = () => {
clearTimeout(timeout);
URL.revokeObjectURL(url);
reject(new Error("Failed to load video"));
};
});
}
createOffscreenCanvas(width, height, scale = 2) {
const factor = typeof scale === "number" && scale > 0 ? scale : 1;
const canvas = document.createElement("canvas");
canvas.width = width * factor;
canvas.height = height * factor;
if (typeof canvas.transferControlToOffscreen !== "function") {
throw new Error("OffscreenCanvas is not supported in this environment");
}
return canvas.transferControlToOffscreen();
}
createBlobPromise(blobType, timeout) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.currentReject = null;
this.currentTimeoutId = null;
if (this.messageHandlers[blobType]) {
delete this.messageHandlers[blobType];
reject(new Error(`${blobType} processing timeout`));
}
}, timeout);
this.currentReject = reject;
this.currentTimeoutId = timeoutId;
this.messageHandlers[blobType] = (msg) => {
delete this.messageHandlers[blobType];
clearTimeout(timeoutId);
this.currentReject = null;
this.currentTimeoutId = null;
const blob = msg[blobType] || msg.data;
if (blob instanceof Blob) {
resolve(blob);
} else {
reject(new Error(`Invalid ${blobType} format`));
}
};
});
}
validateNetworkSize(size) {
if (!this.networks[size]) {
throw new Error(
"Invalid networkSize: use one of " +
Object.keys(this.networks).join(", "),
);
}
}
async upscaleImage(imageFile, networkSize, scaleOverride) {
if (!imageFile) throw new Error("Image file is required");
const size = networkSize || this.networkSize;
this.validateNetworkSize(size);
// Reject any stale promise from a previous aborted operation
this.rejectCurrent(new Error("Aborted by new operation"));
this.processingType = "image";
const arrayBuffer = await imageFile.arrayBuffer();
const mimeType = imageFile.type || "image/jpeg";
const { width, height } = await Upscaler.loadImageMetadata(
arrayBuffer,
mimeType,
);
const weights = await this.loadWeights(size);
const network = this.networks[size];
const scale =
typeof scaleOverride === "number" && scaleOverride > 0
? scaleOverride
: this.imageScale;
const [canvasUp, canvasOrig] = [
this.createOffscreenCanvas(width, height, scale),
this.createOffscreenCanvas(width, height, 1),
];
const blobPromise = this.createBlobPromise(
"imageBlob",
this.timeouts.IMAGE,
);
this.getWorker().postMessage(
{
cmd: "init",
data: {
imageArrayBuffer: arrayBuffer,
imageMimeType: mimeType,
upscaled: canvasUp,
original: canvasOrig,
resolution: { width, height },
network_name: network.name,
weights,
},
},
[canvasUp, canvasOrig],
);
setTimeout(() => {
this.getWorker().postMessage({
cmd: "network",
data: {
name: network.name,
imageArrayBuffer: arrayBuffer,
imageMimeType: mimeType,
weights,
},
});
setTimeout(() => {
this.getWorker().postMessage({
cmd: "getImageBlob",
data: { imageArrayBuffer: arrayBuffer, imageMimeType: mimeType },
});
}, this.delays.NETWORK);
}, this.delays.INIT);
return blobPromise;
}
async upscaleVideo(videoFile, networkSize, scaleOverride) {
if (!videoFile) throw new Error("Video file is required");
const size = networkSize || this.networkSize;
this.validateNetworkSize(size);
if (!Upscaler.isVideoSupported()) {
throw new Error("WebCodecs API not supported. Requires Chrome 94+");
}
// Reject any stale promise from a previous aborted operation
this.rejectCurrent(new Error("Aborted by new operation"));
this.processingType = "video";
const { width, height, duration } = await Upscaler.loadVideoMetadata(
videoFile,
this.timeouts.METADATA,
);
const weights = await this.loadWeights(size);
const network = this.networks[size];
const scale =
typeof scaleOverride === "number" && scaleOverride > 0
? scaleOverride
: this.videoScale;
const [canvasOut, canvasIn] = [
this.createOffscreenCanvas(width, height, scale),
this.createOffscreenCanvas(width, height, 1),
];
const blobPromise = this.createBlobPromise(
"videoBlob",
this.timeouts.VIDEO,
);
this.getWorker().postMessage(
{
cmd: "process",
file: videoFile,
fileSize: videoFile.size,
duration,
adjustedResolution: {
adjustedInputWidth: width,
adjustedInputHeight: height,
adjustedOutputWidth: width * scale,
adjustedOutputHeight: height * scale,
},
upscaled: canvasOut,
original: canvasIn,
weights,
network_name: network.name,
skipDemuxProgress: true,
},
[canvasOut, canvasIn],
);
return blobPromise;
}
async startRealtimeUpscale(
videoElement,
canvasElement,
networkSize,
scaleOverride,
) {
if (!videoElement || !canvasElement) {
throw new Error("videoElement and canvasElement are required");
}
const size = networkSize || this.networkSize;
this.validateNetworkSize(size);
const network = this.networks[size];
const weights = await this.loadWeights(size);
const scale =
typeof scaleOverride === "number" && scaleOverride > 0
? scaleOverride
: this.videoScale;
const ensureMetadata = () => {
return new Promise((resolve, reject) => {
if (videoElement.readyState >= 1) {
resolve();
return;
}
const onLoaded = () => {
cleanup();
resolve();
};
const onError = () => {
cleanup();
reject(new Error("Failed to load video metadata"));
};
const cleanup = () => {
videoElement.removeEventListener("loadedmetadata", onLoaded);
videoElement.removeEventListener("error", onError);
};
videoElement.addEventListener("loadedmetadata", onLoaded);
videoElement.addEventListener("error", onError);
});
};
await ensureMetadata();
const width = videoElement.videoWidth;
const height = videoElement.videoHeight;
if (!width || !height) {
throw new Error("Invalid video dimensions");
}
const firstInit = !canvasElement._upscalerTransferred;
let offscreen = null;
if (firstInit) {
canvasElement.width = width * scale;
canvasElement.height = height * scale;
if (typeof canvasElement.transferControlToOffscreen !== "function") {
throw new Error("OffscreenCanvas is not supported in this environment");
}
offscreen = canvasElement.transferControlToOffscreen();
canvasElement._upscalerTransferred = true;
}
const captureCanvas = document.createElement("canvas");
captureCanvas.width = width;
captureCanvas.height = height;
const captureCtx = captureCanvas.getContext("2d");
if (this.realtimeLoopId) {
cancelAnimationFrame(this.realtimeLoopId);
this.realtimeLoopId = null;
}
const initPayload = {
cmd: "realtimeInit",
data: {
upscaled: firstInit ? offscreen : null,
resolution: {
width,
height,
scale,
outputWidth: width * scale,
outputHeight: height * scale,
},
network_name: network.name,
weights,
},
};
if (firstInit && offscreen) {
this.getWorker().postMessage(initPayload, [offscreen]);
} else {
this.getWorker().postMessage(initPayload);
}
this.realtimeState = {
running: true,
video: videoElement,
canvas: canvasElement,
scale,
busy: false,
frameIndex: 0,
captureCanvas,
captureCtx,
};
const loop = async () => {
if (!this.realtimeState || !this.realtimeState.running) return;
const state = this.realtimeState;
const v = state.video;
const haveCurrentData = v.HAVE_CURRENT_DATA || 2;
if (
!v.paused &&
!v.ended &&
!state.busy &&
v.readyState >= haveCurrentData &&
v.videoWidth > 0 &&
v.videoHeight > 0
) {
state.busy = true;
try {
const captureCanvas = state.captureCanvas;
const captureCtx = state.captureCtx;
if (!captureCanvas || !captureCtx) {
throw new Error("Missing capture canvas");
}
captureCtx.drawImage(v, 0, 0, captureCanvas.width, captureCanvas.height);
const frame = await createImageBitmap(captureCanvas);
state.frameIndex += 1;
this.getWorker().postMessage(
{
cmd: "realtimeFrame",
frame,
},
[frame],
);
} catch (e) {
console.warn("realtimeFrame error", e);
} finally {
if (this.realtimeState) {
this.realtimeState.busy = false;
}
}
}
this.realtimeLoopId = requestAnimationFrame(loop);
};
this.realtimeLoopId = requestAnimationFrame(loop);
}
stopRealtimeUpscale() {
if (this.realtimeLoopId) {
cancelAnimationFrame(this.realtimeLoopId);
this.realtimeLoopId = null;
}
if (this.realtimeState) {
this.realtimeState.running = false;
this.realtimeState = null;
}
}
async downloadUpscaled(file, networkSize, filename = null) {
const isVideo = file.type.startsWith("video");
const size = networkSize || this.networkSize;
const blob = isVideo
? await this.upscaleVideo(file, size)
: await this.upscaleImage(file, size);
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download =
filename || `upscaled_${Date.now()}.${isVideo ? "mp4" : "png"}`;
link.click();
URL.revokeObjectURL(url);
return blob;
}
getSupportedNetworks() {
return Object.keys(this.networks);
}
static async getGPUCapability() {
try {
if (navigator.gpu) {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) {
const buffer = adapter.limits.maxStorageBufferBindingSize / 512;
const texture = 8192 * 8192;
return Math.min(buffer, texture);
}
}
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl2");
if (gl) return gl.getParameter(gl.MAX_TEXTURE_SIZE) ** 2;
return 8388608;
} catch {
return 8388608;
}
}
static getBackendType() {
try {
if (navigator.gpu) return "webgpu";
if (document.createElement("canvas").getContext("webgl2")) return "webgl";
return "unknown";
} catch {
return "unknown";
}
}
getAppState() {
return {
backend: Upscaler.getBackendType(),
isProcessing: false,
progress: 0,
width: 0,
height: 0,
};
}
onProgress(callback) {
if (typeof callback === "function") {
this.progressCallback = callback;
}
}
async upscaleVideoBatch(files, options = {}) {
const {
networkSize,
directoryHandle,
onFileStart,
onFileProgress,
onFileComplete,
onFileError,
} = options;
const results = [];
const size = networkSize || this.networkSize;
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!file.type.startsWith("video/")) {
const err = new Error(`Skipped (not a video): ${file.name}`);
if (onFileError) onFileError(file, i, err);
results.push({ file, error: err, success: false });
continue;
}
try {
if (onFileStart) onFileStart(file, i, files.length);
this.onProgress((progress) => {
if (onFileProgress) onFileProgress(file, i, progress);
});
const blob = await this.upscaleVideo(file, size);
if (directoryHandle) {
const outName = this._buildOutputName(file.name);
const fh = await directoryHandle.getFileHandle(outName, {
create: true,
});
const writable = await fh.createWritable();
await writable.write(blob);
await writable.close();
}
if (onFileComplete) onFileComplete(file, i, blob);
results.push({ file, blob, success: true });
} catch (error) {
if (onFileError) onFileError(file, i, error);
results.push({ file, error, success: false });
}
}
this.progressCallback = null;
return results;
}
_buildOutputName(originalName) {
const dot = originalName.lastIndexOf(".");
const base = dot > 0 ? originalName.slice(0, dot) : originalName;
return `${base}_upscaled.mp4`;
}
dispose() {
this.stopRealtimeUpscale();
if (this.workerInstance) {
this.workerInstance.terminate();
this.workerInstance = null;
}
this.messageHandlers = {};
this.progressCallback = null;
this.processingType = null;
this.weightsCache.clear();
}
}
window.Upscaler = Upscaler;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
(globalThis.webpackChunkfree_video_upscaler=globalThis.webpackChunkfree_video_upscaler||[]).push([[869],{41:(t,e)=>{var i,r,s=(i=new Date,r=4,{setLogLevel:function(t){r=t==this.debug?1:t==this.info?2:t==this.warn?3:(this.error,4)},debug:function(t,e){void 0===console.debug&&(console.debug=console.log),1>=r&&console.debug("["+s.getDurationString(new Date-i,1e3)+"]","["+t+"]",e)},log:function(t,e){this.debug(t.msg)},info:function(t,e){2>=r&&console.info("["+s.getDurationString(new Date-i,1e3)+"]","["+t+"]",e)},warn:function(t,e){3>=r&&console.warn("["+s.getDurationString(new Date-i,1e3)+"]","["+t+"]",e)},error:function(t,e){4>=r&&console.error("["+s.getDurationString(new Date-i,1e3)+"]","["+t+"]",e)}});s.getDurationString=function(t,e){var i;function r(t,e){for(var i=(""+t).split(".");i[0].length<e;)i[0]="0"+i[0];return i.join(".")}t<0?(i=!0,t=-t):i=!1;var s=t/(e||1),a=Math.floor(s/3600);s-=3600*a;var n=Math.floor(s/60),o=1e3*(s-=60*n);return o-=1e3*(s=Math.floor(s)),o=Math.floor(o),(i?"-":"")+a+":"+r(n,2)+":"+r(s,2)+"."+r(o,3)},s.printRanges=function(t){var e=t.length;if(e>0){for(var i="",r=0;r<e;r++)r>0&&(i+=","),i+="["+s.getDurationString(t.start(r))+","+s.getDurationString(t.end(r))+"]";return i}return"(empty)"},e.Log=s;var a=function(t){if(!(t instanceof ArrayBuffer))throw"Needs an array buffer";this.buffer=t,this.dataview=new DataView(t),this.position=0};a.prototype.getPosition=function(){return this.position},a.prototype.getEndPosition=function(){return this.buffer.byteLength},a.prototype.getLength=function(){return this.buffer.byteLength},a.prototype.seek=function(t){var e=Math.max(0,Math.min(this.buffer.byteLength,t));return this.position=isNaN(e)||!isFinite(e)?0:e,!0},a.prototype.isEos=function(){return this.getPosition()>=this.getEndPosition()},a.prototype.readAnyInt=function(t,e){var i=0;if(this.position+t<=this.buffer.byteLength){switch(t){case 1:i=e?this.dataview.getInt8(this.position):this.dataview.getUint8(this.position);break;case 2:i=e?this.dataview.getInt16(this.position):this.dataview.getUint16(this.position);break;case 3:if(e)throw"No method for reading signed 24 bits values";i=this.dataview.getUint8(this.position)<<16,i|=this.dataview.getUint8(this.position+1)<<8,i|=this.dataview.getUint8(this.position+2);break;case 4:i=e?this.dataview.getInt32(this.position):this.dataview.getUint32(this.position);break;case 8:if(e)throw"No method for reading signed 64 bits values";i=this.dataview.getUint32(this.position)<<32,i|=this.dataview.getUint32(this.position+4);break;default:throw"readInt method not implemented for size: "+t}return this.position+=t,i}throw"Not enough bytes in buffer"},a.prototype.readUint8=function(){return this.readAnyInt(1,!1)},a.prototype.readUint16=function(){return this.readAnyInt(2,!1)},a.prototype.readUint24=function(){return this.readAnyInt(3,!1)},a.prototype.readUint32=function(){return this.readAnyInt(4,!1)},a.prototype.readUint64=function(){return this.readAnyInt(8,!1)},a.prototype.readString=function(t){if(this.position+t<=this.buffer.byteLength){for(var e="",i=0;i<t;i++)e+=String.fromCharCode(this.readUint8());return e}throw"Not enough bytes in buffer"},a.prototype.readCString=function(){for(var t=[];;){var e=this.readUint8();if(0===e)break;t.push(e)}return String.fromCharCode.apply(null,t)},a.prototype.readInt8=function(){return this.readAnyInt(1,!0)},a.prototype.readInt16=function(){return this.readAnyInt(2,!0)},a.prototype.readInt32=function(){return this.readAnyInt(4,!0)},a.prototype.readInt64=function(){return this.readAnyInt(8,!1)},a.prototype.readUint8Array=function(t){for(var e=new Uint8Array(t),i=0;i<t;i++)e[i]=this.readUint8();return e},a.prototype.readInt16Array=function(t){for(var e=new Int16Array(t),i=0;i<t;i++)e[i]=this.readInt16();return e},a.prototype.readUint16Array=function(t){for(var e=new Int16Array(t),i=0;i<t;i++)e[i]=this.readUint16();return e},a.prototype.readUint32Array=function(t){for(var e=new Uint32Array(t),i=0;i<t;i++)e[i]=this.readUint32();return e},a.prototype.readInt32Array=function(t){for(var e=new Int32Array(t),i=0;i<t;i++)e[i]=this.readInt32();return e},e.MP4BoxStream=a;var n=function(t,e,i){this._byteOffset=e||0,t instanceof ArrayBuffer?this.buffer=t:"object"==typeof t?(this.dataView=t,e&&(this._byteOffset+=e)):this.buffer=new ArrayBuffer(t||0),this.position=0,this.endianness=null==i?n.LITTLE_ENDIAN:i};n.prototype={},n.prototype.getPosition=function(){return this.position},n.prototype._realloc=function(t){if(this._dynamicSize){var e=this._byteOffset+this.position+t,i=this._buffer.byteLength;if(e<=i)e>this._byteLength&&(this._byteLength=e);else{for(i<1&&(i=1);e>i;)i*=2;var r=new ArrayBuffer(i),s=new Uint8Array(this._buffer);new Uint8Array(r,0,s.length).set(s),this.buffer=r,this._byteLength=e}}},n.prototype._trimAlloc=function(){if(this._byteLength!=this._buffer.byteLength){var t=new ArrayBuffer(this._byteLength),e=new Uint8Array(t),i=new Uint8Array(this._buffer,0,e.length);e.set(i),this.buffer=t}},n.BIG_ENDIAN=!1,n.LITTLE_ENDIAN=!0,Line truncated