mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
Add Logging singleton to manually control Mediabunny's console output (fixes #415)
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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,'
|
||||
|
||||
@@ -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,'
|
||||
|
||||
@@ -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,'
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.',
|
||||
);
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
-3
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user