Merge main into release for tag v1.15.2

This commit is contained in:
github-actions[bot]
2025-09-16 21:15:44 +00:00
20 changed files with 165 additions and 80 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ Core features include:
- **Wide format support**: Read and write MP4, MOV, WebM, MKV, WAVE, MP3, Ogg, ADTS - **Wide format support**: Read and write MP4, MOV, WebM, MKV, WAVE, MP3, Ogg, ADTS
- **Built-in encoding & decoding**: Supports 25+ video, audio, and subtitle codecs, hardware-accelerated using the WebCodecs API - **Built-in encoding & decoding**: Supports 25+ video, audio, and subtitle codecs, hardware-accelerated using the WebCodecs API
- **High precision**: Fine-grained, microsecond-accurate reading and writing operations - **High precision**: Fine-grained, microsecond-accurate reading and writing operations
- **Conversion API**: Easy-to-use API with features such as transmuxing, transcoding, resizing, rotation, resampling, trimming, and more - **Conversion API**: Easy-to-use API with features such as transmuxing, transcoding, resizing, rotation, cropping, resampling, trimming, and more
- **Streaming I/O**: Handle reading & writing files of any size with memory-efficient streaming - **Streaming I/O**: Handle reading & writing files of any size with memory-efficient streaming
- **Tree-shakable**: Only bundle what you use (as small as 5 kB gzipped) - **Tree-shakable**: Only bundle what you use (as small as 5 kB gzipped)
- **Zero dependencies**: Implemented in highly performant TypeScript - **Zero dependencies**: Implemented in highly performant TypeScript
+27 -27
View File
@@ -47,23 +47,6 @@
format: outputFormat, format: outputFormat,
target target
}); });
output.setMetadataTags({
title: 'Bigggy',
artist: 'Buck Bunny',
images: [{
data: blobData,
kind: 'coverFront',
mimeType: 'image/jpeg'
}],
trackNumber: 4,
tracksTotal: 10,
discNumber: 5,
discNumberMax: 8,
lyrics: "There's no way\nThat it's not going there",
raw: {
'ENCODER': 'Mediabunny epic own encoder'
}
});
const conversion = await Mediabunny.Conversion.init({ const conversion = await Mediabunny.Conversion.init({
input: new Mediabunny.Input({ input: new Mediabunny.Input({
@@ -109,16 +92,16 @@
*/ */
video: () => ({ video: () => ({
//discard: true, //discard: true,
crop: { //crop: {
left: 0, // left: 0,
top: 0, // top: 0,
width: 500, // width: 500,
height: 500, // height: 500,
}, //},
rotate: 90, //rotate: 90,
width: 200, //width: 200,
height: 500, //height: 500,
fit: 'contain', //fit: 'contain',
//forceTranscode: true, //forceTranscode: true,
//codec: 'avc', //codec: 'avc',
//fit: 'contain', //fit: 'contain',
@@ -141,6 +124,23 @@
//width: 200, //width: 200,
//height: 100, //height: 100,
}), }),
tags: {
title: 'Bigggy',
artist: 'Buck Bunny',
images: [{
data: blobData,
kind: 'coverFront',
mimeType: 'image/jpeg'
}],
trackNumber: 4,
tracksTotal: 10,
discNumber: 5,
discsTotal: 8,
lyrics: "There's no way\nThat it's not going there",
raw: {
'AIGC': 'Mediabunny epic own encoder'
}
},
trim: { trim: {
start: 10, start: 10,
end: 20 end: 20
+1 -6
View File
@@ -14,12 +14,7 @@
source: new Mediabunny.BlobSource(file), source: new Mediabunny.BlobSource(file),
}); });
const audioTrack = await input.getPrimaryAudioTrack(); console.log(await input.getMetadataTags())
const sink = new Mediabunny.EncodedPacketSink(audioTrack);
for await (const packet of sink.packets()) {
console.log(packet);
}
/* /*
const sink = new Mediabunny.EncodedPacketSink(videoTrack); const sink = new Mediabunny.EncodedPacketSink(videoTrack);
+4 -3
View File
@@ -116,9 +116,10 @@
*/ */
let videoSource = new Mediabunny.CanvasSource(canvas, { let videoSource = new Mediabunny.CanvasSource(canvas, {
codec: 'avc', codec: 'vp9',
//fullCodecString: 'avc1.42001f', //fullCodecString: 'avc1.42001f',
bitrate: 1e6 bitrate: 1e6,
onEncoderConfig: console.log,
}); });
let audioSource = new Mediabunny.AudioBufferSource({ let audioSource = new Mediabunny.AudioBufferSource({
codec: 'opus', codec: 'opus',
@@ -207,5 +208,5 @@ Testing... <00:17.350>One... <00:18.125>Two...
await output.finalize(); await output.finalize();
console.log(target); console.log(target);
download(new Blob([target.buffer]), 'test' + format.fileExtension); //download(new Blob([target.buffer]), 'test' + format.fileExtension);
</script> </script>
+6 -5
View File
@@ -11,6 +11,7 @@ It has the following features:
- Trimming - Trimming
- Video resizing & fitting - Video resizing & fitting
- Video rotation - Video rotation
- Video cropping
- Video frame rate adjustment - Video frame rate adjustment
- Audio resampling - Audio resampling
- Audio up/downmixing - Audio up/downmixing
@@ -48,7 +49,7 @@ await conversion.execute();
That's it! A `Conversion` simply takes an instance of `Input` and `Output`, then reads the data from the input and writes it to the output. If you're unfamiliar with [`Input`](./reading-media-files) and [`Output`](./writing-media-files), check out their respective guides. That's it! A `Conversion` simply takes an instance of `Input` and `Output`, then reads the data from the input and writes it to the output. If you're unfamiliar with [`Input`](./reading-media-files) and [`Output`](./writing-media-files), check out their respective guides.
::: info ::: info
The `Output` passed to the `Conversion` must be *fresh*; that is, it must have no added tracks and be in the `'pending'` state (not started yet). The `Output` passed to the `Conversion` must be *fresh*; that is, it must have no added tracks or metadata tags and be in the `'pending'` state (not started yet).
::: :::
Unconfigured, the conversion process handles all the details automatically, such as: Unconfigured, the conversion process handles all the details automatically, such as:
@@ -298,17 +299,17 @@ By default, any [descriptive metadata tags](../api/MetadataTags.md) of the input
// Set your own metadata: // Set your own metadata:
const conversion = await Conversion.init({ const conversion = await Conversion.init({
// ... // ...
tags: () => ({ tags: {
title: 're:Turning', title: 're:Turning',
artist: 'Alexander Panos', artist: 'Alexander Panos',
}), },
// ... // ...
}); });
// Or, augment the input's metadata: // Or, augment the input's metadata:
const conversion = await Conversion.init({ const conversion = await Conversion.init({
// ... // ...
tags: inputTags => ({ tags: (inputTags) => ({
...inputTags, // Keep the existing metadata ...inputTags, // Keep the existing metadata
images: [{ // And add cover art images: [{ // And add cover art
data: new Uint8Array(...), data: new Uint8Array(...),
@@ -323,7 +324,7 @@ const conversion = await Conversion.init({
// Or, remove all metadata // Or, remove all metadata
const conversion = await Conversion.init({ const conversion = await Conversion.init({
// ... // ...
tags: () => ({}), tags: {},
// ... // ...
}); });
``` ```
+1 -1
View File
@@ -17,7 +17,7 @@ Here's a long list of stuff this library does:
- Lazy, optimized, on-demand file reading - Lazy, optimized, on-demand file reading
- Input and output streaming, arbitrary file size support - Input and output streaming, arbitrary file size support
- File location independence (memory, disk, network, ...) - File location independence (memory, disk, network, ...)
- Utilities for compression, resizing, rotation, resampling, trimming - Utilities for compression, resizing, rotation, cropping, resampling, trimming
- Transmuxing and transcoding - Transmuxing and transcoding
- Microsecond-accurate reading and writing precision - Microsecond-accurate reading and writing precision
- Efficient seeking through time - Efficient seeking through time
+6 -6
View File
@@ -1,12 +1,12 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"version": "1.15.1", "version": "1.15.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mediabunny", "name": "mediabunny",
"version": "1.15.1", "version": "1.15.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@@ -7749,9 +7749,9 @@
} }
}, },
"node_modules/mediabunny": { "node_modules/mediabunny": {
"version": "1.15.0", "version": "1.15.1",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.15.0.tgz", "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.15.1.tgz",
"integrity": "sha512-yeqr+4sRNzl7QqeHckr+SgPKfaQwSSmhTdVSgKgbenneC+TFlBrk+zCRhF9b7kwLtrm6qziJc/flqYQccX6W/Q==", "integrity": "sha512-+eRTVzd3E4LuGYZzPSQcPzuGdAIljohSlzYTX358XsfLM2qH1lQIBYa+erx7wzVcGQLRNjdV7x7ZS0EpK04DfA==",
"license": "MPL-2.0", "license": "MPL-2.0",
"peer": true, "peer": true,
"workspaces": [ "workspaces": [
@@ -12242,7 +12242,7 @@
}, },
"packages/mp3-encoder": { "packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"version": "1.15.1", "version": "1.15.2",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
+9 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.15.1", "version": "1.15.2",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
@@ -22,6 +22,10 @@
"dist", "dist",
"src" "src"
], ],
"browser": {
"./dist/modules/src/node.js": false,
"./src/node.ts": false
},
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {
"build": "./build.sh", "build": "./build.sh",
@@ -102,11 +106,15 @@
"decoder", "decoder",
"convert", "convert",
"mp4", "mp4",
"mov",
"fmp4", "fmp4",
"webm", "webm",
"mkv",
"wav", "wav",
"mp3", "mp3",
"ogg", "ogg",
"adts",
"flac",
"video", "video",
"audio", "audio",
"subtitles", "subtitles",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.15.1", "version": "1.15.2",
"description": "MP3 encoder extension for Mediabunny, based on LAME.", "description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+1 -1
View File
@@ -24,7 +24,7 @@ const walkDir = (dir: string) => {
const fixFile = (filePath: string) => { const fixFile = (filePath: string) => {
const content = fs.readFileSync(filePath, 'utf8'); const content = fs.readFileSync(filePath, 'utf8');
const fixed = content.replace( const fixed = content.replace(
/(\s+from\s+['"])([^'"]*)(['"])/g, /(\s+from\s+['"])(\.[^'"]*)(['"])/g, // This only matches relative imports
'$1$2.js$3', '$1$2.js$3',
); );
+3
View File
@@ -16,6 +16,9 @@ const createVariants = async (
entryPoints: [entryPoint], entryPoints: [entryPoint],
bundle: true, bundle: true,
logLevel: 'info', logLevel: 'info',
logOverride: {
'import-is-undefined': 'silent', // Warning caused by the disabled "node.ts" import
},
banner: { banner: {
js: `/*! js: `/*!
* Copyright (c) 2025-present, Vanilagy and contributors * Copyright (c) 2025-present, Vanilagy and contributors
+3 -3
View File
@@ -1622,16 +1622,16 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
const main = () => { const main = () => {
const args = process.argv.slice(2); const args = process.argv.slice(2);
// Check for --dry flag // Check for --dry flag
const dryIndex = args.indexOf('--dry'); const dryIndex = args.indexOf('--dry');
const dry = dryIndex !== -1; const dry = dryIndex !== -1;
// Remove --dry flag from args // Remove --dry flag from args
if (dry) { if (dry) {
args.splice(dryIndex, 1); args.splice(dryIndex, 1);
} }
if (args.length < 2) { if (args.length < 2) {
console.error('Usage: npm run generate-docs [--dry] <entry-file1> [entry-file2 ...] <api-config-file>'); console.error('Usage: npm run generate-docs [--dry] <entry-file1> [entry-file2 ...] <api-config-file>');
console.error(' --dry: Check if docs are generatable without writing files'); console.error(' --dry: Check if docs are generatable without writing files');
+22 -9
View File
@@ -89,13 +89,13 @@ export type ConversionOptions = {
}; };
/** /**
* A callback that returns or resolves to the descriptive metadata tags that should be written to the output file. * An object or a callback that returns or resolves to an object containing the descriptive metadata tags that
* As input, this function will be passed the tags of the input file, allowing you to modify, augment or extend * should be written to the output file. If a function is passed, it will be passed the tags of the input file as
* them. * its first argument, allowing you to modify, augment or extend them.
* *
* If no function is set, the input's metadata tags will be copied to the output. * If no function is set, the input's metadata tags will be copied to the output.
*/ */
tags?: (inputTags: MetadataTags) => MaybePromise<MetadataTags>; tags?: MetadataTags | ((inputTags: MetadataTags) => MaybePromise<MetadataTags>);
}; };
/** /**
@@ -395,8 +395,12 @@ export class Conversion {
if (!(options.output instanceof Output)) { if (!(options.output instanceof Output)) {
throw new TypeError('options.output must be an Output.'); throw new TypeError('options.output must be an Output.');
} }
if (options.output._tracks.length > 0 || options.output.state !== 'pending') { if (
throw new TypeError('options.output must be fresh: no tracks added and not started.'); options.output._tracks.length > 0
|| Object.keys(options.output._metadataTags).length > 0
|| options.output.state !== 'pending'
) {
throw new TypeError('options.output must be fresh: no tracks or metadata tags added and not started.');
} }
if (typeof options.video !== 'function') { if (typeof options.video !== 'function') {
@@ -426,8 +430,15 @@ export class Conversion {
&& options.trim.start >= options.trim.end) { && options.trim.start >= options.trim.end) {
throw new TypeError('options.trim.start must be less than options.trim.end.'); throw new TypeError('options.trim.start must be less than options.trim.end.');
} }
if (options.tags !== undefined && typeof options.tags !== 'function') { if (
throw new TypeError('options.tags, when provided, must be a function.'); options.tags !== undefined
&& (typeof options.tags !== 'object' || !options.tags)
&& typeof options.tags !== 'function'
) {
throw new TypeError('options.tags, when provided, must be an object or a function.');
}
if (typeof options.tags === 'object') {
validateMetadataTags(options.tags);
} }
this._options = options; this._options = options;
@@ -519,7 +530,9 @@ export class Conversion {
let outputTags: MetadataTags; let outputTags: MetadataTags;
if (this._options.tags) { if (this._options.tags) {
const result = await this._options.tags(inputTags); const result = typeof this._options.tags === 'function'
? await this._options.tags(inputTags)
: this._options.tags;
validateMetadataTags(result); validateMetadataTags(result);
outputTags = result; outputTags = result;
+15
View File
@@ -2306,6 +2306,21 @@ export class IsobmffDemuxer extends Demuxer {
} }
}; break; }; break;
case 'track': {
if (typeof data === 'string') {
const parts = data.split('/');
const trackNum = Number.parseInt(parts[0]!, 10);
const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
if (Number.isInteger(trackNum) && trackNum > 0) {
this.metadataTags.trackNumber ??= trackNum;
}
if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
this.metadataTags.tracksTotal ??= tracksTotal;
}
}
}; break;
case 'trkn': { case 'trkn': {
if (data instanceof Uint8Array) { if (data instanceof Uint8Array) {
const view = toDataView(data); const view = toDataView(data);
+10 -3
View File
@@ -18,6 +18,7 @@ import {
getInt24, getInt24,
getUint24, getUint24,
insertSorted, insertSorted,
isFirefox,
isSafari, isSafari,
last, last,
mapAsyncGenerator, mapAsyncGenerator,
@@ -1199,14 +1200,20 @@ export class CanvasSink {
this._nextCanvasIndex = (this._nextCanvasIndex + 1) % this._canvasPool.length; this._nextCanvasIndex = (this._nextCanvasIndex + 1) % this._canvasPool.length;
} }
const context const context = canvas.getContext('2d', {
= canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; alpha: isFirefox(), // Firefox has VideoFrame glitches with opaque canvases
}) as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
assert(context); assert(context);
context.resetTransform(); context.resetTransform();
if (!canvasIsNew) { if (!canvasIsNew) {
context.clearRect(0, 0, this._width, this._height); if (isFirefox()) {
context.fillStyle = 'black';
context.fillRect(0, 0, this._width, this._height);
} else {
context.clearRect(0, 0, this._width, this._height);
}
} }
sample.drawWithFit(context, { sample.drawWithFit(context, {
+19 -4
View File
@@ -18,7 +18,16 @@ import {
VideoCodec, VideoCodec,
} from './codec'; } from './codec';
import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from './output'; import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from './output';
import { assert, assertNever, CallSerializer, clamp, promiseWithResolvers, setInt24, setUint24 } from './misc'; import {
assert,
assertNever,
CallSerializer,
clamp,
isFirefox,
promiseWithResolvers,
setInt24,
setUint24,
} from './misc';
import { Muxer } from './muxer'; import { Muxer } from './muxer';
import { SubtitleParser } from './subtitles'; import { SubtitleParser } from './subtitles';
import { toAlaw, toUlaw } from './pcm'; import { toAlaw, toUlaw } from './pcm';
@@ -248,12 +257,18 @@ class VideoEncoderWrapper {
canvasIsNew = true; canvasIsNew = true;
} }
const context = this.resizeCanvas.getContext('2d', { alpha: false }) as const context = this.resizeCanvas.getContext('2d', {
CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; alpha: isFirefox(), // Firefox has VideoFrame glitches with opaque canvases
}) as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
assert(context); assert(context);
if (!canvasIsNew) { if (!canvasIsNew) {
context.clearRect(0, 0, this.codedWidth, this.codedHeight); if (isFirefox()) {
context.fillStyle = 'black';
context.fillRect(0, 0, this.codedWidth, this.codedHeight);
} else {
context.clearRect(0, 0, this.codedWidth, this.codedHeight);
}
} }
videoSample.drawWithFit(context, { fit: sizeChangeBehavior }); videoSample.drawWithFit(context, { fit: sizeChangeBehavior });
+9
View File
@@ -642,6 +642,15 @@ export const isSafari = () => {
return result; return result;
}; };
let isFirefoxCache: boolean | null = null;
export const isFirefox = () => {
if (isFirefoxCache !== null) {
return isFirefoxCache;
}
return isFirefoxCache = typeof navigator !== 'undefined' && navigator.userAgent?.includes('Firefox');
};
/** /**
* T or a promise that resolves to T. * T or a promise that resolves to T.
* @group Miscellaneous * @group Miscellaneous
+11
View File
@@ -0,0 +1,11 @@
/*!
* 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/.
*/
// This file contains Node.js-specific code that does not run in a browser.
export * as fs from 'node:fs/promises';
+10 -5
View File
@@ -15,6 +15,7 @@ import {
toDataView, toDataView,
toUint8Array, toUint8Array,
SetRequired, SetRequired,
isFirefox,
} from './misc'; } from './misc';
/** /**
@@ -201,7 +202,8 @@ export class VideoSample {
return new VideoSample( return new VideoSample(
new VideoFrame(data, { new VideoFrame(data, {
timestamp: Math.trunc(init.timestamp! * SECOND_TO_MICROSECOND_FACTOR), timestamp: Math.trunc(init.timestamp! * SECOND_TO_MICROSECOND_FACTOR),
duration: Math.trunc((init.duration ?? 0) * SECOND_TO_MICROSECOND_FACTOR), // Drag 0 to undefined
duration: Math.trunc((init.duration ?? 0) * SECOND_TO_MICROSECOND_FACTOR) || undefined,
}), }),
init, init,
); );
@@ -227,7 +229,10 @@ export class VideoSample {
} }
const canvas = new OffscreenCanvas(width, height); const canvas = new OffscreenCanvas(width, height);
const context = canvas.getContext('2d', { alpha: false, willReadFrequently: true }); const context = canvas.getContext('2d', {
alpha: isFirefox(), // Firefox has VideoFrame glitches with opaque canvases
willReadFrequently: true,
});
assert(context); assert(context);
// Draw it to a canvas // Draw it to a canvas
@@ -342,7 +347,7 @@ export class VideoSample {
dest.set(this._data); dest.set(this._data);
} else { } else {
const canvas = this._data; const canvas = this._data;
const context = canvas.getContext('2d', { alpha: false }); const context = canvas.getContext('2d');
assert(context); assert(context);
const imageData = context.getImageData(0, 0, this.codedWidth, this.codedHeight); const imageData = context.getImageData(0, 0, this.codedWidth, this.codedHeight);
@@ -373,13 +378,13 @@ export class VideoSample {
codedWidth: this.codedWidth, codedWidth: this.codedWidth,
codedHeight: this.codedHeight, codedHeight: this.codedHeight,
timestamp: this.microsecondTimestamp, timestamp: this.microsecondTimestamp,
duration: this.microsecondDuration, duration: this.microsecondDuration || undefined,
colorSpace: this.colorSpace, colorSpace: this.colorSpace,
}); });
} else { } else {
return new VideoFrame(this._data, { return new VideoFrame(this._data, {
timestamp: this.microsecondTimestamp, timestamp: this.microsecondTimestamp,
duration: this.microsecondDuration, duration: this.microsecondDuration || undefined,
}); });
} }
} }
+6 -4
View File
@@ -18,6 +18,9 @@ import {
toDataView, toDataView,
toUint8Array, toUint8Array,
} from './misc'; } from './misc';
import * as nodeAlias from './node';
const node = nodeAlias; // Aliasing it prevents some bundler warnings
export type ReadResult = { export type ReadResult = {
bytes: Uint8Array; bytes: Uint8Array;
@@ -548,9 +551,7 @@ export class FilePathSource extends Source {
// Let's back this source with a StreamSource, makes the implementation very simple // Let's back this source with a StreamSource, makes the implementation very simple
this._streamSource = new StreamSource({ this._streamSource = new StreamSource({
getSize: async () => { getSize: async () => {
const FS_MODULE_NAME = 'node:fs/promises'; fileHandle = await node.fs.open(filePath, 'r');
const fs = await import(/* @vite-ignore */ FS_MODULE_NAME) as typeof import('node:fs/promises');
fileHandle = await fs.open(filePath, 'r');
const stats = await fileHandle.stat(); const stats = await fileHandle.stat();
return stats.size; return stats.size;
@@ -558,8 +559,9 @@ export class FilePathSource extends Source {
read: async (start, end) => { read: async (start, end) => {
assert(fileHandle); assert(fileHandle);
const buffer = Buffer.alloc(end - start); const buffer = new Uint8Array(end - start);
await fileHandle.read(buffer, 0, end - start, start); await fileHandle.read(buffer, 0, end - start, start);
return buffer; return buffer;
}, },
maxCacheSize: options.maxCacheSize, maxCacheSize: options.maxCacheSize,