mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-09 16:53:47 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b12468008 | ||
|
|
2277810853 | ||
|
|
19b1c015b6 |
+3
-3
@@ -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
|
||||||
|
|||||||
@@ -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 }],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -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>
|
||||||
|
|||||||
Generated
+2
-2
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user