Add Logging singleton to manually control Mediabunny's console output (fixes #415)

This commit is contained in:
Vanilagy
2026-06-18 19:32:18 +02:00
parent 92a384f418
commit cdc2ef63dd
28 changed files with 324 additions and 65 deletions
+1
View File
@@ -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.",
+34
View File
@@ -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`.
+3 -1
View File
@@ -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,'
+3 -1
View File
@@ -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,'
+3 -1
View File
@@ -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,'
+2 -2
View File
@@ -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,'
+34
View File
@@ -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`.
+2 -1
View File
@@ -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,'
+1
View File
@@ -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)
+38 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+4 -3
View File
@@ -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.',
);
+4 -3
View File
@@ -7,6 +7,7 @@
*/
import { decodeSynchsafe, encodeSynchsafe } from '../shared/mp3-misc';
import { Logging } from './logging';
import { MetadataTags } from './metadata';
import {
coalesceIndex,
@@ -165,7 +166,7 @@ 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;
}
@@ -214,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;
}
+8 -1
View File
@@ -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';
+3 -2
View File
@@ -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;
}
}
+18 -17
View File
@@ -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
View File
@@ -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);
}
}
}
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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.',
);
+5 -2
View File
@@ -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);
}
+2 -1
View File
@@ -41,6 +41,7 @@ import {
} from '../codec-data';
import { Demuxer } from '../demuxer';
import { Input } from '../input';
import { Logging } from '../logging';
import {
InputAudioTrackBacking,
InputTrackBacking,
@@ -403,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.`,
);
+5 -4
View File
@@ -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
View File
@@ -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 -3
View File
@@ -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,7 +956,7 @@ export class UrlSource extends PathedSource {
&& !(url.pathname.endsWith('.m3u8') || url.pathname.endsWith('.m3u'))
) {
if (!warnedOrigins.has(url.origin)) {
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'
@@ -1009,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;
+2 -1
View File
@@ -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;
}