Merge main into release for tag v1.25.3

This commit is contained in:
github-actions[bot]
2025-11-26 22:33:14 +00:00
12 changed files with 347 additions and 521 deletions
+9 -1
View File
@@ -433,4 +433,12 @@ await textSource.add('00:00:00.000 --> 00:00:02.000\nHello there!\n\n');
await textSource.add('00:00:02.500 --> 00:00:04.000\nChunky chunks.\n\n'); await textSource.add('00:00:02.500 --> 00:00:04.000\nChunky chunks.\n\n');
``` ```
The chunks have certain constraints: A cue must be fully contained within a chunk and cannot be split across multiple smaller chunks (although a chunk can contain multiple cues). Also, the WebVTT preamble must be added first and all at once. The chunks have certain constraints: A cue must be fully contained within a chunk and cannot be split across multiple smaller chunks (although a chunk can contain multiple cues). Also, the WebVTT preamble must be added first and all at once.
::: info
For QuickTime to display WebVTT subtitles, it typically expects alignment information to be specified:
```
00:00:00.000 --> 00:00:02.000 align:center
This is your last chance.
```
:::
+1 -1
View File
@@ -21,7 +21,7 @@ const context = canvas.getContext('2d', { alpha: false, desynchronized: true })!
const frameRate = 30; const frameRate = 30;
const chunks: Uint8Array[] = []; const chunks: Uint8Array<ArrayBuffer>[] = [];
let recording = false; let recording = false;
let output: Output; let output: Output;
let videoSource: CanvasSource; let videoSource: CanvasSource;
@@ -97,7 +97,7 @@ const extractMetadata = (resource: File | string) => {
'Lyrics': tags.lyrics, 'Lyrics': tags.lyrics,
'Comment': tags.comment, 'Comment': tags.comment,
'Images': tags.images?.map((image) => { 'Images': tags.images?.map((image) => {
const blob = new Blob([image.data], { type: image.mimeType }); const blob = new Blob([image.data.slice()], { type: image.mimeType });
const element = new Image(); const element = new Image();
element.src = URL.createObjectURL(blob); element.src = URL.createObjectURL(blob);
+285 -462
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.25.2", "version": "1.25.3",
"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": [
@@ -70,7 +70,7 @@
"@eslint/js": "^9.22.0", "@eslint/js": "^9.22.0",
"@fontsource-variable/rubik": "^5.2.6", "@fontsource-variable/rubik": "^5.2.6",
"@fontsource/dm-mono": "^5.2.6", "@fontsource/dm-mono": "^5.2.6",
"@microsoft/api-extractor": "^7.52.1", "@microsoft/api-extractor": "^7.55.1",
"@stylistic/eslint-plugin": "^4.2.0", "@stylistic/eslint-plugin": "^4.2.0",
"@tailwindcss/vite": "^4.1.7", "@tailwindcss/vite": "^4.1.7",
"@types/markdown-it-footnote": "^3.0.4", "@types/markdown-it-footnote": "^3.0.4",
@@ -84,8 +84,8 @@
"mermaid": "^11.6.0", "mermaid": "^11.6.0",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.7",
"tsx": "^4.19.4", "tsx": "^4.19.4",
"typescript": "^5.8.2", "typescript": "^5.9.3",
"typescript-eslint": "^8.26.1", "typescript-eslint": "^8.48.0",
"vite": "^6.3.5", "vite": "^6.3.5",
"vitepress": "^1.6.3", "vitepress": "^1.6.3",
"vitepress-plugin-llms": "^1.5.1", "vitepress-plugin-llms": "^1.5.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.25.2", "version": "1.25.3",
"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
@@ -850,7 +850,7 @@ export const dOps = (trackData: IsobmffAudioTrackData) => {
let inputSampleRate = trackData.info.sampleRate; let inputSampleRate = trackData.info.sampleRate;
let outputGain = 0; let outputGain = 0;
let channelMappingFamily = 0; let channelMappingFamily = 0;
let channelMappingTable = new Uint8Array(0); let channelMappingTable: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
// Read preskip and from codec private data from the encoder // Read preskip and from codec private data from the encoder
// https://www.rfc-editor.org/rfc/rfc7845#section-5 // https://www.rfc-editor.org/rfc/rfc7845#section-5
+7 -7
View File
@@ -157,20 +157,20 @@ export const writeBits = (bytes: Uint8Array, start: number, end: number, value:
export const toUint8Array = (source: AllowSharedBufferSource): Uint8Array => { export const toUint8Array = (source: AllowSharedBufferSource): Uint8Array => {
if (source.constructor === Uint8Array) { // We want a true Uint8Array, not something that extends it like Buffer if (source.constructor === Uint8Array) { // We want a true Uint8Array, not something that extends it like Buffer
return source; return source;
} else if (source instanceof ArrayBuffer) { } else if (ArrayBuffer.isView(source)) {
return new Uint8Array(source);
} else {
return new Uint8Array(source.buffer, source.byteOffset, source.byteLength); return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
} else {
return new Uint8Array(source);
} }
}; };
export const toDataView = (source: AllowSharedBufferSource) => { export const toDataView = (source: AllowSharedBufferSource): DataView => {
if (source.constructor === DataView) { if (source.constructor === DataView) {
return source; return source;
} else if (source instanceof ArrayBuffer) { } else if (ArrayBuffer.isView(source)) {
return new DataView(source);
} else {
return new DataView(source.buffer, source.byteOffset, source.byteLength); return new DataView(source.buffer, source.byteOffset, source.byteLength);
} else {
return new DataView(source);
} }
}; };
+3 -1
View File
@@ -1250,7 +1250,9 @@ export class AudioSample implements Disposable {
numberOfFrames: this.numberOfFrames, numberOfFrames: this.numberOfFrames,
numberOfChannels: this.numberOfChannels, numberOfChannels: this.numberOfChannels,
timestamp: this.microsecondTimestamp, timestamp: this.microsecondTimestamp,
data: this._data, data: this._data.buffer instanceof ArrayBuffer
? this._data.buffer
: this._data.slice(), // In the case of SharedArrayBuffer, convert to ArrayBuffer
}); });
} }
} }
+32 -39
View File
@@ -102,10 +102,17 @@ export class BufferSource extends Source {
/** @internal */ /** @internal */
_onreadCalled = false; _onreadCalled = false;
/** Creates a new {@link BufferSource} backed the specified `ArrayBuffer` or `ArrayBufferView`. */ /**
constructor(buffer: ArrayBuffer | ArrayBufferView) { * Creates a new {@link BufferSource} backed by the specified `ArrayBuffer`, `SharedArrayBuffer`,
if (!(buffer instanceof ArrayBuffer) && !ArrayBuffer.isView(buffer)) { * or `ArrayBufferView`.
throw new TypeError('buffer must be an ArrayBuffer or ArrayBufferView.'); */
constructor(buffer: AllowSharedBufferSource) {
if (
!(buffer instanceof ArrayBuffer)
&& !(typeof SharedArrayBuffer !== 'undefined' && buffer instanceof SharedArrayBuffer)
&& !ArrayBuffer.isView(buffer)
) {
throw new TypeError('buffer must be an ArrayBuffer, SharedArrayBuffer, or ArrayBufferView.');
} }
super(); super();
@@ -234,12 +241,7 @@ export class BlobSource extends Source {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) { if (done) {
this._orchestrator.forgetWorker(worker); this._orchestrator.forgetWorker(worker);
throw new Error('Blob reader stopped unexpectedly before all requested data was read.');
if (worker.currentPos < worker.targetPos) { // I think this `if` should always hit?
throw new Error('Blob reader stopped unexpectedly before all requested data was read.');
}
break;
} }
if (worker.aborted) { if (worker.aborted) {
@@ -441,7 +443,7 @@ export class UrlSource extends Source {
let fileSize: number; let fileSize: number;
if (response.status === 206) { if (response.status === 206) {
fileSize = this._getPartialLengthFromRangeResponse(response); fileSize = this._getTotalLengthFromRangeResponse(response);
worker = this._orchestrator.createWorker(0, Math.min(fileSize, URL_SOURCE_MIN_LOAD_AMOUNT)); worker = this._orchestrator.createWorker(0, Math.min(fileSize, URL_SOURCE_MIN_LOAD_AMOUNT));
} else { } else {
// Server probably returned a 200. // Server probably returned a 200.
@@ -515,14 +517,6 @@ export class UrlSource extends Source {
); );
} }
const length = this._getPartialLengthFromRangeResponse(response);
const required = worker.targetPos - worker.currentPos;
if (length < required) {
throw new Error(
`HTTP response unexpectedly too short: Needed at least ${required} bytes, got only ${length}.`,
);
}
if (!response.body) { if (!response.body) {
throw new Error( throw new Error(
'Missing HTTP response body stream. The used fetch function must provide the response body as a' 'Missing HTTP response body stream. The used fetch function must provide the response body as a'
@@ -568,16 +562,17 @@ export class UrlSource extends Source {
const { done, value } = readResult; const { done, value } = readResult;
if (done) { if (done) {
this._orchestrator.forgetWorker(worker); if (worker.currentPos >= worker.targetPos) {
// All data was delivered, we're good
if (worker.currentPos < worker.targetPos) { this._orchestrator.forgetWorker(worker);
throw new Error( worker.running = false;
'Response stream reader stopped unexpectedly before all requested data was read.', return;
);
} }
worker.running = false; // The response stopped early, before the target. This can happen if server decides to cap range
return; // requests arbitrarily, even if the request had an uncapped end. In this case, let's fetch the rest
// of the data using a new request.
break;
} }
this.onread?.(worker.currentPos, worker.currentPos + value.length); this.onread?.(worker.currentPos, worker.currentPos + value.length);
@@ -597,25 +592,23 @@ export class UrlSource extends Source {
} }
/** @internal */ /** @internal */
private _getPartialLengthFromRangeResponse(response: Response) { private _getTotalLengthFromRangeResponse(response: Response) {
const contentRange = response.headers.get('Content-Range'); const contentRange = response.headers.get('Content-Range');
if (contentRange) { if (contentRange) {
const match = /\/(\d+)/.exec(contentRange); const match = /\/(\d+)/.exec(contentRange);
if (match) { if (match) {
return Number(match[1]); return Number(match[1]);
} else {
throw new Error(`Invalid Content-Range header: ${contentRange}`);
} }
}
const contentLength = response.headers.get('Content-Length');
if (contentLength) {
return Number(contentLength);
} else { } else {
const contentLength = response.headers.get('Content-Length'); throw new Error(
if (contentLength) { 'Partial HTTP response (status 206) must surface either Content-Range or'
return Number(contentLength); + ' Content-Length header.',
} else { );
throw new Error(
'Partial HTTP response (status 206) must surface either Content-Range or'
+ ' Content-Length header.',
);
}
} }
} }
+1 -1
View File
@@ -62,7 +62,7 @@ export type StreamTargetChunk = {
/** The operation type. */ /** The operation type. */
type: 'write'; // This ensures automatic compatibility with FileSystemWritableFileStream type: 'write'; // This ensures automatic compatibility with FileSystemWritableFileStream
/** The data to write. */ /** The data to write. */
data: Uint8Array; data: Uint8Array<ArrayBuffer>;
/** The byte offset in the output file at which to write the data. */ /** The byte offset in the output file at which to write the data. */
position: number; position: number;
}; };
+2 -2
View File
@@ -190,7 +190,7 @@ const MAX_CHUNKS_AT_ONCE = 2;
interface Chunk { interface Chunk {
start: number; start: number;
written: ChunkSection[]; written: ChunkSection[];
data: Uint8Array; data: Uint8Array<ArrayBuffer>;
shouldFlush: boolean; shouldFlush: boolean;
} }
@@ -281,7 +281,7 @@ export class StreamTargetWriter extends Writer {
const chunks: { const chunks: {
start: number; start: number;
size: number; size: number;
data?: Uint8Array; data?: Uint8Array<ArrayBuffer>;
}[] = []; }[] = [];
const sorted = [...this.sections].sort((a, b) => a.start - b.start); const sorted = [...this.sections].sort((a, b) => a.start - b.start);