Compare commits

...
3 Commits
Author SHA1 Message Date
Vanilagy 4b12468008 Add support for reading and writing RF64 WAVE files 2025-07-18 16:48:23 +02:00
Vanilagy 2277810853 Add sponsor 2025-07-18 14:24:21 +02:00
Vanilagy 19b1c015b6 Add full URLs for OpenGraph image, add more graph tags 2025-07-18 09:25:24 +02:00
12 changed files with 135 additions and 25 deletions
+3 -3
View File
@@ -21,7 +21,7 @@
chunked: true, chunked: true,
chunkSize: 2**20 chunkSize: 2**20
}); });
const outputFormat = new Mediabunny.Mp4OutputFormat(); const outputFormat = new Mediabunny.WavOutputFormat({ large: true });
const button = document.createElement('button'); const button = document.createElement('button');
button.textContent = 'Cancel'; button.textContent = 'Cancel';
@@ -38,8 +38,8 @@
target target
}), }),
audio: { audio: {
codec: 'opus', //codec: 'opus',
bitrate: 128000, //bitrate: 128000,
//numberOfChannels: 1, //numberOfChannels: 1,
//sampleRate: 4000 //sampleRate: 4000
//discard: true //discard: true
+10 -3
View File
@@ -4,11 +4,13 @@ import tailwindcss from '@tailwindcss/vite';
import llmstxt from 'vitepress-plugin-llms'; import llmstxt from 'vitepress-plugin-llms';
import { HeadConfig } from 'vitepress'; import { HeadConfig } from 'vitepress';
const DESCRIPTION = 'A JavaScript library for reading, writing, and converting media files. Directly in the browser,'
+ ' and faster than anybunny else.';
// https://vitepress.dev/reference/site-config // https://vitepress.dev/reference/site-config
export default withMermaid({ export default withMermaid({
title: 'Mediabunny', title: 'Mediabunny',
description: 'A JavaScript library for reading, writing, and converting media files. Directly in the browser, and' description: DESCRIPTION,
+ ' faster than anybunny else.',
cleanUrls: true, cleanUrls: true,
head: [ head: [
['link', { rel: 'icon', type: 'image/png', href: '/mediabunny-logo.png' }], ['link', { rel: 'icon', type: 'image/png', href: '/mediabunny-logo.png' }],
@@ -16,9 +18,13 @@ export default withMermaid({
['meta', { property: 'og:type', content: 'website' }], ['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:site_name', content: 'Mediabunny' }], ['meta', { property: 'og:site_name', content: 'Mediabunny' }],
['meta', { property: 'og:url', content: 'https://mediabunny.dev/' }], ['meta', { property: 'og:url', content: 'https://mediabunny.dev/' }],
['meta', { property: 'og:image', content: '/mediabunny-og-image.png' }], ['meta', { property: 'og:image', content: 'https://mediabunny.dev/mediabunny-og-image.png' }],
['meta', { property: 'og:locale', content: 'en-US' }], ['meta', { property: 'og:locale', content: 'en-US' }],
['meta', { property: 'og:description', content: DESCRIPTION }],
['meta', { name: 'twitter:image', content: 'https://mediabunny.dev/mediabunny-og-image.png' }],
['meta', { name: 'twitter:card', content: 'summary_large_image' }], ['meta', { name: 'twitter:card', content: 'summary_large_image' }],
['meta', { name: 'twitter:site', content: '@vanilagy' }],
['meta', { name: 'twitter:description', content: DESCRIPTION }],
], ],
themeConfig: { themeConfig: {
logo: '/mediabunny-logo.svg', logo: '/mediabunny-logo.svg',
@@ -114,6 +120,7 @@ export default withMermaid({
((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push( ((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push(
['meta', { property: 'og:title', content: title }], ['meta', { property: 'og:title', content: title }],
['meta', { property: 'twitter:title', content: title }],
); );
}, },
}); });
+3
View File
@@ -227,8 +227,11 @@ const output = new Output({
The following options are available: The following options are available:
```ts ```ts
type WavOutputFormatOptions = { type WavOutputFormatOptions = {
large?: boolean;
onHeader?: (data: Uint8Array, position: number) => unknown; onHeader?: (data: Uint8Array, position: number) => unknown;
}; };
``` ```
- `large`\
When enabled, an RF64 file be written, allowing for file sizes to exceed 4 GiB, which is otherwise not possible for regular WAVE files.
- `onHeader`\ - `onHeader`\
Will be called once the file header is written. The header consists of the RIFF header, the format chunk, and the start of the data chunk (with a placeholder size of 0). Will be called once the file header is written. The header consists of the RIFF header, the format chunk, and the start of the data chunk (with a placeholder size of 0).
+1
View File
@@ -95,6 +95,7 @@ const sponsors = {
{ image: 'https://avatars.githubusercontent.com/u/84167135', name: 'Memenome', url: 'https://github.com/memenome' }, { image: 'https://avatars.githubusercontent.com/u/84167135', name: 'Memenome', url: 'https://github.com/memenome' },
{ image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' }, { image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' },
{ image: 'https://avatars.githubusercontent.com/u/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' }, { image: 'https://avatars.githubusercontent.com/u/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' },
{ image: 'https://avatars.githubusercontent.com/u/91711202', name: 'ihasq', url: 'https://github.com/ihasq' },
], ],
}; };
</script> </script>
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"version": "1.1.1", "version": "1.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mediabunny", "name": "mediabunny",
"version": "1.1.1", "version": "1.2.0",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-mediacapture-transform": "^0.1.11",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.1.1", "version": "1.2.0",
"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",
"main": "./dist/bundles/mediabunny.cjs", "main": "./dist/bundles/mediabunny.cjs",
+1 -1
View File
@@ -291,7 +291,7 @@ export class WaveInputFormat extends InputFormat {
const riffReader = new RiffReader(input._mainReader); const riffReader = new RiffReader(input._mainReader);
const riffType = riffReader.readAscii(4); const riffType = riffReader.readAscii(4);
if (riffType !== 'RIFF' && riffType !== 'RIFX') { if (riffType !== 'RIFF' && riffType !== 'RIFX' && riffType !== 'RF64') {
return false; return false;
} }
+9
View File
@@ -546,6 +546,12 @@ export class Mp3OutputFormat extends OutputFormat {
* @public * @public
*/ */
export type WavOutputFormatOptions = { export type WavOutputFormatOptions = {
/**
* When enabled, an RF64 file be written, allowing for file sizes to exceed 4 GiB, which is otherwise not possible
* for regular WAVE files.
*/
large?: boolean;
/** /**
* Will be called once the file header is written. The header consists of the RIFF header, the format chunk, and the * Will be called once the file header is written. The header consists of the RIFF header, the format chunk, and the
* start of the data chunk (with a placeholder size of 0). * start of the data chunk (with a placeholder size of 0).
@@ -565,6 +571,9 @@ export class WavOutputFormat extends OutputFormat {
if (!options || typeof options !== 'object') { if (!options || typeof options !== 'object') {
throw new TypeError('options must be an object.'); throw new TypeError('options must be an object.');
} }
if (options.large !== undefined && typeof options.large !== 'boolean') {
throw new TypeError('options.large, when provided, must be a boolean.');
}
if (options.onHeader !== undefined && typeof options.onHeader !== 'function') { if (options.onHeader !== undefined && typeof options.onHeader !== 'function') {
throw new TypeError('options.onHeader, when provided, must be a function.'); throw new TypeError('options.onHeader, when provided, must be a function.');
} }
+15
View File
@@ -35,6 +35,21 @@ export class RiffReader {
return view.getUint32(offset, this.littleEndian); return view.getUint32(offset, this.littleEndian);
} }
readU64() {
let low: number;
let high: number;
if (this.littleEndian) {
low = this.readU32();
high = this.readU32();
} else {
high = this.readU32();
low = this.readU32();
}
return high * 0x100000000 + low;
}
readAscii(length: number) { readAscii(length: number) {
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length);
this.pos += length; this.pos += length;
+9 -3
View File
@@ -14,14 +14,20 @@ export class RiffWriter {
constructor(private writer: Writer) {} constructor(private writer: Writer) {}
writeU16(value: number) {
this.helperView.setUint16(0, value, true);
this.writer.write(this.helper.subarray(0, 2));
}
writeU32(value: number) { writeU32(value: number) {
this.helperView.setUint32(0, value, true); this.helperView.setUint32(0, value, true);
this.writer.write(this.helper.subarray(0, 4)); this.writer.write(this.helper.subarray(0, 4));
} }
writeU16(value: number) { writeU64(value: number) {
this.helperView.setUint16(0, value, true); this.helperView.setUint32(0, value, true);
this.writer.write(this.helper.subarray(0, 2)); this.helperView.setUint32(4, Math.floor(value / 2 ** 32), true);
this.writer.write(this.helper);
} }
writeAscii(text: string) { writeAscii(text: string) {
+24 -3
View File
@@ -53,9 +53,13 @@ export class WaveDemuxer extends Demuxer {
const actualFileSize = await this.metadataReader.reader.source.getSize(); const actualFileSize = await this.metadataReader.reader.source.getSize();
const riffType = this.metadataReader.readAscii(4); const riffType = this.metadataReader.readAscii(4);
this.metadataReader.littleEndian = riffType === 'RIFF'; this.metadataReader.littleEndian = riffType !== 'RIFX';
const totalFileSize = Math.min(this.metadataReader.readU32() + 8, actualFileSize); const isRf64 = riffType === 'RF64';
const outerChunkSize = this.metadataReader.readU32();
let totalFileSize = isRf64 ? actualFileSize : Math.min(outerChunkSize + 8, actualFileSize);
const format = this.metadataReader.readAscii(4); const format = this.metadataReader.readAscii(4);
if (format !== 'WAVE') { if (format !== 'WAVE') {
@@ -63,6 +67,9 @@ export class WaveDemuxer extends Demuxer {
} }
this.metadataReader.pos = 12; this.metadataReader.pos = 12;
let chunksRead = 0;
let dataChunkSize: number | null = null;
while (this.metadataReader.pos < totalFileSize) { while (this.metadataReader.pos < totalFileSize) {
await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + 8); await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + 8);
@@ -70,14 +77,28 @@ export class WaveDemuxer extends Demuxer {
const chunkSize = this.metadataReader.readU32(); const chunkSize = this.metadataReader.readU32();
const startPos = this.metadataReader.pos; const startPos = this.metadataReader.pos;
if (isRf64 && chunksRead === 0 && chunkId !== 'ds64') {
throw new Error('Invalid RF64 file: First chunk must be "ds64".');
}
if (chunkId === 'fmt ') { if (chunkId === 'fmt ') {
await this.parseFmtChunk(chunkSize); await this.parseFmtChunk(chunkSize);
} else if (chunkId === 'data') { } else if (chunkId === 'data') {
dataChunkSize ??= chunkSize;
this.dataStart = this.metadataReader.pos; this.dataStart = this.metadataReader.pos;
this.dataSize = Math.min(chunkSize, totalFileSize - this.dataStart); this.dataSize = Math.min(dataChunkSize, totalFileSize - this.dataStart);
} else if (chunkId === 'ds64') {
// File and data chunk sizes are defined in here instead
const riffChunkSize = this.metadataReader.readU64();
dataChunkSize = this.metadataReader.readU64();
totalFileSize = Math.min(riffChunkSize + 8, actualFileSize);
} }
this.metadataReader.pos = startPos + chunkSize + (chunkSize & 1); // Handle padding this.metadataReader.pos = startPos + chunkSize + (chunkSize & 1); // Handle padding
chunksRead++;
} }
if (!this.audioInfo) { if (!this.audioInfo) {
+50 -2
View File
@@ -18,10 +18,13 @@ import { assert } from '../misc';
export class WaveMuxer extends Muxer { export class WaveMuxer extends Muxer {
private format: WavOutputFormat; private format: WavOutputFormat;
private isRf64: boolean;
private writer: Writer; private writer: Writer;
private riffWriter: RiffWriter; private riffWriter: RiffWriter;
private headerWritten = false; private headerWritten = false;
private dataSize = 0; private dataSize = 0;
private sampleRate: number | null = null;
private sampleCount = 0;
constructor(output: Output, format: WavOutputFormat) { constructor(output: Output, format: WavOutputFormat) {
super(output); super(output);
@@ -29,6 +32,7 @@ export class WaveMuxer extends Muxer {
this.format = format; this.format = format;
this.writer = output._writer; this.writer = output._writer;
this.riffWriter = new RiffWriter(output._writer); this.riffWriter = new RiffWriter(output._writer);
this.isRf64 = !!format._options.large;
} }
async start() { async start() {
@@ -58,13 +62,22 @@ export class WaveMuxer extends Muxer {
assert(meta.decoderConfig); assert(meta.decoderConfig);
this.writeHeader(track, meta.decoderConfig); this.writeHeader(track, meta.decoderConfig);
this.sampleRate = meta.decoderConfig.sampleRate;
this.headerWritten = true; this.headerWritten = true;
} }
this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key'); this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
if (!this.isRf64 && this.writer.getPos() + packet.data.byteLength >= 2 ** 32) {
throw new Error(
'Adding more audio data would exceed the maximum RIFF size of 4 GiB. To write larger files, use'
+ ' RF64 by setting `large: true` in the WavOutputFormatOptions.',
);
}
this.writer.write(packet.data); this.writer.write(packet.data);
this.dataSize += packet.data.byteLength; this.dataSize += packet.data.byteLength;
this.sampleCount += Math.round(packet.duration * this.sampleRate!);
await this.writer.flush(); await this.writer.flush();
} finally { } finally {
@@ -101,10 +114,26 @@ export class WaveMuxer extends Muxer {
const blockSize = pcmInfo.sampleSize * channels; const blockSize = pcmInfo.sampleSize * channels;
// RIFF header // RIFF header
this.riffWriter.writeAscii('RIFF'); this.riffWriter.writeAscii(this.isRf64 ? 'RF64' : 'RIFF');
if (this.isRf64) {
this.riffWriter.writeU32(0xffffffff); // Not used in RF64
} else {
this.riffWriter.writeU32(0); // File size placeholder this.riffWriter.writeU32(0); // File size placeholder
}
this.riffWriter.writeAscii('WAVE'); this.riffWriter.writeAscii('WAVE');
if (this.isRf64) {
this.riffWriter.writeAscii('ds64');
this.riffWriter.writeU32(28); // Chunk size
this.riffWriter.writeU64(0); // RIFF size placeholder
this.riffWriter.writeU64(0); // Data size placeholder
this.riffWriter.writeU64(0); // Sample count placeholder
this.riffWriter.writeU32(0); // Table length
// Empty table
}
// fmt chunk // fmt chunk
this.riffWriter.writeAscii('fmt '); this.riffWriter.writeAscii('fmt ');
this.riffWriter.writeU32(16); // Chunk size this.riffWriter.writeU32(16); // Chunk size
@@ -117,7 +146,12 @@ export class WaveMuxer extends Muxer {
// data chunk // data chunk
this.riffWriter.writeAscii('data'); this.riffWriter.writeAscii('data');
if (this.isRf64) {
this.riffWriter.writeU32(0xffffffff); // Not used in RF64
} else {
this.riffWriter.writeU32(0); // Data size placeholder this.riffWriter.writeU32(0); // Data size placeholder
}
if (this.format._options.onHeader) { if (this.format._options.onHeader) {
const { data, start } = this.writer.stopTrackingWrites(); const { data, start } = this.writer.stopTrackingWrites();
@@ -130,13 +164,27 @@ export class WaveMuxer extends Muxer {
const endPos = this.writer.getPos(); const endPos = this.writer.getPos();
if (this.isRf64) {
// Write riff size
this.writer.seek(20);
this.riffWriter.writeU64(endPos - 8);
// Write data size
this.writer.seek(28);
this.riffWriter.writeU64(this.dataSize);
// Write sample count
this.writer.seek(36);
this.riffWriter.writeU64(this.sampleCount);
} else {
// Write file size // Write file size
this.writer.seek(4); this.writer.seek(4);
this.riffWriter.writeU32(this.dataSize + 36); // File size - 8 this.riffWriter.writeU32(endPos - 8);
// Write data chunk size // Write data chunk size
this.writer.seek(40); this.writer.seek(40);
this.riffWriter.writeU32(this.dataSize); this.riffWriter.writeU32(this.dataSize);
}
this.writer.seek(endPos); this.writer.seek(endPos);