mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-04 22:33:49 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9f7ab2fa2 | ||
|
|
a295cd76c6 | ||
|
|
b9efcc867d | ||
|
|
cb9f4e0fdf | ||
|
|
d63d7f6ff9 | ||
|
|
f3613b7260 | ||
|
|
b3396727f0 | ||
|
|
af25bb41ab | ||
|
|
e672a51dc6 | ||
|
|
2586e0e6f8 | ||
|
|
9f9809fbd1 | ||
|
|
dcda90fddb | ||
|
|
d7e8273185 |
+5
-4
@@ -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
|
||||
},
|
||||
});
|
||||
|
||||
+22
-4
@@ -14,12 +14,30 @@
|
||||
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(2.131875));
|
||||
|
||||
/*
|
||||
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
|
||||
|
||||
for await (const packet of sink.packets()) {
|
||||
console.log(packet)
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
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 });
|
||||
|
||||
@@ -60,6 +60,12 @@ await input.computeDuration(); // => 1905.4615
|
||||
```
|
||||
More specifically, the duration is defined as the maximum end timestamp across all tracks.
|
||||
|
||||
Mediabunny also lets you read descriptive metadata tags from media files, such as title, artist, or cover art:
|
||||
```ts
|
||||
await input.getMetadataTags(); // => MetadataTags
|
||||
```
|
||||
For more info, see [`MetadataTags`](../api/MetadataTags).
|
||||
|
||||
## Reading track metadata
|
||||
|
||||
You can extract the list of all media tracks in the file like so:
|
||||
@@ -76,12 +82,6 @@ await input.getPrimaryVideoTrack(); // => InputVideoTrack | null
|
||||
await input.getPrimaryAudioTrack(); // => InputAudioTrack | null
|
||||
```
|
||||
|
||||
Mediabunny also lets you read descriptive metadata tags from media files, such as title, artist, or cover art:
|
||||
```ts
|
||||
await input.getMetadataTags(); // => MetadataTags
|
||||
```
|
||||
For more info, see [`MetadataTags`](../api/MetadataTags).
|
||||
|
||||
::: info
|
||||
Subtitle tracks are currently not supported for reading.
|
||||
:::
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -96,6 +96,13 @@ const generateThumbnails = async (resource: File | string) => {
|
||||
timestampElement.className
|
||||
= 'absolute bottom-0 right-0 bg-black/30 text-white px-1 py-0.5 text-[11px] rounded-tl-lg';
|
||||
container.append(timestampElement);
|
||||
} else {
|
||||
// Add something to indicate that the thumbnail is missing
|
||||
const p = document.createElement('p');
|
||||
p.textContent = '?';
|
||||
p.className = 'absolute inset-0 flex items-center justify-center text-3xl opacity-50';
|
||||
|
||||
container.append(p);
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
Generated
+6
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.3",
|
||||
"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.2",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.14.2.tgz",
|
||||
"integrity": "sha512-JCup3rpsJGIw3I7E7bZSmVEANCr/OwfwkH3g/Wq1TcHEJ7ymLLw+jC3+/Nnn/JLXzFnw8/I6fV3xECLnFedfHg==",
|
||||
"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.3",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.3",
|
||||
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.14.0",
|
||||
"version": "1.14.3",
|
||||
"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
@@ -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 }>>();
|
||||
|
||||
|
||||
+67
-50
@@ -26,8 +26,27 @@ import { EncodedPacket, PacketType } from './packet';
|
||||
// Rec. ITU-T H.265
|
||||
// https://stackoverflow.com/questions/24884827
|
||||
|
||||
export enum AvcNalUnitType {
|
||||
IDR = 5,
|
||||
SPS = 7,
|
||||
PPS = 8,
|
||||
SPS_EXT = 13,
|
||||
}
|
||||
|
||||
export enum HevcNalUnitType {
|
||||
RASL_N = 8,
|
||||
RASL_R = 9,
|
||||
BLA_W_LP = 16,
|
||||
RSV_IRAP_VCL23 = 23,
|
||||
VPS_NUT = 32,
|
||||
SPS_NUT = 33,
|
||||
PPS_NUT = 34,
|
||||
PREFIX_SEI_NUT = 39,
|
||||
SUFFIX_SEI_NUT = 40,
|
||||
}
|
||||
|
||||
/** Finds all NAL units in an AVC packet in Annex B format. */
|
||||
const findNalUnitsInAnnexB = (packetData: Uint8Array) => {
|
||||
export const findNalUnitsInAnnexB = (packetData: Uint8Array) => {
|
||||
const nalUnits: Uint8Array[] = [];
|
||||
let i = 0;
|
||||
|
||||
@@ -184,6 +203,21 @@ export type AvcDecoderConfigurationRecord = {
|
||||
sequenceParameterSetExt: Uint8Array[] | null;
|
||||
};
|
||||
|
||||
export const extractAvcNalUnits = (packetData: Uint8Array, decoderConfig: VideoDecoderConfig) => {
|
||||
if (decoderConfig.description) {
|
||||
// Stream is length-prefixed. Let's extract the size of the length prefix from the decoder config
|
||||
|
||||
const bytes = toUint8Array(decoderConfig.description);
|
||||
const lengthSizeMinusOne = bytes[4]! & 0b11;
|
||||
const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4;
|
||||
|
||||
return findNalUnitsInLengthPrefixed(packetData, lengthSize);
|
||||
} else {
|
||||
// Stream is in Annex B format
|
||||
return findNalUnitsInAnnexB(packetData);
|
||||
}
|
||||
};
|
||||
|
||||
const extractNalUnitTypeForAvc = (data: Uint8Array) => {
|
||||
return data[0]! & 0x1F;
|
||||
};
|
||||
@@ -193,9 +227,9 @@ export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array) =>
|
||||
try {
|
||||
const nalUnits = findNalUnitsInAnnexB(packetData);
|
||||
|
||||
const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === 7);
|
||||
const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === 8);
|
||||
const spsExtUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === 13);
|
||||
const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === AvcNalUnitType.SPS);
|
||||
const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === AvcNalUnitType.PPS);
|
||||
const spsExtUnits = nalUnits.filter(unit => extractNalUnitTypeForAvc(unit) === AvcNalUnitType.SPS_EXT);
|
||||
|
||||
if (spsUnits.length === 0) {
|
||||
return null;
|
||||
@@ -337,12 +371,6 @@ export const serializeAvcDecoderConfigurationRecord = (record: AvcDecoderConfigu
|
||||
return new Uint8Array(bytes);
|
||||
};
|
||||
|
||||
const NALU_TYPE_VPS = 32;
|
||||
const NALU_TYPE_SPS = 33;
|
||||
const NALU_TYPE_PPS = 34;
|
||||
const NALU_TYPE_SEI_PREFIX = 39;
|
||||
const NALU_TYPE_SEI_SUFFIX = 40;
|
||||
|
||||
// Data specified in ISO 14496-15
|
||||
export type HevcDecoderConfigurationRecord = {
|
||||
configurationVersion: number;
|
||||
@@ -369,7 +397,22 @@ export type HevcDecoderConfigurationRecord = {
|
||||
}[];
|
||||
};
|
||||
|
||||
const extractNalUnitTypeForHevc = (data: Uint8Array) => {
|
||||
export const extractHevcNalUnits = (packetData: Uint8Array, decoderConfig: VideoDecoderConfig) => {
|
||||
if (decoderConfig.description) {
|
||||
// Stream is length-prefixed. Let's extract the size of the length prefix from the decoder config
|
||||
|
||||
const bytes = toUint8Array(decoderConfig.description);
|
||||
const lengthSizeMinusOne = bytes[21]! & 0b11;
|
||||
const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4;
|
||||
|
||||
return findNalUnitsInLengthPrefixed(packetData, lengthSize);
|
||||
} else {
|
||||
// Stream is in Annex B format
|
||||
return findNalUnitsInAnnexB(packetData);
|
||||
}
|
||||
};
|
||||
|
||||
export const extractNalUnitTypeForHevc = (data: Uint8Array) => {
|
||||
return (data[0]! >> 1) & 0x3F;
|
||||
};
|
||||
|
||||
@@ -380,12 +423,12 @@ export const extractHevcDecoderConfigurationRecord = (
|
||||
try {
|
||||
const nalUnits = findNalUnitsInAnnexB(packetData);
|
||||
|
||||
const vpsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_VPS);
|
||||
const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_SPS);
|
||||
const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_PPS);
|
||||
const vpsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === HevcNalUnitType.VPS_NUT);
|
||||
const spsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === HevcNalUnitType.SPS_NUT);
|
||||
const ppsUnits = nalUnits.filter(unit => extractNalUnitTypeForHevc(unit) === HevcNalUnitType.PPS_NUT);
|
||||
const seiUnits = nalUnits.filter(
|
||||
unit => extractNalUnitTypeForHevc(unit) === NALU_TYPE_SEI_PREFIX
|
||||
|| extractNalUnitTypeForHevc(unit) === NALU_TYPE_SEI_SUFFIX,
|
||||
unit => extractNalUnitTypeForHevc(unit) === HevcNalUnitType.PREFIX_SEI_NUT
|
||||
|| extractNalUnitTypeForHevc(unit) === HevcNalUnitType.SUFFIX_SEI_NUT,
|
||||
);
|
||||
|
||||
if (spsUnits.length === 0 || ppsUnits.length === 0) return null;
|
||||
@@ -521,7 +564,7 @@ export const extractHevcDecoderConfigurationRecord = (
|
||||
? [
|
||||
{
|
||||
arrayCompleteness: 1,
|
||||
nalUnitType: NALU_TYPE_VPS,
|
||||
nalUnitType: HevcNalUnitType.VPS_NUT,
|
||||
nalUnits: vpsUnits,
|
||||
},
|
||||
]
|
||||
@@ -530,7 +573,7 @@ export const extractHevcDecoderConfigurationRecord = (
|
||||
? [
|
||||
{
|
||||
arrayCompleteness: 1,
|
||||
nalUnitType: NALU_TYPE_SPS,
|
||||
nalUnitType: HevcNalUnitType.SPS_NUT,
|
||||
nalUnits: spsUnits,
|
||||
},
|
||||
]
|
||||
@@ -539,7 +582,7 @@ export const extractHevcDecoderConfigurationRecord = (
|
||||
? [
|
||||
{
|
||||
arrayCompleteness: 1,
|
||||
nalUnitType: NALU_TYPE_PPS,
|
||||
nalUnitType: HevcNalUnitType.PPS_NUT,
|
||||
nalUnits: ppsUnits,
|
||||
},
|
||||
]
|
||||
@@ -1440,22 +1483,9 @@ export const determineVideoPacketType = async (
|
||||
const decoderConfig = await videoTrack.getDecoderConfig();
|
||||
assert(decoderConfig);
|
||||
|
||||
let nalUnits: Uint8Array[];
|
||||
const nalUnits = extractAvcNalUnits(packet.data, decoderConfig);
|
||||
const isKeyframe = nalUnits.some(x => extractNalUnitTypeForAvc(x) === AvcNalUnitType.IDR);
|
||||
|
||||
if (decoderConfig.description) {
|
||||
// Stream is length-prefixed. Let's extract the size of the length prefix from the decoder config
|
||||
|
||||
const bytes = toUint8Array(decoderConfig.description);
|
||||
const lengthSizeMinusOne = bytes[4]! & 0b11;
|
||||
const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4;
|
||||
|
||||
nalUnits = findNalUnitsInLengthPrefixed(packet.data, lengthSize);
|
||||
} else {
|
||||
// Stream is in Annex B format
|
||||
nalUnits = findNalUnitsInAnnexB(packet.data);
|
||||
}
|
||||
|
||||
const isKeyframe = nalUnits.some(x => extractNalUnitTypeForAvc(x) === 5);
|
||||
return isKeyframe ? 'key' : 'delta';
|
||||
};
|
||||
|
||||
@@ -1463,25 +1493,12 @@ export const determineVideoPacketType = async (
|
||||
const decoderConfig = await videoTrack.getDecoderConfig();
|
||||
assert(decoderConfig);
|
||||
|
||||
let nalUnits: Uint8Array[];
|
||||
|
||||
if (decoderConfig.description) {
|
||||
// Stream is length-prefixed. Let's extract the size of the length prefix from the decoder config
|
||||
|
||||
const bytes = toUint8Array(decoderConfig.description);
|
||||
const lengthSizeMinusOne = bytes[21]! & 0b11;
|
||||
const lengthSize = (lengthSizeMinusOne + 1) as 1 | 2 | 3 | 4;
|
||||
|
||||
nalUnits = findNalUnitsInLengthPrefixed(packet.data, lengthSize);
|
||||
} else {
|
||||
// Stream is in Annex B format
|
||||
nalUnits = findNalUnitsInAnnexB(packet.data);
|
||||
}
|
||||
|
||||
const nalUnits = extractHevcNalUnits(packet.data, decoderConfig);
|
||||
const isKeyframe = nalUnits.some((x) => {
|
||||
const type = extractNalUnitTypeForHevc(x);
|
||||
return 16 <= type && type <= 23;
|
||||
return HevcNalUnitType.BLA_W_LP <= type && type <= HevcNalUnitType.RSV_IRAP_VCL23;
|
||||
});
|
||||
|
||||
return isKeyframe ? 'key' : 'delta';
|
||||
};
|
||||
|
||||
|
||||
+11
-3
@@ -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);
|
||||
|
||||
+29
-3
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { parsePcmCodec, PCM_AUDIO_CODECS, PcmAudioCodec, VideoCodec, AudioCodec } from './codec';
|
||||
import { extractHevcNalUnits, extractNalUnitTypeForHevc, HevcNalUnitType } from './codec-data';
|
||||
import { CustomVideoDecoder, customVideoDecoders, CustomAudioDecoder, customAudioDecoders } from './custom-coder';
|
||||
import { InputAudioTrack, InputTrack, InputVideoTrack } from './input-track';
|
||||
import {
|
||||
@@ -624,8 +625,8 @@ export abstract class BaseMediaSampleSink<
|
||||
const nextPacket = await packetSink.getNextPacket(currentPacket);
|
||||
assert(nextPacket);
|
||||
|
||||
currentPacket = nextPacket;
|
||||
decoder.decode(nextPacket);
|
||||
currentPacket = nextPacket;
|
||||
}
|
||||
|
||||
maxSequenceNumber = -1;
|
||||
@@ -757,12 +758,14 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
||||
|
||||
inputTimestamps: number[] = []; // Timestamps input into the decoder, sorted.
|
||||
sampleQueue: VideoSample[] = []; // Safari-specific thing, check usage.
|
||||
currentPacketIndex = 0;
|
||||
raslSkipped = false; // For HEVC stuff
|
||||
|
||||
constructor(
|
||||
onSample: (sample: VideoSample) => unknown,
|
||||
onError: (error: DOMException) => unknown,
|
||||
codec: VideoCodec,
|
||||
decoderConfig: VideoDecoderConfig,
|
||||
public codec: VideoCodec,
|
||||
public decoderConfig: VideoDecoderConfig,
|
||||
public rotation: Rotation,
|
||||
public timeResolution: number,
|
||||
) {
|
||||
@@ -848,6 +851,26 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
||||
}
|
||||
|
||||
decode(packet: EncodedPacket) {
|
||||
if (this.codec === 'hevc' && this.currentPacketIndex > 0 && !this.raslSkipped) {
|
||||
// If we're using HEVC, we need to make sure to skip any RASL slices that follow a non-IDR key frame such as
|
||||
// CRA_NUT. This is because RASL slices cannot be decoded without data before the CRA_NUT. Browsers behave
|
||||
// differently here: Chromium drops the packets, Safari throws a decoder error. Either way, it's not good
|
||||
// and causes bugs upstream. So, let's take the dropping into our own hands.
|
||||
const nalUnits = extractHevcNalUnits(packet.data, this.decoderConfig);
|
||||
const hasRaslPicture = nalUnits.some((x) => {
|
||||
const type = extractNalUnitTypeForHevc(x);
|
||||
return type === HevcNalUnitType.RASL_N || type === HevcNalUnitType.RASL_R;
|
||||
});
|
||||
|
||||
if (hasRaslPicture) {
|
||||
return; // Drop
|
||||
}
|
||||
|
||||
this.raslSkipped = true;
|
||||
}
|
||||
|
||||
this.currentPacketIndex++;
|
||||
|
||||
if (this.customDecoder) {
|
||||
this.customDecoderQueueSize++;
|
||||
void this.customDecoderCallSerializer
|
||||
@@ -879,6 +902,9 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
||||
|
||||
this.sampleQueue.length = 0;
|
||||
}
|
||||
|
||||
this.currentPacketIndex = 0;
|
||||
this.raslSkipped = false;
|
||||
}
|
||||
|
||||
close() {
|
||||
|
||||
Reference in New Issue
Block a user