Compare commits

...
4 Commits
9 changed files with 273 additions and 48 deletions
+5 -4
View File
@@ -24,7 +24,7 @@
chunked: true,
chunkSize: 2**20
});
const outputFormat = new Mediabunny.WavOutputFormat({});
const outputFormat = new Mediabunny.Mp4OutputFormat({});
const button = document.createElement('button');
button.textContent = 'Cancel';
@@ -47,7 +47,7 @@
format: outputFormat,
target
});
output.setMetadata({
output.setMetadataTags({
title: 'Bigggy',
artist: 'Buck Bunny',
images: [{
@@ -105,7 +105,8 @@
},
*/
video: () => ({
discard: true,
//discard: true,
forceTranscode: true,
//codec: 'avc',
//fit: 'contain',
//frameRate: 27.123,
@@ -128,7 +129,7 @@
//height: 100,
}),
trim: {
start: 0,
start: 1,
end: 10
},
});
+14 -4
View File
@@ -14,12 +14,22 @@
source: new Mediabunny.BlobSource(file),
});
const audioTrack = await input.getPrimaryAudioTrack();
const sink = new Mediabunny.EncodedPacketSink(audioTrack);
const videoTrack = await input.getPrimaryVideoTrack();
const sink = new Mediabunny.VideoSampleSink(videoTrack);
console.log(await audioTrack.getDecoderConfig())
console.log(await sink.getFirstPacket());
console.log(await sink.getSample(await videoTrack.getFirstTimestamp()))
/*
for await (const sample of sink.samples(0.99)) {
console.log(sample);
if (sample.timestamp >= 2) {
break;
}
}
*/
//console.log(await sink.getSample(1))
/*
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
+1 -1
View File
@@ -57,7 +57,7 @@
@reference "../base.css";
ul {
@apply py-1 px-4 bg-gray-500/10 rounded-lg;
@apply py-1 px-4 bg-gray-500/10 rounded-lg max-w-lg;
}
b {
@@ -80,6 +80,37 @@ const extractMetadata = (resource: File | string) => {
: {}
),
}))),
'Metadata tags': input.getMetadataTags().then((tags) => {
const result = {
'Title': tags.title,
'Description': tags.description,
'Artist': tags.artist,
'Album': tags.album,
'Album artist': tags.albumArtist,
'Track number': tags.trackNumber,
'Tracks total': tags.tracksTotal,
'Disc number': tags.discNumber,
'Discs total': tags.discsTotal,
'Genre': tags.genre,
'Date': tags.date?.toISOString().slice(0, 10),
'Lyrics': tags.lyrics,
'Comment': tags.comment,
'Images': tags.images?.map((image) => {
const blob = new Blob([image.data], { type: image.mimeType });
const element = new Image();
element.src = URL.createObjectURL(blob);
return element;
}),
'Raw tag count': tags.raw && Object.keys(tags.raw).length,
};
if (Object.values(result).some(x => x !== undefined)) {
return result;
} else {
return undefined;
}
}),
};
fileNameElement.textContent = resource instanceof File ? resource.name : resource;
@@ -93,7 +124,9 @@ const extractMetadata = (resource: File | string) => {
// Creates an HTML element to display any given value
const renderValue = (value: unknown) => {
if (Array.isArray(value)) {
if (value instanceof HTMLElement) {
return value;
} else if (Array.isArray(value)) {
const arrayAsObject: Record<string, unknown> = Object.fromEntries(
value.map((item, index) => [(index + 1).toString(), item]),
);
@@ -116,6 +149,7 @@ const renderObject = (object: Record<string, unknown>) => {
for (const key of keys) {
const value = object[key];
const listItem = document.createElement('li');
listItem.style.wordBreak = 'break-word';
const keySpan = document.createElement('b');
keySpan.textContent = `${key}: `;
@@ -132,6 +166,10 @@ const renderObject = (object: Record<string, unknown>) => {
// Replace the loading text with the resolved value
listItem.removeChild(loadingSpan);
listItem.appendChild(renderValue(resolvedValue));
if (resolvedValue === undefined) {
listElement.removeChild(listItem);
}
}).catch((error) => {
console.error(error);
@@ -146,7 +184,9 @@ const renderObject = (object: Record<string, unknown>) => {
listItem.appendChild(renderValue(value));
}
listElement.appendChild(listItem);
if (value !== undefined) {
listElement.appendChild(listItem);
}
}
return listElement;
+6 -6
View File
@@ -1,12 +1,12 @@
{
"name": "mediabunny",
"version": "1.14.0",
"version": "1.14.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
"version": "1.14.0",
"version": "1.14.2",
"license": "MPL-2.0",
"workspaces": [
"packages/*"
@@ -7749,9 +7749,9 @@
}
},
"node_modules/mediabunny": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.13.3.tgz",
"integrity": "sha512-kV12CVwMdhMQILGrVcyDWOgnZHwAytStQYKZeEaUj32XamYhlDX0Od4SkXVr0Yms8iapJsiCoEQdSK4UKUfooA==",
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.14.1.tgz",
"integrity": "sha512-TjLg8GQGGsnGePcA6i0NItGe9a5G8WaleCGaXpb5V5okwuK5KpNKfcmTZXItnjfPLo7FvfEZI0NFp1lIR8Os7Q==",
"license": "MPL-2.0",
"peer": true,
"workspaces": [
@@ -12242,7 +12242,7 @@
},
"packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder",
"version": "1.14.0",
"version": "1.14.2",
"license": "MPL-2.0",
"devDependencies": {
"@types/emscripten": "^1.40.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mediabunny",
"author": "Vanilagy",
"version": "1.14.0",
"version": "1.14.2",
"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.14.0",
"version": "1.14.2",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+192 -26
View File
@@ -1,6 +1,6 @@
// This script has been 100% vibe-coded with Claude, meaning I literally haven't looked at any of the code. It's
// probably a mess, but it solves a one-off problem where only the output matters, and the output is indeed good, which
// is the point of a custom script for this: full, precise control.
// This script has been 100% vibe-coded with Claude (and Gemini!), meaning I literally haven't looked at any of the
// code. It's probably a mess, but it solves a one-off problem where only the output matters, and the output is indeed
// good, which is the point of a custom script for this: full, precise control.
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unused-vars */
@@ -386,7 +386,34 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
}
});
// Phase 2: Generate documentation for each symbol
// Data structures for "Used by" feature
const usedByReferences = new Map<string, Set<{ user: string; context: string; type: 'constructor' | 'method' | 'property' | 'extends' | 'type_param' | 'type_alias' | 'variable' | 'function' }>>();
const generatedDocs = new Map<string, string>();
const addUsage = (
used: string,
user: string,
context: string,
type: 'constructor' | 'method' | 'property' | 'extends' | 'type_param' | 'type_alias' | 'variable' | 'function',
) => {
// No self-references
if (used === user) return;
if (!usedByReferences.has(used)) {
usedByReferences.set(used, new Set());
}
const usageSet = usedByReferences.get(used)!;
// Check for duplicates before adding
for (const item of usageSet) {
if (item.user === user && item.context === context && item.type === type) {
return;
}
}
usageSet.add({ user, context, type });
};
// Phase 2: Generate documentation for each symbol (and collect usage data)
allSymbols.forEach((exportSymbol) => {
const declaration = exportSymbol.valueDeclaration || exportSymbol.declarations?.[0];
if (!declaration) return;
@@ -492,9 +519,10 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
const allReferences = filterToExportedTypes([...new Set(allTypeStrings.flatMap(findAllTypeReferences))], variableName);
markdown += formatReferences(allReferences);
const outputPath = path.join(outputDir, `${variableName}.md`);
fs.writeFileSync(outputPath, markdown);
console.log(`Generated: ${outputPath}`);
// In tandem: update usage map
allReferences.forEach(ref => addUsage(ref, variableName, variableName, 'function'));
generatedDocs.set(variableName, markdown);
}
} else {
// Handle regular variables
@@ -507,9 +535,10 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
const references = filterToExportedTypes(findAllTypeReferences(variableValue), variableName);
markdown += formatReferences(references);
const outputPath = path.join(outputDir, `${variableName}.md`);
fs.writeFileSync(outputPath, markdown);
console.log(`Generated: ${outputPath}`);
// In tandem: update usage map
references.forEach(ref => addUsage(ref, variableName, variableName, 'variable'));
generatedDocs.set(variableName, markdown);
}
return;
@@ -643,6 +672,9 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
if (typeParamReferencesText) {
typeParameters += typeParamReferencesText;
}
// In tandem: update usage map
typeParamReferences.forEach(ref => addUsage(ref, className, className, 'type_param'));
}
// Check for extends clause (only for classes/interfaces)
@@ -653,6 +685,11 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
if (extendsClauseNode && extendsClauseNode.types[0]) {
const superClassName = extendsClauseNode.types[0].expression.getText();
extendsClause = `\n\n**Extends:** [\`${superClassName}\`](./${superClassName}.md)\n`;
// In tandem: update usage map
if (exportedTypes.has(superClassName)) {
addUsage(superClassName, className, className, 'extends');
}
}
}
@@ -835,6 +872,9 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
const references = filterToExportedTypes(findAllTypeReferences(paramType), className);
const referencesText = formatReferences(references);
// In tandem: update usage map
references.forEach(ref => addUsage(ref, className, paramName, 'property'));
const propertyContent = `### \`${paramName}\`\n\n\`\`\`ts\n${propertyDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`;
properties.push(propertyContent);
});
@@ -905,6 +945,9 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
const overloadReferences = filterToExportedTypes([...new Set(ctorTypeStrings.flatMap(findAllTypeReferences))], className);
constructorBlock += formatReferences(overloadReferences, linkedTypes);
// In tandem: update usage map
overloadReferences.forEach(ref => addUsage(ref, className, className, 'constructor'));
constructorBlocks.push(constructorBlock);
});
@@ -950,6 +993,9 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
const references = filterToExportedTypes(findAllTypeReferences(type), className);
const referencesText = formatReferences(references, linkedTypes);
// In tandem: update usage map
references.forEach(ref => addUsage(ref, className, name, 'property'));
// Check if this is an event handler (starts with "on" and can be a function)
const isEventHandler = name.startsWith('on') && (
type.includes('=>')
@@ -993,6 +1039,9 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
const references = filterToExportedTypes(findAllTypeReferences(type), className);
const referencesText = formatReferences(references, linkedTypes);
// In tandem: update usage map
references.forEach(ref => addUsage(ref, className, name, 'property'));
const inheritedBadge = '';
properties.push(`### \`${name}\`${inheritedBadge}\n\n\`\`\`ts\n${accessorDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`);
} else if (ts.isSetAccessorDeclaration(member) && member.name) {
@@ -1013,6 +1062,9 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
const references = filterToExportedTypes(findAllTypeReferences(paramType), className);
const referencesText = formatReferences(references, linkedTypes);
// In tandem: update usage map
references.forEach(ref => addUsage(ref, className, name, 'property'));
const inheritedBadge = '';
properties.push(`### \`${name}\`${inheritedBadge}\n\n\`\`\`ts\n${accessorDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`);
} else if ((ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) && member.name) {
@@ -1106,6 +1158,9 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
const allReferences = filterToExportedTypes([...new Set(allTypeStrings.flatMap(findAllTypeReferences))], className);
methodContent += formatReferences(allReferences, linkedTypes);
// In tandem: update usage map
allReferences.forEach(ref => addUsage(ref, className, name, 'method'));
if (isStatic) {
staticMethods.push(methodContent);
} else {
@@ -1174,6 +1229,9 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
const references = filterToExportedTypes(findAllTypeReferences(cleanedType), className);
const referencesText = formatReferences(references);
// In tandem: update usage map
references.forEach(ref => addUsage(ref, className, propName, 'property'));
// Check if this is an event handler (starts with "on" and can be a function)
const isEventHandler = propName.startsWith('on') && (
cleanedType.includes('=>')
@@ -1252,19 +1310,39 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
// Add subclasses section for classes that have subclasses
if (ts.isClassDeclaration(declaration) && classHierarchy.has(className)) {
const subclasses = classHierarchy.get(className)!;
// Sort by definition order instead of alphabetically
subclasses.sort((a, b) => {
const orderA = symbolOrderMap.get(a);
const orderB = symbolOrderMap.get(b);
if (orderA === undefined) throw new Error(`Symbol '${a}' not found in entry files export order`);
if (orderB === undefined) throw new Error(`Symbol '${b}' not found in entry files export order`);
return orderA - orderB;
});
markdown += `\n## Subclasses\n\n`;
subclasses.forEach((sub) => {
markdown += `- [\`${sub}\`](./${sub}.md)\n`;
});
// Recursively build hierarchical list
const buildHierarchicalList = (parentClass: string, depth = 0, visited = new Set<string>()): string => {
if (visited.has(parentClass)) return ''; // Prevent infinite loops
visited.add(parentClass);
const directChildren = classHierarchy.get(parentClass) || [];
if (directChildren.length === 0) return '';
// Sort children by definition order
const sortedChildren = [...directChildren].sort((a, b) => {
const orderA = symbolOrderMap.get(a);
const orderB = symbolOrderMap.get(b);
if (orderA === undefined) throw new Error(`Symbol '${a}' not found in entry files export order`);
if (orderB === undefined) throw new Error(`Symbol '${b}' not found in entry files export order`);
return orderA - orderB;
});
let result = '';
const indent = ' '.repeat(depth);
for (const child of sortedChildren) {
result += `${indent}- [\`${child}\`](./${child}.md)\n`;
// Recursively add children of this child
result += buildHierarchicalList(child, depth + 1, visited);
}
return result;
};
const hierarchicalList = buildHierarchicalList(className);
if (hierarchicalList) {
markdown += `\n## Subclasses\n\n${hierarchicalList}`;
}
}
// Add instances section for classes that have instances
@@ -1284,6 +1362,11 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
});
}
// Add placeholder for "Used by" section for classes and interfaces
if (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) {
markdown += '\n<!-- USED_BY_SECTION -->\n';
}
// Add type definition for type aliases
if (ts.isTypeAliasDeclaration(declaration) && declaration.type) {
const resolvedType = typeChecker.getTypeAtLocation(declaration);
@@ -1346,7 +1429,13 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
const typeReferences = filterToExportedTypes([...new Set(allTypeRefs)], className);
const typeReferencesText = formatReferences(typeReferences);
// In tandem: update usage map
typeReferences.forEach(ref => addUsage(ref, className, className, 'type_alias'));
markdown += `\n\`\`\`ts\n${typeDefinition}\n\`\`\`${typeReferencesText}`;
// Add placeholder for "Used by" section for type aliases
markdown += '\n<!-- USED_BY_SECTION -->\n';
}
if (typeParameters) {
@@ -1372,12 +1461,89 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string) => {
if (methods.length > 0) {
markdown += `\n## Methods\n\n${methods.join('\n\n')}\n`;
}
const outputPath = path.join(outputDir, `${className}.md`);
fs.writeFileSync(outputPath, markdown);
console.log(`Generated: ${outputPath}`);
generatedDocs.set(className, markdown);
}
});
// Phase 3: Assemble final docs with "Used by" sections and write files
generatedDocs.forEach((markdown, symbolName) => {
const usages = usedByReferences.get(symbolName);
let usedByMarkdown = '';
if (usages && usages.size > 0) {
const symbolSubclasses = classHierarchy.get(symbolName) || [];
const usedByLines = [...usages]
.filter((usage) => {
// Filter out subclasses from "Used by" since they already appear in "Subclasses" section
if (usage.type === 'extends' && symbolSubclasses.includes(usage.user)) {
return false;
}
// Filter out top-level type references when there are more specific contexts available
// This prevents redundancy where both "TypeName" and "TypeName.property" appear
if (usage.type === 'type_alias' || usage.type === 'type_param') {
// Check if there are more specific usages from the same user (property, method, constructor, etc.)
const hasMoreSpecificUsage = [...usages].some(otherUsage =>
otherUsage.user === usage.user
&& otherUsage.type !== 'type_alias'
&& otherUsage.type !== 'type_param'
&& otherUsage.type !== 'extends',
);
if (hasMoreSpecificUsage) {
return false;
}
}
return true;
})
.map((usage) => {
let displayText = '';
let link = '';
switch (usage.type) {
case 'constructor':
displayText = `new ${usage.user}()`;
link = `./${usage.user}.md#constructor`;
break;
case 'method':
displayText = `${usage.user}.${usage.context}()`;
link = `./${usage.user}.md#${usage.context.toLowerCase()}`;
break;
case 'property':
case 'variable':
displayText = `${usage.user}.${usage.context}`;
link = `./${usage.user}.md#${usage.context.toLowerCase()}`;
break;
case 'function':
displayText = `${usage.user}()`;
link = `./${usage.user}.md`;
break;
case 'extends':
case 'type_param':
case 'type_alias':
displayText = usage.user;
link = `./${usage.user}.md`;
break;
}
return { text: `[\`${displayText}\`](${link})`, sortKey: displayText.toLowerCase() };
});
if (usedByLines.length > 0) {
// Sort alphabetically by display text
usedByLines.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
const listItems = usedByLines.map(item => `- ${item.text}`).join('\n');
usedByMarkdown = `\n## Used by\n\n${listItems}\n`;
}
}
const finalMarkdown = markdown.replace('<!-- USED_BY_SECTION -->', usedByMarkdown);
const outputPath = path.join(outputDir, `${symbolName}.md`);
fs.writeFileSync(outputPath, finalMarkdown);
console.log(`Generated: ${outputPath}`);
});
// Generate index.md with all exported symbols grouped by group
const entriesByGroup = new Map<string, Array<{ name: string; type: string; order: number }>>();
+11 -3
View File
@@ -534,9 +534,14 @@ export class Conversion {
if (this.onProgress) {
this._computeProgress = true;
this._totalDuration = Math.min(
await this.input.computeDuration() - this._startTimestamp,
(await this.input.computeDuration()) - this._startTimestamp,
this._endTimestamp - this._startTimestamp,
);
for (const track of this.utilizedTracks) {
this._maxTimestamps.set(track.id, 0);
}
this.onProgress?.(0);
}
@@ -723,7 +728,7 @@ export class Conversion {
await tempOutput.start();
const sink = new VideoSampleSink(track);
const firstSample = await sink.getSample(this._startTimestamp);
const firstSample = await sink.getSample(firstTimestamp); // Let's just use the first sample
if (firstSample) {
try {
@@ -1160,7 +1165,10 @@ export class Conversion {
}
assert(this._totalDuration !== null);
this._maxTimestamps.set(trackId, Math.max(endTimestamp, this._maxTimestamps.get(trackId) ?? -Infinity));
this._maxTimestamps.set(
trackId,
Math.max(endTimestamp, this._maxTimestamps.get(trackId)!),
);
const minTimestamp = Math.min(...this._maxTimestamps.values());
const newProgress = clamp(minTimestamp / this._totalDuration, 0, 1);