mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-04 06:13:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e686659d9 | ||
|
|
cdc2ef63dd | ||
|
|
92a384f418 | ||
|
|
5b890efc29 | ||
|
|
7f963367b5 | ||
|
|
bfd4059c22 |
+10
-1
@@ -17,9 +17,17 @@
|
||||
source: new Mediabunny.BlobSource(file),
|
||||
});
|
||||
|
||||
const track = await input.getPrimaryAudioTrack();
|
||||
const track = await input.getPrimaryVideoTrack();
|
||||
const packetSink = new Mediabunny.EncodedPacketSink(track);
|
||||
|
||||
const first = await packetSink.getFirstPacket({ verifyKeyPackets: true });
|
||||
const second = await packetSink.getNextPacket(first, { verifyKeyPackets: true });
|
||||
const third = await packetSink.getNextPacket(second, { verifyKeyPackets: true });
|
||||
const fourth = await packetSink.getNextPacket(third, { verifyKeyPackets: true });
|
||||
console.log(first, second, third, fourth);
|
||||
|
||||
/*
|
||||
|
||||
for await (const packet of packetSink.packets()) {
|
||||
//console.log(packet);
|
||||
break;
|
||||
@@ -39,6 +47,7 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
const track = await input.getPrimaryAudioTrack();
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"Encoding": "Encoder configuration and encodability checks.",
|
||||
"Decoding": "Decoder configuration and decodability checks.",
|
||||
"Custom coders": "API for adding custom encoders and decoders.",
|
||||
"Logging": "Control over what Mediabunny logs to the console.",
|
||||
"Miscellaneous": "Whatever's left.",
|
||||
|
||||
"@mediabunny/server": "Adds full video/audio decoder and encoder support to Mediabunny running in server-side environments such as Node, Bun, or Deno.",
|
||||
|
||||
@@ -275,6 +275,40 @@ return new VideoSample(pixels, {
|
||||
});
|
||||
```
|
||||
|
||||
## Controlling logging
|
||||
|
||||
Especially in command-line applications you usually don't want Mediabunny interfering with your stdout and stderr output. Mediabunny provides ways to control its console output:
|
||||
```ts
|
||||
import { Logging, LogLevel } from 'mediabunny';
|
||||
|
||||
// The default: Mediabunny can log errors, warnings, and information messages.
|
||||
Logging.level = LogLevel.Info;
|
||||
|
||||
// Only log warnings and errors.
|
||||
Logging.level = LogLevel.Warnings;
|
||||
|
||||
// Only log errors.
|
||||
Logging.level = LogLevel.Errors;
|
||||
|
||||
// Don't log anything at all.
|
||||
Logging.level = LogLevel.Silent;
|
||||
```
|
||||
|
||||
You can also hook into log events:
|
||||
```ts
|
||||
Logging.on('error', (args: unknown[]) => {
|
||||
// Handle error message
|
||||
});
|
||||
|
||||
Logging.on('warn', (args: unknown[]) => {
|
||||
// Handle warning message
|
||||
});
|
||||
|
||||
Logging.on('info', (args: unknown[]) => {
|
||||
// Handle info message
|
||||
});
|
||||
```
|
||||
|
||||
## Implementation details
|
||||
|
||||
`@mediabunny/server` uses [NodeAV](https://github.com/seydx/node-av) under the hood which provides N-API C bindings to FFmpeg's C API. Using NodeAV, this package implements [custom decoders and encoders](https://mediabunny.dev/guide/supported-formats-and-codecs#custom-coders) by directly using the APIs provided by `libavcodec`.
|
||||
|
||||
Generated
+7
-7
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
".",
|
||||
@@ -12864,7 +12864,7 @@
|
||||
},
|
||||
"packages/aac-encoder": {
|
||||
"name": "@mediabunny/aac-encoder",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12879,7 +12879,7 @@
|
||||
},
|
||||
"packages/ac3": {
|
||||
"name": "@mediabunny/ac3",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12894,7 +12894,7 @@
|
||||
},
|
||||
"packages/flac-encoder": {
|
||||
"name": "@mediabunny/flac-encoder",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12909,7 +12909,7 @@
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12924,7 +12924,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@mediabunny/server",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"node-av": "^6.0.0"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"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/aac-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"description": "AAC encoder extension for Mediabunny, based on FFmpeg.",
|
||||
"main": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Logging } from 'mediabunny';
|
||||
|
||||
const AAC_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/aac-encoder loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[AAC_ENCODER_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
Logging._error(
|
||||
'[WARNING]\n@mediabunny/aac-encoder was loaded twice.'
|
||||
+ ' This will likely cause the encoder not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/aac-encoder,'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/ac3",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.",
|
||||
"main": "./dist/bundles/mediabunny-ac3.mjs",
|
||||
"module": "./dist/bundles/mediabunny-ac3.mjs",
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Logging } from 'mediabunny';
|
||||
|
||||
const AC3_LOADED_SYMBOL = Symbol.for('@mediabunny/ac3 loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[AC3_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
Logging._error(
|
||||
'[WARNING]\n@mediabunny/ac3 was loaded twice.'
|
||||
+ ' This will likely cause the encoder/decoder not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/ac3,'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/flac-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
|
||||
"main": "./dist/bundles/mediabunny-flac-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-flac-encoder.mjs",
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Logging } from 'mediabunny';
|
||||
|
||||
const FLAC_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/flac-encoder loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[FLAC_ENCODER_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
Logging._error(
|
||||
'[WARNING]\n@mediabunny/flac-encoder was loaded twice.'
|
||||
+ ' This will likely cause the encoder not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/flac-encoder,'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { CustomAudioEncoder, AudioCodec, AudioSample, EncodedPacket, registerEncoder } from 'mediabunny';
|
||||
import { CustomAudioEncoder, AudioCodec, AudioSample, EncodedPacket, Logging, registerEncoder } from 'mediabunny';
|
||||
import { MP3_FRAME_HEADER_SIZE, readMp3FrameHeader, SAMPLING_RATES } from '../../../shared/mp3-misc';
|
||||
import type { WorkerCommand, WorkerResponse, WorkerResponseData } from './shared';
|
||||
// @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand
|
||||
@@ -14,7 +14,7 @@ import createWorker from './encode.worker';
|
||||
|
||||
const MP3_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/mp3-encoder loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[MP3_ENCODER_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
Logging._error(
|
||||
'[WARNING]\n@mediabunny/mp3-encoder was loaded twice.'
|
||||
+ ' This will likely cause the encoder not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/mp3-encoder,'
|
||||
|
||||
@@ -279,6 +279,40 @@ return new VideoSample(pixels, {
|
||||
});
|
||||
```
|
||||
|
||||
## Controlling logging
|
||||
|
||||
Especially in command-line applications you usually don't want Mediabunny interfering with your stdout and stderr output. Mediabunny provides ways to control its console output:
|
||||
```ts
|
||||
import { Logging, LogLevel } from 'mediabunny';
|
||||
|
||||
// The default: Mediabunny can log errors, warnings, and information messages.
|
||||
Logging.level = LogLevel.Info;
|
||||
|
||||
// Only log warnings and errors.
|
||||
Logging.level = LogLevel.Warnings;
|
||||
|
||||
// Only log errors.
|
||||
Logging.level = LogLevel.Errors;
|
||||
|
||||
// Don't log anything at all.
|
||||
Logging.level = LogLevel.None;
|
||||
```
|
||||
|
||||
You can also hook into log events:
|
||||
```ts
|
||||
Logging.on('error', (args: unknown[]) => {
|
||||
// Handle error message
|
||||
});
|
||||
|
||||
Logging.on('warn', (args: unknown[]) => {
|
||||
// Handle warning message
|
||||
});
|
||||
|
||||
Logging.on('info', (args: unknown[]) => {
|
||||
// Handle info message
|
||||
});
|
||||
```
|
||||
|
||||
## Implementation details
|
||||
|
||||
`@mediabunny/server` uses [NodeAV](https://github.com/seydx/node-av) under the hood which provides N-API C bindings to FFmpeg's C API. Using NodeAV, this package implements [custom decoders and encoders](https://mediabunny.dev/guide/supported-formats-and-codecs#custom-coders) by directly using the APIs provided by `libavcodec`.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/server",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.48.0",
|
||||
"version": "1.49.0",
|
||||
"description": "Adds full video and audio decoder and encoder support to Mediabunny for use in server-side environments (Node, Bun, Deno). Based on NodeAV.",
|
||||
"main": "./dist/bundles/mediabunny-server.cjs",
|
||||
"module": "./dist/bundles/mediabunny-server.mjs",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import {
|
||||
AudioSample,
|
||||
Logging,
|
||||
MaybePromise,
|
||||
registerDecoder,
|
||||
registerEncoder,
|
||||
@@ -24,7 +25,7 @@ import { copyAudioSampleToAvFrame, AvFrameAudioSampleResource } from './audio-sa
|
||||
|
||||
const SERVER_LOADED_SYMBOL = Symbol.for('@mediabunny/server loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[SERVER_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
Logging._error(
|
||||
'[WARNING]\n@mediabunny/server was loaded twice.'
|
||||
+ ' This will likely cause the package not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of @mediabunny/server,'
|
||||
|
||||
@@ -23,6 +23,7 @@ const checkDocblocks = (filePath: string) => {
|
||||
|| ts.isFunctionDeclaration(node)
|
||||
|| ts.isTypeAliasDeclaration(node)
|
||||
|| ts.isEnumDeclaration(node)
|
||||
|| ts.isEnumMember(node)
|
||||
|| ts.isPropertySignature(node)
|
||||
|| ts.isMethodSignature(node)
|
||||
|| ts.isVariableStatement(node)
|
||||
|
||||
@@ -683,7 +683,7 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
|
||||
}
|
||||
|
||||
// Check if it's a supported type
|
||||
if (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration) || ts.isVariableDeclaration(declaration)) {
|
||||
if (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration) || ts.isVariableDeclaration(declaration) || ts.isEnumDeclaration(declaration)) {
|
||||
// Supported types - continue processing
|
||||
} else {
|
||||
// Unsupported type - throw error with type info
|
||||
@@ -777,6 +777,43 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle enum declarations separately
|
||||
if (ts.isEnumDeclaration(declaration)) {
|
||||
const enumName = declaration.name.text;
|
||||
|
||||
const description = extractJsDocDescription(declaration, {
|
||||
tagHandling: 'filterAll',
|
||||
transform: text => processLinkTags(text, enumName),
|
||||
});
|
||||
|
||||
const order = symbolOrderMap.get(enumName);
|
||||
if (order === undefined) {
|
||||
throw new Error(`Symbol '${enumName}' not found in entry files export order`);
|
||||
}
|
||||
indexEntries.push({ name: enumName, type: 'Enum', group: groupName, order });
|
||||
|
||||
// Reconstruct the enum body so each member keeps its value and description
|
||||
const memberLines = declaration.members.map((member) => {
|
||||
const memberName = member.name.getText();
|
||||
const initializer = member.initializer ? ` = ${member.initializer.getText()}` : '';
|
||||
const memberDescription = extractJsDocDescription(member, {
|
||||
tagHandling: 'filterAll',
|
||||
transform: text => processLinkTags(text, enumName),
|
||||
});
|
||||
const commentLine = memberDescription ? `\t/** ${memberDescription} */\n` : '';
|
||||
return `${commentLine}\t${memberName}${initializer},`;
|
||||
});
|
||||
|
||||
const enumDefinition = `enum ${enumName} {\n${memberLines.join('\n')}\n}`;
|
||||
const deprecationNotice = getDeprecationNotice(declaration, enumName);
|
||||
let markdown = `${buildFrontmatter(description)}<script setup>\nimport { VPBadge } from 'vitepress/theme'\n</script>\n\n<VPBadge type="info" text="Enum" />\n\n# ${enumName}\n\n${deprecationNotice}${description ? `${description}\n\n` : ''}`;
|
||||
markdown += `\`\`\`ts\n${enumDefinition}\n\`\`\``;
|
||||
|
||||
generatedDocs.set(enumName, markdown);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
const className = declaration.name.text;
|
||||
const isAbstract = ts.isClassDeclaration(declaration) && declaration.modifiers?.some(mod => mod.kind === ts.SyntaxKind.AbstractKeyword);
|
||||
|
||||
+7
-6
@@ -26,6 +26,7 @@ import {
|
||||
isChromium,
|
||||
setUint24,
|
||||
} from './misc';
|
||||
import { Logging } from './logging';
|
||||
import { PacketType } from './packet';
|
||||
import { MetadataTags } from './metadata';
|
||||
import { AC3_SAMPLE_RATES, EAC3_REDUCED_SAMPLE_RATES } from '../shared/ac3-misc';
|
||||
@@ -318,7 +319,7 @@ export const extractAvcDecoderConfigurationRecord = (packetData: Uint8Array): Av
|
||||
sequenceParameterSetExt: hasExtendedData ? spsExtUnits : null,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error building AVC Decoder Configuration Record:', error);
|
||||
Logging._error('Error building AVC Decoder Configuration Record:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -478,7 +479,7 @@ export const deserializeAvcDecoderConfigurationRecord = (data: Uint8Array): AvcD
|
||||
|
||||
return record;
|
||||
} catch (error) {
|
||||
console.error('Error deserializing AVC Decoder Configuration Record:', error);
|
||||
Logging._error('Error deserializing AVC Decoder Configuration Record:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -795,7 +796,7 @@ export const parseAvcSps = (sps: Uint8Array): AvcSpsInfo | null => {
|
||||
maxDecFrameBuffering,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error parsing AVC SPS:', error);
|
||||
Logging._error('Error parsing AVC SPS:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -1044,7 +1045,7 @@ export const parseHevcSps = (sps: Uint8Array): HevcSpsInfo | null => {
|
||||
minSpatialSegmentationIdc,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error parsing HEVC SPS:', error);
|
||||
Logging._error('Error parsing HEVC SPS:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -1176,7 +1177,7 @@ export const extractHevcDecoderConfigurationRecord = (packetData: Uint8Array) =>
|
||||
|
||||
return record;
|
||||
} catch (error) {
|
||||
console.error('Error building HEVC Decoder Configuration Record:', error);
|
||||
Logging._error('Error building HEVC Decoder Configuration Record:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -1612,7 +1613,7 @@ export const deserializeHevcDecoderConfigurationRecord = (data: Uint8Array): Hev
|
||||
arrays,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error deserializing HEVC Decoder Configuration Record:', error);
|
||||
Logging._error('Error deserializing HEVC Decoder Configuration Record:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
+4
-3
@@ -23,6 +23,7 @@ import {
|
||||
} from './encode';
|
||||
import { Input } from './input';
|
||||
import { InputAudioTrack, InputTrack, InputVideoTrack } from './input-track';
|
||||
import { Logging } from './logging';
|
||||
import {
|
||||
AudioSampleSink,
|
||||
EncodedPacketSink,
|
||||
@@ -958,7 +959,7 @@ export class Conversion {
|
||||
}
|
||||
|
||||
if (warnElements.length > 0) {
|
||||
console.warn(...warnElements);
|
||||
Logging._warn(...warnElements);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1122,7 +1123,7 @@ export class Conversion {
|
||||
}
|
||||
|
||||
if (this._canceled) {
|
||||
console.warn('Conversion already canceled.');
|
||||
Logging._warn('Conversion already canceled.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1327,7 +1328,7 @@ export class Conversion {
|
||||
firstSample.close();
|
||||
await tempOutput.finalize();
|
||||
} catch (error) {
|
||||
console.info('Error when probing encoder support. Falling back to rerender path.', error);
|
||||
Logging._info('Error when probing encoder support. Falling back to rerender path.', error);
|
||||
needsRerender = true;
|
||||
void tempOutput.cancel();
|
||||
}
|
||||
|
||||
+5
-4
@@ -9,6 +9,7 @@
|
||||
import { AudioCodec, VideoCodec } from './codec';
|
||||
import { canDecodeAudioMemo, canDecodeVideoMemo } from './decode';
|
||||
import { canEncodeAudioMemo, canEncodeVideoMemo } from './encode';
|
||||
import { Logging } from './logging';
|
||||
import { MaybePromise } from './misc';
|
||||
import { EncodedPacket } from './packet';
|
||||
import { AudioSample, VideoSample } from './sample';
|
||||
@@ -149,7 +150,7 @@ export const registerDecoder = (decoder: typeof CustomVideoDecoder | typeof Cust
|
||||
const casted = decoder as typeof CustomVideoDecoder;
|
||||
|
||||
if (customVideoDecoders.includes(casted)) {
|
||||
console.warn('Video decoder already registered.');
|
||||
Logging._warn('Video decoder already registered.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,7 +160,7 @@ export const registerDecoder = (decoder: typeof CustomVideoDecoder | typeof Cust
|
||||
const casted = decoder as typeof CustomAudioDecoder;
|
||||
|
||||
if (customAudioDecoders.includes(casted)) {
|
||||
console.warn('Audio decoder already registered.');
|
||||
Logging._warn('Audio decoder already registered.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -181,7 +182,7 @@ export const registerEncoder = (encoder: typeof CustomVideoEncoder | typeof Cust
|
||||
const casted = encoder as typeof CustomVideoEncoder;
|
||||
|
||||
if (customVideoEncoders.includes(casted)) {
|
||||
console.warn('Video encoder already registered.');
|
||||
Logging._warn('Video encoder already registered.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -191,7 +192,7 @@ export const registerEncoder = (encoder: typeof CustomVideoEncoder | typeof Cust
|
||||
const casted = encoder as typeof CustomAudioEncoder;
|
||||
|
||||
if (customAudioEncoders.includes(casted)) {
|
||||
console.warn('Audio encoder already registered.');
|
||||
Logging._warn('Audio encoder already registered.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
getSampleRateOrUncommon,
|
||||
} from './flac-misc';
|
||||
import { Bitstream } from '../../shared/bitstream';
|
||||
import { ID3_V2_HEADER_SIZE, parseId3V2Tag, readId3V2Header } from '../id3';
|
||||
|
||||
type FlacAudioInfo = {
|
||||
numberOfChannels: number;
|
||||
@@ -102,9 +103,34 @@ export class FlacDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
async readMetadata() {
|
||||
let currentPos = 4; // Skip 'fLaC'
|
||||
|
||||
return (this.metadataPromise ??= (async () => {
|
||||
// Read all ID3v2 tags at the start of the file
|
||||
let currentPos = 0;
|
||||
while (true) {
|
||||
let headerSlice = this.reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
|
||||
if (headerSlice instanceof Promise) headerSlice = await headerSlice;
|
||||
|
||||
if (!headerSlice) {
|
||||
this.lastSampleLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const id3V2Header = readId3V2Header(headerSlice);
|
||||
if (!id3V2Header) {
|
||||
break;
|
||||
}
|
||||
|
||||
let contentSlice = this.reader.requestSlice(headerSlice.filePos, id3V2Header.size);
|
||||
if (contentSlice instanceof Promise) contentSlice = await contentSlice;
|
||||
assert(contentSlice);
|
||||
|
||||
parseId3V2Tag(contentSlice, id3V2Header, this.metadataTags);
|
||||
|
||||
currentPos = headerSlice.filePos + id3V2Header.size;
|
||||
}
|
||||
|
||||
currentPos += 4; // Skip 'fLaC'
|
||||
|
||||
while (
|
||||
this.reader.fileSize === null
|
||||
|| currentPos < this.reader.fileSize
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { MediaCodec, validateAudioChunkMetadata, validateVideoChunkMetadata } from '../codec';
|
||||
import { Logging } from '../logging';
|
||||
import { EncodedAudioPacketSource, EncodedVideoPacketSource } from '../media-source';
|
||||
import {
|
||||
arrayArgmax,
|
||||
@@ -197,7 +198,7 @@ export class HlsMuxer extends Muxer {
|
||||
|
||||
if (track.type === otherTrack.type) {
|
||||
if (!illegalPairingDetected) {
|
||||
console.warn(
|
||||
Logging._warn(
|
||||
`Illegal pairing of two ${track.type} tracks detected, which is not possible in HLS;`
|
||||
+ ` treating them as unpaired.`,
|
||||
);
|
||||
@@ -213,7 +214,7 @@ export class HlsMuxer extends Muxer {
|
||||
|| (otherTrack.isVideoTrack() && otherTrack.metadata.hasOnlyKeyPackets)
|
||||
) {
|
||||
if (!keyPacketsOnlyPairingWarned) {
|
||||
console.warn(
|
||||
Logging._warn(
|
||||
`A key-packets-only video track is pairable with another track, which is not`
|
||||
+ ` possible in HLS; treating them as unpaired.`,
|
||||
);
|
||||
@@ -1370,7 +1371,7 @@ export class HlsMuxer extends Muxer {
|
||||
masterPlaylistText += `#EXT-X-MEDIA:TYPE=${type.toUpperCase()},GROUP-ID="${decl.groupId}"`;
|
||||
|
||||
if (name !== null && /[\n\r"]/.test(name)) {
|
||||
console.warn(
|
||||
Logging._warn(
|
||||
'Dropping track name since it includes a line feed, carriage return, or double quote'
|
||||
+ ' character, which are not allowed in HLS playlist attributes.',
|
||||
);
|
||||
|
||||
+14
-14
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { decodeSynchsafe, encodeSynchsafe } from '../shared/mp3-misc';
|
||||
import { Logging } from './logging';
|
||||
import { MetadataTags } from './metadata';
|
||||
import {
|
||||
coalesceIndex,
|
||||
@@ -152,7 +153,11 @@ export const readId3V2Header = (slice: FileSlice): Id3V2Header | null => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const size = decodeSynchsafe(sizeRaw);
|
||||
let size = decodeSynchsafe(sizeRaw);
|
||||
|
||||
if (flags & Id3V2HeaderFlags.Footer) {
|
||||
size += ID3_V2_HEADER_SIZE;
|
||||
}
|
||||
|
||||
return { majorVersion, revision, flags, size };
|
||||
};
|
||||
@@ -161,16 +166,16 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, tags: Metad
|
||||
// https://id3.org/id3v2.3.0
|
||||
|
||||
if (![2, 3, 4].includes(header.majorVersion)) {
|
||||
console.warn(`Unsupported ID3v2 major version: ${header.majorVersion}`);
|
||||
Logging._warn(`Unsupported ID3v2 major version: ${header.majorVersion}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const bytes = readBytes(slice, header.size);
|
||||
const reader = new Id3V2Reader(header, bytes);
|
||||
const dataSize = (header.flags & Id3V2HeaderFlags.Footer)
|
||||
? header.size - ID3_V2_HEADER_SIZE
|
||||
: header.size;
|
||||
|
||||
if (header.flags & Id3V2HeaderFlags.Footer) {
|
||||
reader.removeFooter();
|
||||
}
|
||||
const bytes = readBytes(slice, dataSize);
|
||||
const reader = new Id3V2Reader(header, bytes);
|
||||
|
||||
if ((header.flags & Id3V2HeaderFlags.Unsynchronisation) && header.majorVersion === 3) {
|
||||
reader.ununsynchronizeAll();
|
||||
@@ -210,13 +215,13 @@ export const parseId3V2Tag = (slice: FileSlice, header: Id3V2Header, tags: Metad
|
||||
}
|
||||
|
||||
if (frameEncrypted) {
|
||||
console.warn(`Skipping encrypted ID3v2 frame ${frame.id}`);
|
||||
Logging._warn(`Skipping encrypted ID3v2 frame ${frame.id}`);
|
||||
reader.pos = frameEndPos;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (frameCompressed) {
|
||||
console.warn(`Skipping compressed ID3v2 frame ${frame.id}`); // Maybe someday? Idk
|
||||
Logging._warn(`Skipping compressed ID3v2 frame ${frame.id}`); // Maybe someday? Idk
|
||||
reader.pos = frameEndPos;
|
||||
continue;
|
||||
}
|
||||
@@ -463,11 +468,6 @@ export class Id3V2Reader {
|
||||
this.view = new DataView(this.bytes.buffer);
|
||||
}
|
||||
|
||||
removeFooter() {
|
||||
this.bytes = this.bytes.subarray(0, this.bytes.length - ID3_V2_HEADER_SIZE);
|
||||
this.view = new DataView(this.bytes.buffer);
|
||||
}
|
||||
|
||||
readBytes(length: number) {
|
||||
const slice = this.bytes.subarray(this.pos, this.pos + length);
|
||||
this.pos += length;
|
||||
|
||||
+8
-1
@@ -9,9 +9,11 @@
|
||||
/// <reference types="dom-mediacapture-transform" preserve="true" />
|
||||
/// <reference types="dom-webcodecs" preserve="true" />
|
||||
|
||||
import { Logging } from './logging';
|
||||
|
||||
const MEDIABUNNY_LOADED_SYMBOL = Symbol.for('mediabunny loaded');
|
||||
if ((globalThis as Record<symbol, unknown>)[MEDIABUNNY_LOADED_SYMBOL]) {
|
||||
console.error(
|
||||
Logging._error(
|
||||
'[WARNING]\nMediabunny was loaded twice.'
|
||||
+ ' This will likely cause Mediabunny not to work correctly.'
|
||||
+ ' Check if multiple dependencies are importing different versions of Mediabunny,'
|
||||
@@ -151,6 +153,11 @@ export {
|
||||
type FilePath,
|
||||
type MaybePromise,
|
||||
} from './misc';
|
||||
export {
|
||||
Logging,
|
||||
LogLevel,
|
||||
type LoggingEvents,
|
||||
} from './logging';
|
||||
export {
|
||||
type PsshBox,
|
||||
} from './isobmff/isobmff-misc';
|
||||
|
||||
+17
-1
@@ -446,7 +446,23 @@ export class OggInputFormat extends InputFormat {
|
||||
export class FlacInputFormat extends InputFormat {
|
||||
/** @internal */
|
||||
async _canReadInput(input: Input) {
|
||||
let slice = input._reader.requestSlice(0, 4);
|
||||
let currentPos = 0;
|
||||
|
||||
// There might be ID3v2 headers at the start, skip 'em
|
||||
while (true) {
|
||||
let slice = input._reader.requestSlice(currentPos, ID3_V2_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) break;
|
||||
|
||||
const id3V2Header = readId3V2Header(slice);
|
||||
if (!id3V2Header) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentPos = slice.filePos + id3V2Header.size;
|
||||
}
|
||||
|
||||
let slice = input._reader.requestSlice(currentPos, 4);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) return false;
|
||||
|
||||
|
||||
+3
-2
@@ -10,6 +10,7 @@ import { AudioCodec, MediaCodec, VideoCodec } from './codec';
|
||||
import { determineVideoPacketType } from './codec-data';
|
||||
import { customAudioDecoders, customVideoDecoders } from './custom-coder';
|
||||
import { Input } from './input';
|
||||
import { Logging } from './logging';
|
||||
import { EncodedPacketSink, PacketRetrievalOptions } from './media-sink';
|
||||
import { assert, MaybePromise, Rational, Rotation, roundToDivisor, simplifyRational } from './misc';
|
||||
import { TrackType } from './output';
|
||||
@@ -763,7 +764,7 @@ export class InputVideoTrack extends InputTrack {
|
||||
const support = await VideoDecoder.isConfigSupported(decoderConfig);
|
||||
return support.supported === true;
|
||||
} catch (error) {
|
||||
console.error('Error during decodability check:', error);
|
||||
Logging._error('Error during decodability check:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -903,7 +904,7 @@ export class InputAudioTrack extends InputTrack {
|
||||
return support.supported === true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during decodability check:', error);
|
||||
Logging._error('Error during decodability check:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ import { DEFAULT_TRACK_DISPOSITION, MetadataTags, RichImageData, TrackDispositio
|
||||
import { AC3_SAMPLE_RATES } from '../../shared/ac3-misc';
|
||||
import { Bitstream } from '../../shared/bitstream';
|
||||
import { Aes128CbcContext } from '../aes';
|
||||
import { Logging } from '../logging';
|
||||
|
||||
type InternalTrack = {
|
||||
id: number;
|
||||
@@ -925,7 +926,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
if (relevantEntryFound) {
|
||||
console.warn(
|
||||
Logging._warn(
|
||||
'Unsupported edit list: multiple edits are not currently supported. Only using first edit.',
|
||||
);
|
||||
break;
|
||||
@@ -937,7 +938,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
if (mediaRate !== 1) {
|
||||
console.warn('Unsupported edit list entry: media rate must be 1.');
|
||||
Logging._warn('Unsupported edit list entry: media rate must be 1.');
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1093,9 +1094,9 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (codecName === 'av01') {
|
||||
track.info.codec = 'av1';
|
||||
} else if (codecName === null) {
|
||||
console.warn(`Unknown encrypted video codec due to missing frma box.`);
|
||||
Logging._warn(`Unknown encrypted video codec due to missing frma box.`);
|
||||
} else {
|
||||
console.warn(`Unsupported video codec (sample entry type '${sampleBoxInfo.name}').`);
|
||||
Logging._warn(`Unsupported video codec (sample entry type '${sampleBoxInfo.name}').`);
|
||||
}
|
||||
} else {
|
||||
slice.skip(6 * 1 + 2);
|
||||
@@ -1170,7 +1171,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (sampleSize === 16) {
|
||||
track.info.codec = track.info.pcmLittleEndian ? 'pcm-s16' : 'pcm-s16be';
|
||||
} else {
|
||||
console.warn(`Unsupported sample size ${sampleSize} for codec 'twos'.`);
|
||||
Logging._warn(`Unsupported sample size ${sampleSize} for codec 'twos'.`);
|
||||
track.info.codec = null;
|
||||
}
|
||||
} else if (codecName === 'sowt') {
|
||||
@@ -1179,7 +1180,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (sampleSize === 16) {
|
||||
track.info.codec = 'pcm-s16';
|
||||
} else {
|
||||
console.warn(`Unsupported sample size ${sampleSize} for codec 'sowt'.`);
|
||||
Logging._warn(`Unsupported sample size ${sampleSize} for codec 'sowt'.`);
|
||||
track.info.codec = null;
|
||||
}
|
||||
} else if (codecName === 'raw ') {
|
||||
@@ -1203,7 +1204,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (pcmSampleSize === 32) {
|
||||
track.info.codec = 'pcm-s32';
|
||||
} else {
|
||||
console.warn(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
||||
Logging._warn(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
||||
track.info.codec = null;
|
||||
}
|
||||
} else {
|
||||
@@ -1214,7 +1215,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (pcmSampleSize === 32) {
|
||||
track.info.codec = 'pcm-s32be';
|
||||
} else {
|
||||
console.warn(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
||||
Logging._warn(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
||||
track.info.codec = null;
|
||||
}
|
||||
}
|
||||
@@ -1227,7 +1228,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (pcmSampleSize === 64) {
|
||||
track.info.codec = 'pcm-f64';
|
||||
} else {
|
||||
console.warn(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
||||
Logging._warn(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
||||
track.info.codec = null;
|
||||
}
|
||||
} else {
|
||||
@@ -1236,7 +1237,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (pcmSampleSize === 64) {
|
||||
track.info.codec = 'pcm-f64be';
|
||||
} else {
|
||||
console.warn(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
||||
Logging._warn(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
||||
track.info.codec = null;
|
||||
}
|
||||
}
|
||||
@@ -1271,12 +1272,12 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
if (track.info.codec === null) {
|
||||
console.warn('Unsupported PCM format.');
|
||||
Logging._warn('Unsupported PCM format.');
|
||||
}
|
||||
} else if (codecName === null) {
|
||||
console.warn(`Unknown encrypted audio codec due to missing frma box.`);
|
||||
Logging._warn(`Unknown encrypted audio codec due to missing frma box.`);
|
||||
} else {
|
||||
console.warn(`Unsupported audio codec (sample entry type '${sampleBoxInfo.name}').`);
|
||||
Logging._warn(`Unsupported audio codec (sample entry type '${sampleBoxInfo.name}').`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1317,7 +1318,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
defaultSkipByteBlock: null,
|
||||
};
|
||||
} else {
|
||||
console.warn(`Unsupported encryption scheme '${schemeType}'.`);
|
||||
Logging._warn(`Unsupported encryption scheme '${schemeType}'.`);
|
||||
}
|
||||
}; break;
|
||||
|
||||
@@ -1544,7 +1545,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (objectTypeIndication === 0xdd) {
|
||||
track.info.codec = 'vorbis'; // "nonstandard, gpac uses it" - FFmpeg
|
||||
} else {
|
||||
console.warn(
|
||||
Logging._warn(
|
||||
`Unsupported audio codec (objectTypeIndication ${objectTypeIndication}) - discarding track.`,
|
||||
);
|
||||
}
|
||||
@@ -1730,7 +1731,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
const config = parseEac3Config(bytes);
|
||||
|
||||
if (!config) {
|
||||
console.warn('Invalid dec3 box contents, ignoring.');
|
||||
Logging._warn('Invalid dec3 box contents, ignoring.');
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2377,7 +2378,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
break;
|
||||
}
|
||||
if (entryCount > 1) {
|
||||
console.warn('Multiple saio entries are not supported; using the first offset only.');
|
||||
Logging._warn('Multiple saio entries are not supported; using the first offset only.');
|
||||
}
|
||||
|
||||
let offset = version === 0 ? readU32Be(slice) : Number(readU64Be(slice));
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/*!
|
||||
* Copyright (c) 2026-present, Vanilagy and contributors
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { EventEmitter, type EventListenerOptions } from './misc';
|
||||
|
||||
/**
|
||||
* Controls how much information Mediabunny prints to the console. Higher levels include all lower levels.
|
||||
*
|
||||
* @group Logging
|
||||
* @public
|
||||
*/
|
||||
export enum LogLevel {
|
||||
/** Nothing is printed to the console. */
|
||||
Silent = 0,
|
||||
/** Only errors are printed. */
|
||||
Errors = 1,
|
||||
/** Errors and warnings are printed. */
|
||||
Warnings = 2,
|
||||
/** Errors, warnings, and informational messages are printed. */
|
||||
Info = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* The events emitted by {@link Logging}. Each event carries the same arguments that were passed to the corresponding
|
||||
* log call.
|
||||
*
|
||||
* @group Logging
|
||||
* @public
|
||||
*/
|
||||
export type LoggingEvents = {
|
||||
/** Emitted before an error is logged. */
|
||||
error: unknown[];
|
||||
/** Emitted before a warning is logged. */
|
||||
warn: unknown[];
|
||||
/** Emitted before an informational message is logged. */
|
||||
info: unknown[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Mediabunny's central logging singleton. Use {@link Logging.level} to control how much is printed to the console,
|
||||
* and subscribe to log events using {@link Logging.on}.
|
||||
*
|
||||
* Having manual control over logging is useful for command-line applications where you want full say over the output.
|
||||
*
|
||||
* @group Logging
|
||||
* @public
|
||||
*/
|
||||
export class Logging {
|
||||
private constructor() {}
|
||||
|
||||
/** @internal */
|
||||
static _level: LogLevel = LogLevel.Info;
|
||||
/** @internal */
|
||||
static _emitterInstance: EventEmitter<LoggingEvents> | null = null;
|
||||
|
||||
/** The current log level. Defaults to {@link LogLevel.Info}. */
|
||||
static get level() {
|
||||
return Logging._level;
|
||||
}
|
||||
|
||||
static set level(value: LogLevel) {
|
||||
if (
|
||||
value !== LogLevel.Silent
|
||||
&& value !== LogLevel.Errors
|
||||
&& value !== LogLevel.Warnings
|
||||
&& value !== LogLevel.Info
|
||||
) {
|
||||
throw new TypeError('Invalid log level. Use one of the values of the LogLevel enum.');
|
||||
}
|
||||
|
||||
Logging._level = value;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
static get _emitter() {
|
||||
// Created lazily to avoid touching the EventEmitter binding at module-eval time
|
||||
return Logging._emitterInstance ??= new EventEmitter<LoggingEvents>();
|
||||
}
|
||||
|
||||
/** Registers a listener for a log event. Returns a function that, when called, removes the listener again. */
|
||||
static on<K extends keyof LoggingEvents>(
|
||||
event: K,
|
||||
listener: (data: LoggingEvents[K]) => unknown,
|
||||
options?: EventListenerOptions,
|
||||
) {
|
||||
return Logging._emitter.on(event, listener, options);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
static _error(...args: unknown[]) {
|
||||
Logging._emitter._emit('error', args);
|
||||
|
||||
if (Logging._level >= LogLevel.Errors) {
|
||||
console.error(...args);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
static _warn(...args: unknown[]) {
|
||||
Logging._emitter._emit('warn', args);
|
||||
|
||||
if (Logging._level >= LogLevel.Warnings) {
|
||||
console.warn(...args);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
static _info(...args: unknown[]) {
|
||||
Logging._emitter._emit('info', args);
|
||||
|
||||
if (Logging._level >= LogLevel.Info) {
|
||||
console.info(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from '../codec';
|
||||
import { Demuxer } from '../demuxer';
|
||||
import { Input } from '../input';
|
||||
import { Logging } from '../logging';
|
||||
import {
|
||||
InputAudioTrackBacking,
|
||||
InputTrackBacking,
|
||||
@@ -1021,7 +1022,7 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
|| instruction.scope !== ContentEncodingScope.Block
|
||||
|| instruction.data.algorithm !== ContentCompAlgo.HeaderStripping;
|
||||
})) {
|
||||
console.warn(`Track #${this.currentTrack.id} has an unsupported content encoding; dropping.`);
|
||||
Logging._warn(`Track #${this.currentTrack.id} has an unsupported content encoding; dropping.`);
|
||||
this.currentTrack = null;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -54,6 +54,7 @@ import {
|
||||
VideoSample,
|
||||
VideoSamplePixelFormat,
|
||||
} from './sample';
|
||||
import { Logging } from './logging';
|
||||
|
||||
/**
|
||||
* Additional options for controlling packet retrieval.
|
||||
@@ -1324,7 +1325,7 @@ export class ColorAlphaMerger {
|
||||
this.gl = null;
|
||||
this.canvas = null;
|
||||
mergerGpuUnavailable = true;
|
||||
console.warn('Falling back to CPU for color/alpha merging.', error);
|
||||
Logging._warn('Falling back to CPU for color/alpha merging.', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -64,6 +64,7 @@ import {
|
||||
} from './encode';
|
||||
import { AudioResampler } from './resample';
|
||||
import { determineVideoPacketType } from './codec-data';
|
||||
import { Logging } from './logging';
|
||||
|
||||
/**
|
||||
* Base class for media sources. Media sources are used to add media samples to an output file.
|
||||
@@ -968,7 +969,7 @@ export class ColorAlphaSplitter {
|
||||
this.gl = null;
|
||||
this.canvas = null;
|
||||
splitterGpuUnavailable = true;
|
||||
console.warn('Falling back to CPU for color/alpha splitting.', error);
|
||||
Logging._warn('Falling back to CPU for color/alpha splitting.', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1116,7 +1117,7 @@ export class ColorAlphaSplitter {
|
||||
this.gl.shaderSource(shader, source);
|
||||
this.gl.compileShader(shader);
|
||||
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
||||
console.error('Shader compile error:', this.gl.getShaderInfoLog(shader));
|
||||
Logging._error('Shader compile error:', this.gl.getShaderInfoLog(shader));
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
@@ -1673,7 +1674,7 @@ export class MediaStreamVideoTrackSource extends VideoSource {
|
||||
/** @internal */
|
||||
override async _start() {
|
||||
if (!this._errorPromiseAccessed) {
|
||||
console.warn(
|
||||
Logging._warn(
|
||||
'Make sure not to ignore the `errorPromise` field on MediaStreamVideoTrackSource, so that any internal'
|
||||
+ ' errors get bubbled up properly.',
|
||||
);
|
||||
@@ -2783,7 +2784,7 @@ export class MediaStreamAudioTrackSource extends AudioSource {
|
||||
/** @internal */
|
||||
override async _start() {
|
||||
if (!this._errorPromiseAccessed) {
|
||||
console.warn(
|
||||
Logging._warn(
|
||||
'Make sure not to ignore the `errorPromise` field on MediaStreamAudioTrackSource, so that any internal'
|
||||
+ ' errors get bubbled up properly.',
|
||||
);
|
||||
|
||||
+4
-2
@@ -20,7 +20,8 @@ import { isRecordStringString } from './misc';
|
||||
* in Vorbis-style comment headers.
|
||||
* - For WAVE files, the metadata refers to the chunks within the RIFF INFO chunk.
|
||||
* - For ADTS files, the metadata refers to the ID3v2 tags.
|
||||
* - For FLAC files, the metadata lives in Vorbis style in the Vorbis comment block.
|
||||
* - For FLAC files, the metadata lives in Vorbis style in the Vorbis comment block, or sometimes in ID3v2 tags at the
|
||||
* start of the file.
|
||||
* - For MPEG-TS files, metadata tags are currently not supported.
|
||||
*
|
||||
* @group Metadata tags
|
||||
@@ -78,7 +79,8 @@ export type MetadataTags = {
|
||||
* Additionally, the `'vendor'` key refers to the vendor string within this header.
|
||||
* - WAVE: The individual metadata chunks within the RIFF INFO chunk. Values are always ISO 8859-1 strings.
|
||||
* - FLAC: The key-value string pairs from the vorbis metadata block (see RFC 9639, Section D.2.3).
|
||||
* Additionally, the `'vendor'` key refers to the vendor string within this header.
|
||||
* Additionally, the `'vendor'` key refers to the vendor string within this header. If ID3v2 tags appear at the
|
||||
* start of the file, their content is stored just like for MP3.
|
||||
* - MPEG-TS: Not supported.
|
||||
*/
|
||||
raw?: Record<string, string | Uint8Array | RichImageData | AttachedFile | Record<string, string> | null>;
|
||||
|
||||
+5
-2
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { Bitstream } from '../shared/bitstream';
|
||||
import { Logging } from './logging';
|
||||
|
||||
export function assert(x: unknown): asserts x {
|
||||
if (!x) {
|
||||
@@ -577,7 +578,7 @@ export const retriedFetch = async (
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error('Retrying failed fetch. Error:', error);
|
||||
Logging._error('Retrying failed fetch. Error:', error);
|
||||
|
||||
if (!Number.isFinite(retryDelayInSeconds) || retryDelayInSeconds < 0) {
|
||||
throw new TypeError('Retry delay must be a non-negative finite number.');
|
||||
@@ -1215,7 +1216,7 @@ export class EventEmitter<TEvents extends Record<string, unknown>> {
|
||||
/** @internal */
|
||||
_listeners = new Map<keyof TEvents, Set<{ fn: (data: never) => unknown; once: boolean }>>();
|
||||
|
||||
/** Registers a listener for the given event. */
|
||||
/** Registers a listener for the given event. Returns a function that, when called, removes the listener again. */
|
||||
on<K extends keyof TEvents>(
|
||||
event: K,
|
||||
listener: (data: TEvents[K]) => unknown,
|
||||
@@ -1246,6 +1247,8 @@ export class EventEmitter<TEvents extends Record<string, unknown>> {
|
||||
try {
|
||||
(entry.fn as (data: unknown) => void)(data);
|
||||
} catch (error) {
|
||||
// Deliberately not routed through Logging here: Logging emits via an EventEmitter, so a throwing
|
||||
// log listener would recurse straight back into this handler.
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,6 @@ import {
|
||||
determineVideoPacketType,
|
||||
extractAvcDecoderConfigurationRecord,
|
||||
extractHevcDecoderConfigurationRecord,
|
||||
extractNalUnitTypeForAvc,
|
||||
extractNalUnitTypeForHevc,
|
||||
EAC3_NUMBLKS_TABLE,
|
||||
getEac3ChannelCount,
|
||||
getEac3SampleRate,
|
||||
@@ -38,9 +36,12 @@ import {
|
||||
parseEac3SyncFrame,
|
||||
parseHevcSps,
|
||||
AC3_FRAME_SIZES,
|
||||
extractNalUnitTypeForAvc,
|
||||
extractNalUnitTypeForHevc,
|
||||
} from '../codec-data';
|
||||
import { Demuxer } from '../demuxer';
|
||||
import { Input } from '../input';
|
||||
import { Logging } from '../logging';
|
||||
import {
|
||||
InputAudioTrackBacking,
|
||||
InputTrackBacking,
|
||||
@@ -57,6 +58,7 @@ import {
|
||||
floorToMultiple,
|
||||
last,
|
||||
MATRIX_COEFFICIENTS_MAP_INVERSE,
|
||||
readExpGolomb,
|
||||
Rotation,
|
||||
roundIfAlmostInteger,
|
||||
toDataView,
|
||||
@@ -206,6 +208,12 @@ export class MpegTsDemuxer extends Demuxer {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasProgramMap && !this.elementaryStreams.some(x => x.pid === packetHeader.pid)) {
|
||||
// Don't care about this PID
|
||||
currentPos += this.packetStride;
|
||||
continue;
|
||||
}
|
||||
|
||||
const section = await this.readSection(
|
||||
currentPos,
|
||||
true,
|
||||
@@ -396,7 +404,7 @@ export class MpegTsDemuxer extends Demuxer {
|
||||
// we can't determine its metadata and also have no idea how to packetize its data.
|
||||
|
||||
if (!ignoredStreamTypes.has(streamType)) {
|
||||
console.warn(
|
||||
Logging._warn(
|
||||
`Note: MPEG-TS streams with stream_type 0x${streamType.toString(16)} are not`
|
||||
+ ` currently supported.`,
|
||||
);
|
||||
@@ -1976,6 +1984,9 @@ class PacketReadingContext {
|
||||
const elementaryStream = this.elementaryStream;
|
||||
|
||||
if (elementaryStream.info.type === 'video') {
|
||||
// Our job here is to separate the video stream into access units. Sometimes this is easy (like when AUDs
|
||||
// are present), sometimes it's a little harder.
|
||||
|
||||
const codec = elementaryStream.info.codec;
|
||||
const CHUNK_SIZE = 1024;
|
||||
|
||||
@@ -1983,7 +1994,10 @@ class PacketReadingContext {
|
||||
throw new Error('Unhandled.');
|
||||
}
|
||||
|
||||
const nalHeaderSize = codec === 'avc' ? 1 : 2;
|
||||
let packetStartPos: number | null = null;
|
||||
let frameStartFound = false;
|
||||
let lastFirstMacroblockInSlice = 0;
|
||||
|
||||
while (true) {
|
||||
let remaining = this.ensureBuffered(CHUNK_SIZE);
|
||||
@@ -2005,11 +2019,10 @@ class PacketReadingContext {
|
||||
}
|
||||
i = zeroIndex;
|
||||
|
||||
// Check if we have enough bytes to identify a start code
|
||||
const posBeforeZero = chunkStartPos + i;
|
||||
|
||||
// Need at least 4 more bytes after the 0x00 to check for start code + NAL type
|
||||
if (i + 4 >= length) {
|
||||
// Need 3 more bytes after the 0x00 to recognize a start code prefix
|
||||
if (i + 3 >= length) {
|
||||
// Not enough data in current chunk, seek back and let the next iteration handle it
|
||||
this.seekTo(posBeforeZero);
|
||||
break;
|
||||
@@ -2020,16 +2033,13 @@ class PacketReadingContext {
|
||||
const b3 = chunk[i + 3]!;
|
||||
|
||||
let startCodeLength = 0;
|
||||
let nalUnitTypeByte: number | null = null;
|
||||
|
||||
// Check for 4-byte start code (0x00000001)
|
||||
if (b1 === 0x00 && b2 === 0x00 && b3 === 0x01) {
|
||||
startCodeLength = 4;
|
||||
nalUnitTypeByte = chunk[i + 4]!;
|
||||
} else if (b1 === 0x00 && b2 === 0x01) {
|
||||
// 3-byte start code (0x000001)
|
||||
startCodeLength = 3;
|
||||
nalUnitTypeByte = b3;
|
||||
}
|
||||
|
||||
if (startCodeLength === 0) {
|
||||
@@ -2040,31 +2050,90 @@ class PacketReadingContext {
|
||||
|
||||
const startCodePos = posBeforeZero;
|
||||
|
||||
if (packetStartPos === null) {
|
||||
// This is our first start code, mark packet start
|
||||
packetStartPos = startCodePos;
|
||||
i += startCodeLength;
|
||||
continue;
|
||||
// The packet only really begins at the first NAL unit; anything before it isn't usable
|
||||
packetStartPos ??= startCodePos;
|
||||
|
||||
const nalHeaderStart = i + startCodeLength;
|
||||
const payloadStart = nalHeaderStart + nalHeaderSize;
|
||||
|
||||
// Bytes peeked from the start of a slice header to decode first_mb_in_slice. Six bytes (48 bits)
|
||||
// comfortably covers the exp-Golomb code for any realistic macroblock count
|
||||
const AVC_SLICE_HEADER_PEEK_SIZE = 6;
|
||||
|
||||
// We read the NAL header plus, for slices, the start of the slice header to decode the
|
||||
// first_mb_in_slice / first_slice_segment_in_pic_flag. Make sure all of it is buffered.
|
||||
const bytesNeeded = payloadStart + (codec === 'avc' ? AVC_SLICE_HEADER_PEEK_SIZE : 1);
|
||||
if (bytesNeeded > length) {
|
||||
this.seekTo(posBeforeZero);
|
||||
break;
|
||||
}
|
||||
|
||||
// We have a second start code. Check if it's an AUD.
|
||||
if (nalUnitTypeByte !== null) {
|
||||
const nalUnitType = codec === 'avc'
|
||||
? extractNalUnitTypeForAvc(nalUnitTypeByte)
|
||||
: extractNalUnitTypeForHevc(nalUnitTypeByte);
|
||||
const isAud = codec === 'avc'
|
||||
? nalUnitType === AvcNalUnitType.AUD
|
||||
: nalUnitType === HevcNalUnitType.AUD_NUT;
|
||||
const headerByte0 = chunk[nalHeaderStart]!;
|
||||
|
||||
if (isAud) {
|
||||
// End the packet at this start code (before the AUD)
|
||||
const packetLength = startCodePos - packetStartPos;
|
||||
this.seekTo(packetStartPos);
|
||||
return this.supplyPacket(packetLength, 0);
|
||||
let nalUnitType: number;
|
||||
let isSlice: boolean;
|
||||
let isAccessUnitStart: boolean;
|
||||
|
||||
if (codec === 'avc') {
|
||||
nalUnitType = extractNalUnitTypeForAvc(headerByte0);
|
||||
isSlice = nalUnitType === AvcNalUnitType.NON_IDR_SLICE
|
||||
|| nalUnitType === AvcNalUnitType.SLICE_DPA
|
||||
|| nalUnitType === AvcNalUnitType.IDR;
|
||||
isAccessUnitStart = nalUnitType === AvcNalUnitType.SEI
|
||||
|| nalUnitType === AvcNalUnitType.SPS
|
||||
|| nalUnitType === AvcNalUnitType.PPS
|
||||
|| nalUnitType === AvcNalUnitType.AUD;
|
||||
} else {
|
||||
nalUnitType = extractNalUnitTypeForHevc(headerByte0);
|
||||
const layerId = ((headerByte0 & 1) << 5) | (chunk[nalHeaderStart + 1]! >> 3);
|
||||
if (layerId > 0) {
|
||||
// Higher layers don't delimit the base-layer frames we care about
|
||||
i += startCodeLength;
|
||||
continue;
|
||||
}
|
||||
|
||||
// VCL slices: 0..RASL_R, plus the IRAP range BLA_W_LP..CRA_NUT
|
||||
isSlice = nalUnitType <= HevcNalUnitType.RASL_R
|
||||
|| (nalUnitType >= HevcNalUnitType.BLA_W_LP && nalUnitType <= 21);
|
||||
// VPS..FD, prefix SEI, and the reserved/unspecified non-VCL ranges
|
||||
isAccessUnitStart = (nalUnitType >= HevcNalUnitType.VPS_NUT && nalUnitType <= 37)
|
||||
|| nalUnitType === HevcNalUnitType.PREFIX_SEI_NUT
|
||||
|| (nalUnitType >= 41 && nalUnitType <= 44)
|
||||
|| (nalUnitType >= 48 && nalUnitType <= 55);
|
||||
}
|
||||
|
||||
let isFrameBoundary = false;
|
||||
|
||||
if (isSlice) {
|
||||
let startsNewPicture: boolean;
|
||||
|
||||
if (codec === 'avc') {
|
||||
const headerBytes = chunk.subarray(payloadStart, payloadStart + AVC_SLICE_HEADER_PEEK_SIZE);
|
||||
const firstMacroblockInSlice = readExpGolomb(new Bitstream(headerBytes));
|
||||
startsNewPicture = !frameStartFound || firstMacroblockInSlice <= lastFirstMacroblockInSlice;
|
||||
lastFirstMacroblockInSlice = firstMacroblockInSlice;
|
||||
} else {
|
||||
startsNewPicture = (chunk[payloadStart]! >> 7) === 1;
|
||||
}
|
||||
|
||||
if (startsNewPicture) {
|
||||
if (frameStartFound) {
|
||||
isFrameBoundary = true;
|
||||
} else {
|
||||
frameStartFound = true;
|
||||
}
|
||||
}
|
||||
} else if (isAccessUnitStart && frameStartFound) {
|
||||
isFrameBoundary = true;
|
||||
}
|
||||
|
||||
if (isFrameBoundary) {
|
||||
// End the packet at this start code (the next frame begins here)
|
||||
const packetLength = startCodePos - packetStartPos;
|
||||
this.seekTo(packetStartPos);
|
||||
return this.supplyPacket(packetLength, 0);
|
||||
}
|
||||
|
||||
// Not an AUD, continue searching
|
||||
i += startCodeLength;
|
||||
}
|
||||
|
||||
@@ -2074,8 +2143,8 @@ class PacketReadingContext {
|
||||
}
|
||||
}
|
||||
|
||||
// End of stream - return remaining data if we have a packet start
|
||||
if (packetStartPos !== null) {
|
||||
// End of stream - emit whatever's left as the final packet
|
||||
if (packetStartPos !== null && this.endPos > packetStartPos) {
|
||||
const packetLength = this.endPos - packetStartPos;
|
||||
this.seekTo(packetStartPos);
|
||||
return this.supplyPacket(packetLength, 0);
|
||||
|
||||
+5
-4
@@ -13,6 +13,7 @@ import { OutputFormat } from './output-format';
|
||||
import { AudioSource, MediaSource, SubtitleSource, VideoSource } from './media-source';
|
||||
import { PathedTarget, Target, TargetRequest } from './target';
|
||||
import { Writer } from './writer';
|
||||
import { Logging } from './logging';
|
||||
|
||||
/**
|
||||
* List of all track types.
|
||||
@@ -794,7 +795,7 @@ export class Output<
|
||||
}
|
||||
|
||||
if (this._startPromise) {
|
||||
console.warn('Output has already been started.');
|
||||
Logging._warn('Output has already been started.');
|
||||
return this._startPromise;
|
||||
}
|
||||
|
||||
@@ -831,13 +832,13 @@ export class Output<
|
||||
*/
|
||||
async cancel() {
|
||||
if (this._cancelPromise) {
|
||||
console.warn('Output has already been canceled.');
|
||||
Logging._warn('Output has already been canceled.');
|
||||
return this._cancelPromise;
|
||||
} else if (this.state === 'finalizing' || this.state === 'finalized') {
|
||||
// Don't wanna warn when finalizing since that shows a warning when finalization fails and then cancel
|
||||
// is called
|
||||
if (this.state === 'finalized') {
|
||||
console.warn('Output has already been finalized.');
|
||||
Logging._warn('Output has already been finalized.');
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -872,7 +873,7 @@ export class Output<
|
||||
throw new Error('Cannot finalize after canceling.');
|
||||
}
|
||||
if (this._finalizePromise) {
|
||||
console.warn('Output has already been finalized.');
|
||||
Logging._warn('Output has already been finalized.');
|
||||
return this._finalizePromise;
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -32,6 +32,7 @@ import {
|
||||
MaybePromise,
|
||||
DeepReadonly,
|
||||
} from './misc';
|
||||
import { Logging } from './logging';
|
||||
|
||||
polyfillSymbolDispose();
|
||||
|
||||
@@ -55,7 +56,7 @@ if (typeof FinalizationRegistry !== 'undefined') {
|
||||
if (value.type === 'video') {
|
||||
if (now - lastVideoGcErrorLog >= 1000) {
|
||||
// This error is annoying but oh so important
|
||||
console.error(
|
||||
Logging._error(
|
||||
`A VideoSample was garbage collected without first being closed. For proper resource management,`
|
||||
+ ` make sure to call close() on all your VideoSamples as soon as you're done using them.`,
|
||||
);
|
||||
@@ -68,7 +69,7 @@ if (typeof FinalizationRegistry !== 'undefined') {
|
||||
}
|
||||
} else {
|
||||
if (now - lastAudioGcErrorLog >= 1000) {
|
||||
console.error(
|
||||
Logging._error(
|
||||
`An AudioSample was garbage collected without first being closed. For proper resource management,`
|
||||
+ ` make sure to call close() on all your AudioSamples as soon as you're done using them.`,
|
||||
);
|
||||
|
||||
+4
-4
@@ -28,6 +28,7 @@ import {
|
||||
} from './misc';
|
||||
import * as nodeAlias from './node';
|
||||
import { InputDisposedError } from './input';
|
||||
import { Logging } from './logging';
|
||||
|
||||
polyfillSymbolDispose();
|
||||
|
||||
@@ -658,7 +659,7 @@ const DEFAULT_RETRY_DELAY
|
||||
= typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean' ? navigator.onLine : true;
|
||||
|
||||
if (isOnline && originOfSrc !== null && originOfSrc !== window.location.origin) {
|
||||
console.warn(
|
||||
Logging._warn(
|
||||
`Request will not be retried because a CORS error was suspected due to different origins. You can`
|
||||
+ ` modify this behavior by providing your own function for the 'getRetryDelay' option.`,
|
||||
);
|
||||
@@ -955,8 +956,7 @@ export class UrlSource extends PathedSource {
|
||||
&& !(url.pathname.endsWith('.m3u8') || url.pathname.endsWith('.m3u'))
|
||||
) {
|
||||
if (!warnedOrigins.has(url.origin)) {
|
||||
console.log(this._usedForHls, this._url, url.pathname);
|
||||
console.warn(
|
||||
Logging._warn(
|
||||
`HTTP server (origin ${url.origin}) did not respond to a range request with 206 Partial`
|
||||
+ ' Content, meaning the entire resource will now be downloaded. To enable efficient'
|
||||
+ ' media file streaming across a network, please make sure your server supports'
|
||||
@@ -1010,7 +1010,7 @@ export class UrlSource extends PathedSource {
|
||||
|
||||
const retryDelayInSeconds = this._getRetryDelay(1, error, this._url);
|
||||
if (retryDelayInSeconds !== null) {
|
||||
console.error('Error while reading response stream. Attempting to resume.', error);
|
||||
Logging._error('Error while reading response stream. Attempting to resume.', error);
|
||||
await wait(1000 * retryDelayInSeconds);
|
||||
|
||||
break;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { WavOutputFormat } from '../output-format';
|
||||
import { assert, assertNever, isIso88591Compatible, keyValueIterator } from '../misc';
|
||||
import { MetadataTags, metadataTagsAreEmpty } from '../metadata';
|
||||
import { Id3V2Writer } from '../id3';
|
||||
import { Logging } from '../logging';
|
||||
|
||||
export class WaveMuxer extends Muxer {
|
||||
private format: WavOutputFormat;
|
||||
@@ -206,7 +207,7 @@ export class WaveMuxer extends Muxer {
|
||||
const writeInfoTag = (tag: string, value: string) => {
|
||||
if (!isIso88591Compatible(value)) {
|
||||
// No Unicode supported here
|
||||
console.warn(`Didn't write tag '${tag}' because '${value}' is not ISO 8859-1-compatible.`);
|
||||
Logging._warn(`Didn't write tag '${tag}' because '${value}' is not ISO 8859-1-compatible.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { assert, toUint8Array } from '../../src/misc.js';
|
||||
import { Input } from '../../src/input.js';
|
||||
import { BufferSource, FilePathSource } from '../../src/source.js';
|
||||
@@ -284,3 +285,59 @@ test('appendOnly writes correct STREAMINFO header', async () => {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]));
|
||||
});
|
||||
|
||||
test('can read a FLAC file with leading ID3v2 tags', async () => {
|
||||
const createId3V23TitleTag = (title: string) => {
|
||||
const titleBytes = new TextEncoder().encode(title);
|
||||
const frame = new Uint8Array(11 + titleBytes.length);
|
||||
frame.set([0x54, 0x49, 0x54, 0x32]); // TIT2
|
||||
frame[7] = 1 + titleBytes.length;
|
||||
frame[10] = 0; // ISO-8859-1
|
||||
frame.set(titleBytes, 11);
|
||||
|
||||
const tag = new Uint8Array(10 + frame.length);
|
||||
tag.set([0x49, 0x44, 0x33, 0x03, 0x00, 0x00]); // ID3v2.3
|
||||
tag[9] = frame.length;
|
||||
tag.set(frame, 10);
|
||||
|
||||
return tag;
|
||||
};
|
||||
|
||||
const concatenateBytes = (...chunks: Uint8Array[]) => {
|
||||
const result = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0));
|
||||
let offset = 0;
|
||||
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.byteLength;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
using input = new Input({
|
||||
source: new BufferSource(concatenateBytes(
|
||||
createId3V23TitleTag('First tag'),
|
||||
createId3V23TitleTag('Second tag'),
|
||||
await fs.readFile(new URL('../public/sample.flac', import.meta.url)),
|
||||
)),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
expect(await input.canRead()).toBe(true);
|
||||
expect(await input.getFormat()).toBe(FLAC);
|
||||
|
||||
const track = await input.getPrimaryAudioTrack();
|
||||
assert(track);
|
||||
expect(await track.getDurationFromMetadata()).toEqual(19.714285714285715);
|
||||
|
||||
const firstPacket = await new EncodedPacketSink(track).getPacket(0);
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.sequenceNumber).toBe(0);
|
||||
expect(firstPacket.timestamp).toBe(0);
|
||||
|
||||
const metadataTags = await input.getMetadataTags();
|
||||
expect(metadataTags.title).toBe('First tag');
|
||||
expect(metadataTags.raw!['TIT2']).toBe('First tag');
|
||||
expect(metadataTags.raw!['TITLE']).toBe('The Happy Meeting');
|
||||
});
|
||||
|
||||
@@ -354,7 +354,7 @@ test('MPEG-TS seeking race condition test', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('MPEG-TS video key packets', async () => {
|
||||
test('MPEG-TS video key packets', { timeout: 10_000 }, async () => {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/trim-buck-bunny-ffmpeg.ts')),
|
||||
@@ -875,3 +875,26 @@ test('MPEG-TS with "extension" PES packets without PTS', async () => {
|
||||
4693, 223, 144, 174, 118, 9155, 1188, 379, 169, 213,
|
||||
]);
|
||||
});
|
||||
|
||||
test('MPEG-TS with AUD-less video packets', async () => {
|
||||
using input = new Input({
|
||||
source: new FilePathSource(path.join(__dirname, '../public/no-aud.ts')),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
assert(videoTrack);
|
||||
|
||||
const sink = new EncodedPacketSink(videoTrack);
|
||||
|
||||
const firstPacket = await sink.getFirstPacket();
|
||||
assert(firstPacket);
|
||||
const secondPacket = await sink.getNextPacket(firstPacket);
|
||||
assert(secondPacket);
|
||||
const thirdPacket = await sink.getNextPacket(secondPacket);
|
||||
assert(thirdPacket);
|
||||
|
||||
expect(firstPacket.data.byteLength).toBe(331774);
|
||||
expect(secondPacket.data.byteLength).toBe(1749);
|
||||
expect(thirdPacket.data.byteLength).toBe(4273);
|
||||
});
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user