Compare commits

...
8 Commits
Author SHA1 Message Date
Vanilagy 605c258029 Bump patch 2025-09-04 11:14:05 +02:00
Vanilagy 4860a4bfe7 Probe encoder support with sample (fixes #90), improve encoder error handling 2025-09-04 11:11:04 +02:00
Vanilagy 68a6202269 Small fixup 2025-09-04 09:58:04 +02:00
Vanilagy 3a4153f35b Add Discord badge 2025-09-04 09:48:04 +02:00
Vanilagy 03a9073c40 Fix API generator 2025-09-04 09:38:56 +02:00
AllwhyandGitHub f8d236888a Rename mediarobot to mechabunny (#92) 2025-09-04 09:24:33 +02:00
Vanilagy a00386d402 What a mess 2025-09-03 21:30:38 +02:00
github-actions[bot] 69fc996c17 Merge main into release for tag v1.13.2 2025-09-03 19:27:57 +00:00
12 changed files with 127 additions and 66 deletions
+1
View File
@@ -3,6 +3,7 @@
[![](https://img.shields.io/npm/v/mediabunny)](https://www.npmjs.com/package/mediabunny)
[![](https://img.shields.io/bundlephobia/minzip/mediabunny)](https://bundlephobia.com/package/mediabunny)
[![](https://img.shields.io/npm/dm/mediabunny)](https://www.npmjs.com/package/mediabunny)
[![](https://img.shields.io/discord/1390044844285497344?logo=discord&label=Discord)](https://discord.gg/hmpkyYuS4U)
<div align="center">
<img src="./docs/public/mediabunny-logo.svg" width="180" height="180">
+6 -7
View File
@@ -18,15 +18,13 @@
const file = fileInput.files[0];
const source = new Mediabunny.BlobSource(file);
const target = new Mediabunny.NullTarget() ?? new Mediabunny.BufferTarget() ?? new Mediabunny.StreamTarget(new WritableStream({
const target = new Mediabunny.BufferTarget() ?? new Mediabunny.StreamTarget(new WritableStream({
write: console.log
}), {
chunked: true,
chunkSize: 2**20
});
const outputFormat = new Mediabunny.Mp4OutputFormat({
onMoov: console.log
});
const outputFormat = new Mediabunny.Mp4OutputFormat({});
const button = document.createElement('button');
button.textContent = 'Cancel';
@@ -76,7 +74,8 @@
},
*/
video: () => ({
//codec: 'avc',
forceTranscode: true,
codec: 'avc',
//fit: 'contain',
//frameRate: 27.123,
//width: 320,
@@ -98,8 +97,8 @@
//height: 100,
}),
trim: {
//start: 0,
//end: 20
start: 0,
end: 5
},
});
console.log(conversion);

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

+2 -2
View File
@@ -3,11 +3,11 @@
<div class="grid place-items-center my-14">
<div class="relative">
<div class="absolute top-0 left-0 size-full rounded-full" style="background: var(--vp-home-hero-image-background-image); filter: var(--vp-home-hero-image-filter)"></div>
<img src="./assets/mediarobot.svg" width="250" class="relative">
<img src="./assets/mechabunny.svg" width="250" class="relative">
</div>
</div>
While Mediabunny is proudly human-coded, we want to encourage any and all usage of Mediabunny, even when the vibes are high.
While Mediabunny is proudly human-generated, we want to encourage any and all usage of Mediabunny, even when the vibes are high.
Mediabunny is still new and is unlikely to be in the training data of modern LLMs, but we can still make the AI perform extremely well by just giving it a little more context.
+1 -2
View File
@@ -2,7 +2,6 @@ import {
ALL_FORMATS,
AudioBufferSink,
BlobSource,
BufferSource,
CanvasSink,
Input,
UrlSource,
@@ -92,7 +91,7 @@ const initMediaPlayer = async (resource: File | string) => {
// Create an Input from the resource
const source = resource instanceof File
? new BufferSource(await resource.arrayBuffer())// new BlobSource(resource)
? new BlobSource(resource)
: new UrlSource(resource);
const input = new Input({
source,
+6 -6
View File
@@ -1,12 +1,12 @@
{
"name": "mediabunny",
"version": "1.13.2",
"version": "1.13.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
"version": "1.13.2",
"version": "1.13.3",
"license": "MPL-2.0",
"workspaces": [
"packages/*"
@@ -7749,9 +7749,9 @@
}
},
"node_modules/mediabunny": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.13.1.tgz",
"integrity": "sha512-Fht/ujfoS0jt8s9W4zDEilnWVsafkBaiIQ5BX0x1tdvM4PClexgYIWDZSf6kC82/c9Bo3ehZPIIfS3aJ9younA==",
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.13.2.tgz",
"integrity": "sha512-quUwxiyA76+iAh2REQAFifHNthgVqzET37ys6v+GC8ljJpOXuP/g/XNsITc+/6r18gGuYQyF/9C4T/CeiTf7gA==",
"license": "MPL-2.0",
"peer": true,
"workspaces": [
@@ -12242,7 +12242,7 @@
},
"packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder",
"version": "1.13.2",
"version": "1.13.3",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mediabunny",
"author": "Vanilagy",
"version": "1.13.2",
"version": "1.13.3",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/mp3-encoder",
"author": "Vanilagy",
"version": "1.13.2",
"version": "1.13.3",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+49 -29
View File
@@ -785,47 +785,67 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
const constructorBlocks: string[] = [];
const allReferencedTypes: string[] = [];
// First, process constructor parameters with visibility modifiers as properties from any constructor
// that has them (typically the implementation or the main signature)
let processedProperties = false;
constructorOverloads.forEach((ctor) => {
// Skip the implementation (the one with a body) unless it's the only constructor
if (ctor.body && constructorOverloads.length > 1) {
// Process constructor parameters with visibility modifiers as properties (only from implementation)
ctor.parameters.forEach((param) => {
// Check if parameter has visibility modifier (public, private, protected, readonly)
const hasVisibilityModifier = param.modifiers?.some(mod =>
// Process constructor parameters with visibility modifiers as properties (only once)
if (!processedProperties) {
const hasVisibilityParams = ctor.parameters.some(param =>
param.modifiers?.some(mod =>
mod.kind === ts.SyntaxKind.PublicKeyword
|| mod.kind === ts.SyntaxKind.PrivateKeyword
|| mod.kind === ts.SyntaxKind.ProtectedKeyword
|| mod.kind === ts.SyntaxKind.ReadonlyKeyword,
);
),
);
if (!hasVisibilityModifier) return;
if (hasVisibilityParams) {
ctor.parameters.forEach((param) => {
// Check if parameter has visibility modifier (public, private, protected, readonly)
const hasVisibilityModifier = param.modifiers?.some(mod =>
mod.kind === ts.SyntaxKind.PublicKeyword
|| mod.kind === ts.SyntaxKind.PrivateKeyword
|| mod.kind === ts.SyntaxKind.ProtectedKeyword
|| mod.kind === ts.SyntaxKind.ReadonlyKeyword,
);
// Skip private parameters
const isPrivate = param.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword);
if (isPrivate) return;
if (!hasVisibilityModifier) return;
const paramName = param.name.getText();
const hasQuestionToken = param.questionToken !== undefined;
const isReadonly = param.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ReadonlyKeyword);
const rawParamType = param.type ? param.type.getText() : typeChecker.typeToString(typeChecker.getTypeAtLocation(param));
const paramType = cleanOptionalType(rawParamType, hasQuestionToken);
// Skip private parameters
const isPrivate = param.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword);
if (isPrivate) return;
const propertyDef = `${isReadonly ? 'readonly ' : ''}${paramName}${hasQuestionToken ? '?' : ''}: ${paramType};`;
const paramName = param.name.getText();
const hasQuestionToken = param.questionToken !== undefined;
const isReadonly = param.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ReadonlyKeyword);
const rawParamType = param.type ? param.type.getText() : typeChecker.typeToString(typeChecker.getTypeAtLocation(param));
const paramType = cleanOptionalType(rawParamType, hasQuestionToken);
// Get description from JSDoc comment on the parameter
const paramJsDoc = ts.getJSDocCommentsAndTags(param)[0];
let desc = '';
if (paramJsDoc && ts.isJSDoc(paramJsDoc) && typeof paramJsDoc.comment === 'string') {
desc = processLinkTags(paramJsDoc.comment.trim(), className);
}
const propertyDef = `${isReadonly ? 'readonly ' : ''}${paramName}${hasQuestionToken ? '?' : ''}: ${paramType};`;
// Find referenced types
const references = filterToExportedTypes(findAllTypeReferences(paramType), className);
const referencesText = formatReferences(references);
// Get description from JSDoc comment on the parameter
const paramJsDoc = ts.getJSDocCommentsAndTags(param)[0];
let desc = '';
if (paramJsDoc && ts.isJSDoc(paramJsDoc) && typeof paramJsDoc.comment === 'string') {
desc = processLinkTags(paramJsDoc.comment.trim(), className);
}
const propertyContent = `### \`${paramName}\`\n\n\`\`\`ts\n${propertyDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`;
properties.push(propertyContent);
});
// Find referenced types
const references = filterToExportedTypes(findAllTypeReferences(paramType), className);
const referencesText = formatReferences(references);
const propertyContent = `### \`${paramName}\`\n\n\`\`\`ts\n${propertyDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`;
properties.push(propertyContent);
});
processedProperties = true;
}
}
});
constructorOverloads.forEach((ctor) => {
// Skip the implementation (the one with a body) unless it's the only constructor
if (ctor.body && constructorOverloads.length > 1) {
return;
}
+42 -8
View File
@@ -46,7 +46,9 @@ import {
Rotation,
} from './misc';
import { Output, TrackType } from './output';
import { Mp4OutputFormat } from './output-format';
import { AudioSample, VideoSample } from './sample';
import { NullTarget } from './target';
/**
* The options for media file conversion.
@@ -582,7 +584,7 @@ export class Conversion {
|| this._startTimestamp > 0
|| firstTimestamp < 0
|| !!trackOptions.frameRate;
const needsRerender = width !== originalWidth
let needsRerender = width !== originalWidth
|| height !== originalHeight
|| (totalRotation !== 0 && !outputSupportsRotation);
@@ -662,6 +664,43 @@ export class Conversion {
const source = new VideoSampleSource(encodingConfig);
videoSource = source;
if (!needsRerender) {
// If we're directly passing decoded samples back to the encoder, sometimes the encoder may error due
// to lack of support of certain video frame formats, like when HDR is at play. To check for this, we
// first try to pass a single frame to the encoder to see how it behaves. If it throws, we then fall
// back to the rerender path.
//
// Creating a new temporary Output is sort of hacky, but due to a lack of an isolated encoder API right
// now, this is the simplest way. Will refactor in the future!
const tempOutput = new Output({
format: new Mp4OutputFormat(), // Supports all video codecs
target: new NullTarget(),
});
const tempSource = new VideoSampleSource(encodingConfig);
tempOutput.addVideoTrack(tempSource);
await tempOutput.start();
const sink = new VideoSampleSink(track);
const firstSample = await sink.getSample(this._startTimestamp);
if (firstSample) {
try {
await tempSource.add(firstSample);
firstSample.close();
await tempOutput.finalize();
} catch (error) {
console.info('Error when probing encoder support. Falling back to rerender path.', error);
needsRerender = true;
void tempOutput.cancel();
}
} else {
await tempOutput.cancel();
}
}
if (needsRerender) {
this._trackPromises.push((async () => {
await this._started;
@@ -1084,13 +1123,8 @@ export class Conversion {
this._maxTimestamps.set(trackId, Math.max(endTimestamp, this._maxTimestamps.get(trackId) ?? -Infinity));
let totalTimestamps = 0;
for (const [, timestamp] of this._maxTimestamps) {
totalTimestamps += timestamp;
}
const averageTimestamp = totalTimestamps / this._totalTrackCount;
const newProgress = clamp(averageTimestamp / this._totalDuration, 0, 1);
const minTimestamp = Math.min(...this._maxTimestamps.values());
const newProgress = clamp(minTimestamp / this._totalDuration, 0, 1);
if (newProgress !== this._lastProgress) {
this._lastProgress = newProgress;
+16 -8
View File
@@ -353,6 +353,7 @@ class VideoEncoderWrapper {
return;
}
const encoderError = new Error();
return this.ensureEncoderPromise = (async () => {
const encoderConfig = buildVideoEncoderConfig({
width: videoSample.codedWidth,
@@ -411,7 +412,7 @@ class VideoEncoderWrapper {
void this.muxer!.addEncodedVideoPacket(this.source._connectedTrack!, packet, meta);
},
error: (error) => {
error.stack = new Error().stack; // Provide a more useful stack trace
error.stack = encoderError.stack; // Provide a more useful stack trace
this.encoderError ??= error;
},
});
@@ -426,7 +427,7 @@ class VideoEncoderWrapper {
}
async flushAndClose(forceClose: boolean) {
this.checkForEncoderError();
if (!forceClose) this.checkForEncoderError();
if (this.customEncoder) {
if (!forceClose) {
@@ -439,10 +440,12 @@ class VideoEncoderWrapper {
await this.encoder.flush();
}
this.encoder.close();
if (this.encoder.state !== 'closed') {
this.encoder.close();
}
}
this.checkForEncoderError();
if (!forceClose) this.checkForEncoderError();
}
getQueueSize() {
@@ -455,6 +458,7 @@ class VideoEncoderWrapper {
checkForEncoderError() {
if (this.encoderError) {
this.encoderError.stack = new Error().stack; // Provide an even more useful stack trace
throw this.encoderError;
}
}
@@ -990,6 +994,7 @@ class AudioEncoderWrapper {
return;
}
const encoderError = new Error();
return this.ensureEncoderPromise = (async () => {
const { numberOfChannels, sampleRate } = audioSample;
@@ -1050,7 +1055,7 @@ class AudioEncoderWrapper {
void this.muxer!.addEncodedAudioPacket(this.source._connectedTrack!, packet, meta);
},
error: (error) => {
error.stack = new Error().stack; // Provide a more useful stack trace
error.stack = encoderError.stack; // Provide a more useful stack trace
this.encoderError ??= error;
},
});
@@ -1158,7 +1163,7 @@ class AudioEncoderWrapper {
}
async flushAndClose(forceClose: boolean) {
this.checkForEncoderError();
if (!forceClose) this.checkForEncoderError();
if (this.customEncoder) {
if (!forceClose) {
@@ -1171,10 +1176,12 @@ class AudioEncoderWrapper {
await this.encoder.flush();
}
this.encoder.close();
if (this.encoder.state !== 'closed') {
this.encoder.close();
}
}
this.checkForEncoderError();
if (!forceClose) this.checkForEncoderError();
}
getQueueSize() {
@@ -1189,6 +1196,7 @@ class AudioEncoderWrapper {
checkForEncoderError() {
if (this.encoderError) {
this.encoderError.stack = new Error().stack; // Provide an even more useful stack trace
throw this.encoderError;
}
}
+2 -2
View File
@@ -114,11 +114,11 @@ export class VideoSample {
* in `init`.
*/
constructor(
data: BufferSource,
data: AllowSharedBufferSource,
init: SetRequired<VideoSampleInit, 'format' | 'codedWidth' | 'codedHeight' | 'timestamp'>
);
constructor(
data: VideoFrame | CanvasImageSource | BufferSource,
data: VideoFrame | CanvasImageSource | AllowSharedBufferSource,
init?: VideoSampleInit,
) {
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {