From a7386fc6c380a66eb4bc16c98d56928a173d1bd9 Mon Sep 17 00:00:00 2001
From: Vanilagy <1696106+Vanilagy@users.noreply.github.com>
Date: Sun, 1 Jun 2025 18:14:37 +0200
Subject: [PATCH] Add file compression example
---
docs/examples.md | 6 +-
examples/file-compression/file-compression.ts | 137 +++++++++
examples/file-compression/index.html | 54 ++++
examples/media-player/index.html | 2 +-
examples/metadata-extraction/index.html | 2 +-
examples/thumbnail-generation/index.html | 2 +-
src/codec.ts | 34 ++-
src/conversion.ts | 120 ++++----
src/input.ts | 4 +-
src/media-source.ts | 261 +++++++++---------
10 files changed, 413 insertions(+), 209 deletions(-)
create mode 100644 examples/file-compression/file-compression.ts
create mode 100644 examples/file-compression/index.html
diff --git a/docs/examples.md b/docs/examples.md
index 967b44e..c5d7354 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -18,8 +18,10 @@ features:
details: "A full video & audio media player, implemented from scratch with Mediakit, with microsecond playback accuracy."
link: /examples/media-player
target: _self
- - title: Feature A
- details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
+ - title: File compression
+ details: Convert an input file to a highly-compressed MP4 file.
+ link: /examples/file-compression
+ target: _self
- title: Feature B
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
- title: Feature C
diff --git a/examples/file-compression/file-compression.ts b/examples/file-compression/file-compression.ts
new file mode 100644
index 0000000..a3e9c46
--- /dev/null
+++ b/examples/file-compression/file-compression.ts
@@ -0,0 +1,137 @@
+import {
+ Input,
+ ALL_FORMATS,
+ BlobSource,
+ Output,
+ BufferTarget,
+ Mp4OutputFormat,
+ Conversion,
+ QUALITY_VERY_LOW,
+} from 'mediakit';
+
+const selectMediaButton = document.querySelector('button') as HTMLButtonElement;
+const fileNameElement = document.querySelector('#file-name') as HTMLParagraphElement;
+const horizontalRule = document.querySelector('hr') as HTMLHRElement;
+const progressBarContainer = document.querySelector('#progress-bar-container') as HTMLDivElement;
+const progressBar = document.querySelector('#progress-bar') as HTMLDivElement;
+const speedometer = document.querySelector('#speedometer') as HTMLParagraphElement;
+const videoElement = document.querySelector('video') as HTMLVideoElement;
+const compressionFacts = document.querySelector('#compression-facts') as HTMLParagraphElement;
+const errorElement = document.querySelector('#error-element') as HTMLParagraphElement;
+
+let currentConversion: Conversion | null = null;
+let currentIntervalId = -1;
+
+const compressFile = async (file: File) => {
+ clearInterval(currentIntervalId);
+ await currentConversion?.cancel();
+
+ fileNameElement.textContent = file.name;
+ horizontalRule.style.display = '';
+ progressBarContainer.style.display = '';
+ speedometer.style.display = '';
+ speedometer.textContent = 'Speed: -';
+ videoElement.style.display = 'none';
+ videoElement.src = '';
+ errorElement.textContent = '';
+
+ try {
+ // Create a new input from the file
+ const input = new Input({
+ source: new BlobSource(file),
+ formats: ALL_FORMATS, // Accept all formats
+ });
+
+ // Define the output file
+ const output = new Output({
+ target: new BufferTarget(),
+ format: new Mp4OutputFormat(),
+ });
+
+ // Initialize the conversion process
+ currentConversion = await Conversion.init({
+ input,
+ output,
+ video: {
+ width: 320, // Height will be deduced automatically to retain aspect ratio
+ bitrate: QUALITY_VERY_LOW,
+ },
+ audio: {
+ bitrate: 32e3,
+ },
+ });
+
+ // Keep track of progress
+ let progress = 0;
+ currentConversion.onProgress = newProgress => progress = newProgress;
+
+ const fileDuration = await input.computeDuration();
+ const startTime = performance.now();
+
+ const updateProgress = () => {
+ progressBar.style.width = `${progress * 100}%`;
+
+ const now = performance.now();
+ const elapsedSeconds = (now - startTime) / 1000;
+ const factor = fileDuration / (elapsedSeconds / progress);
+ speedometer.textContent = `Speed: ~${factor.toPrecision(3)}x real time`;
+ };
+
+ // Update the progress indicator regularly
+ currentIntervalId = window.setInterval(updateProgress, 1000 / 60);
+
+ // Start the conversion process
+ await currentConversion.execute();
+
+ clearInterval(currentIntervalId);
+ updateProgress();
+
+ // Display the final media file
+ videoElement.style.display = '';
+ videoElement.src = URL.createObjectURL(new Blob([output.target.buffer!]));
+ void videoElement.play();
+
+ compressionFacts.style.display = '';
+ compressionFacts.textContent
+ = `${(output.target.buffer!.byteLength / file.size * 100).toPrecision(3)}% of original size`;
+ } catch (error) {
+ errorElement.textContent = String(error);
+ clearInterval(currentIntervalId);
+
+ progressBarContainer.style.display = 'none';
+ speedometer.style.display = 'none';
+ compressionFacts.style.display = 'none';
+ videoElement.style.display = 'none';
+ }
+};
+
+/** === FILE SELECTION LOGIC === */
+
+selectMediaButton.addEventListener('click', () => {
+ const fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.addEventListener('change', () => {
+ const file = fileInput.files?.[0];
+ if (!file) {
+ return;
+ }
+
+ void compressFile(file);
+ });
+
+ fileInput.click();
+});
+
+document.addEventListener('dragover', (event) => {
+ event.preventDefault();
+ event.dataTransfer!.dropEffect = 'copy';
+});
+
+document.addEventListener('drop', (event) => {
+ event.preventDefault();
+ const files = event.dataTransfer?.files;
+ const file = files && files.length > 0 ? files[0] : undefined;
+ if (file) {
+ void compressFile(file);
+ }
+});
diff --git a/examples/file-compression/index.html b/examples/file-compression/index.html
new file mode 100644
index 0000000..8e667c5
--- /dev/null
+++ b/examples/file-compression/index.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+ File compression example | Mediakit
+
+
+
+
+
+
+ File compression example
+ Select or drop a media file, and Mediakit will convert it to a heavily-compressed MP4 file.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
Mediakit
+
+
+
+
+ View source code
+
+
+
diff --git a/examples/media-player/index.html b/examples/media-player/index.html
index 14d0861..bba598c 100644
--- a/examples/media-player/index.html
+++ b/examples/media-player/index.html
@@ -25,7 +25,7 @@
-
+
diff --git a/examples/metadata-extraction/index.html b/examples/metadata-extraction/index.html
index f89bfef..e1847bf 100644
--- a/examples/metadata-extraction/index.html
+++ b/examples/metadata-extraction/index.html
@@ -25,7 +25,7 @@
-
+
diff --git a/examples/thumbnail-generation/index.html b/examples/thumbnail-generation/index.html
index ae16da7..7725c5a 100644
--- a/examples/thumbnail-generation/index.html
+++ b/examples/thumbnail-generation/index.html
@@ -25,7 +25,7 @@
-
+
diff --git a/src/codec.ts b/src/codec.ts
index 34aa50f..b772b17 100644
--- a/src/codec.ts
+++ b/src/codec.ts
@@ -1143,7 +1143,7 @@ export const canEncode = (codec: MediaCodec) => {
export const canEncodeVideo = async (codec: VideoCodec, { width = 1280, height = 720, bitrate = 1e6 }: {
width?: number;
height?: number;
- bitrate?: number;
+ bitrate?: number | Quality;
} = {}) => {
if (!VIDEO_CODECS.includes(codec)) {
return false;
@@ -1154,21 +1154,25 @@ export const canEncodeVideo = async (codec: VideoCodec, { width = 1280, height =
if (!Number.isInteger(height) || height <= 0) {
throw new TypeError('height must be a positive integer.');
}
- if (!Number.isInteger(bitrate) || bitrate <= 0) {
- throw new TypeError('bitrate must be a positive integer.');
+ if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
+ throw new TypeError('bitrate must be a positive integer or a quality.');
}
+ const resolvedBitrate = bitrate instanceof Quality
+ ? bitrate._toVideoBitrate(codec, width, height)
+ : bitrate;
+
if (customVideoEncoders.length > 0) {
const encoderConfig: VideoEncoderConfig = {
codec: buildVideoCodecString(
codec,
width,
height,
- bitrate,
+ resolvedBitrate,
),
width,
height,
- bitrate,
+ bitrate: resolvedBitrate,
...getVideoEncoderConfigExtension(codec),
};
@@ -1183,10 +1187,10 @@ export const canEncodeVideo = async (codec: VideoCodec, { width = 1280, height =
}
const support = await VideoEncoder.isConfigSupported({
- codec: buildVideoCodecString(codec, width, height, bitrate),
+ codec: buildVideoCodecString(codec, width, height, resolvedBitrate),
width,
height,
- bitrate,
+ bitrate: resolvedBitrate,
...getVideoEncoderConfigExtension(codec),
});
@@ -1200,7 +1204,7 @@ export const canEncodeVideo = async (codec: VideoCodec, { width = 1280, height =
export const canEncodeAudio = async (codec: AudioCodec, { numberOfChannels = 2, sampleRate = 48000, bitrate = 128e3 }: {
numberOfChannels?: number;
sampleRate?: number;
- bitrate?: number;
+ bitrate?: number | Quality;
} = {}) => {
if (!AUDIO_CODECS.includes(codec)) {
return false;
@@ -1211,10 +1215,14 @@ export const canEncodeAudio = async (codec: AudioCodec, { numberOfChannels = 2,
if (!Number.isInteger(sampleRate) || sampleRate <= 0) {
throw new TypeError('sampleRate must be a positive integer.');
}
- if (!Number.isInteger(bitrate) || bitrate <= 0) {
+ if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
throw new TypeError('bitrate must be a positive integer.');
}
+ const resolvedBitrate = bitrate instanceof Quality
+ ? bitrate._toAudioBitrate(codec)
+ : bitrate;
+
if (customAudioEncoders.length > 0) {
const encoderConfig: AudioEncoderConfig = {
codec: buildAudioCodecString(
@@ -1224,7 +1232,7 @@ export const canEncodeAudio = async (codec: AudioCodec, { numberOfChannels = 2,
),
numberOfChannels,
sampleRate,
- bitrate,
+ bitrate: resolvedBitrate,
...getAudioEncoderConfigExtension(codec),
};
@@ -1246,7 +1254,7 @@ export const canEncodeAudio = async (codec: AudioCodec, { numberOfChannels = 2,
codec: buildAudioCodecString(codec, numberOfChannels, sampleRate),
numberOfChannels,
sampleRate,
- bitrate,
+ bitrate: resolvedBitrate,
...getAudioEncoderConfigExtension(codec),
});
@@ -1288,7 +1296,7 @@ export const getEncodableVideoCodecs = async (
options?: {
width?: number;
height?: number;
- bitrate?: number;
+ bitrate?: number | Quality;
},
): Promise => {
const bools = await Promise.all(checkedCodecs.map(codec => canEncodeVideo(codec, options)));
@@ -1304,7 +1312,7 @@ export const getEncodableAudioCodecs = async (
options?: {
numberOfChannels?: number;
sampleRate?: number;
- bitrate?: number;
+ bitrate?: number | Quality;
},
): Promise => {
const bools = await Promise.all(checkedCodecs.map(codec => canEncodeAudio(codec, options)));
diff --git a/src/conversion.ts b/src/conversion.ts
index 03e8f07..b488336 100644
--- a/src/conversion.ts
+++ b/src/conversion.ts
@@ -100,12 +100,6 @@ export type ConversionOptions = {
/** The time in the input file at which the output file should end. Must be greater than `start`. */
end: number;
};
-
- /**
- * When set to true, the current progress of the conversion will be computed and kept up to date in the `progress`
- * field of the Conversion instance.
- */
- computeProgress?: boolean;
};
const FALLBACK_NUMBER_OF_CHANNELS = 2;
@@ -160,16 +154,17 @@ export class Conversion {
_canceled = false;
/**
- * A number between 0 and 1, indicating the completion of the conversion. If the `computeProgress` option is not
- * enabled, this value will be stuck at 0. Note that a progress of 1 doesn't necessarily mean the conversion is
- * complete; the conversion is complete once `execute` resolves.
- */
- progress = 0;
- /**
- * A callback that is fired whenever the conversion progresses. Only called if the `computeProgress` option
- * is enabled.
+ * A callback that is fired whenever the conversion progresses. Returns a number between 0 and 1, indicating the
+ * completion of the conversion. Note that a progress of 1 doesn't necessarily mean the conversion is complete;
+ * the conversion is complete once `execute` resolves.
+ *
+ * In order for progress to be computed, this property must be set before `execute` is called.
*/
onProgress?: (progress: number) => unknown = undefined;
+ /** @internal */
+ _computeProgress = false;
+ /** @internal */
+ _lastProgress = 0;
/** The list of tracks that are included in the output file. */
utilizedTracks: InputTrack[] = [];
@@ -179,12 +174,12 @@ export class Conversion {
track: InputTrack;
/** The reason for discarding the track. */
reason:
- | 'discardedByUser'
- | 'maxTrackCountReached'
- | 'maxTrackCountOfTypeReached'
- | 'unknownSourceCodec'
- | 'undecodableSourceCodec'
- | 'noEncodableTargetCodec';
+ | 'discarded_by_user'
+ | 'max_track_count_reached'
+ | 'max_track_count_of_type_reached'
+ | 'unknown_source_codec'
+ | 'undecodable_source_codec'
+ | 'no_encodable_target_codec';
}[] = [];
/** Initializes a new conversion process without starting the conversion. */
@@ -302,9 +297,6 @@ export class Conversion {
&& options.trim.start >= options.trim.end) {
throw new TypeError('options.trim.start must be less than options.trim.end.');
}
- if (options.computeProgress !== undefined && typeof options.computeProgress !== 'boolean') {
- throw new TypeError('options.computeProgress, when provided, must be a boolean.');
- }
this._options = options;
this._input = options.input;
@@ -327,7 +319,7 @@ export class Conversion {
if (track.isVideoTrack() && this._options.video?.discard) {
this.discardedTracks.push({
track,
- reason: 'discardedByUser',
+ reason: 'discarded_by_user',
});
continue;
}
@@ -335,7 +327,7 @@ export class Conversion {
if (track.isAudioTrack() && this._options.audio?.discard) {
this.discardedTracks.push({
track,
- reason: 'discardedByUser',
+ reason: 'discarded_by_user',
});
continue;
}
@@ -343,7 +335,7 @@ export class Conversion {
if (this._totalTrackCount === outputTrackCounts.total.max) {
this.discardedTracks.push({
track,
- reason: 'maxTrackCountReached',
+ reason: 'max_track_count_reached',
});
continue;
}
@@ -351,7 +343,7 @@ export class Conversion {
if (this._addedCounts[track.type] === outputTrackCounts[track.type].max) {
this.discardedTracks.push({
track,
- reason: 'maxTrackCountOfTypeReached',
+ reason: 'max_track_count_of_type_reached',
});
continue;
}
@@ -363,22 +355,14 @@ export class Conversion {
}
}
- const unintentionallyDiscardedTracks = this.discardedTracks.filter(x => x.reason !== 'discardedByUser');
+ const unintentionallyDiscardedTracks = this.discardedTracks.filter(x => x.reason !== 'discarded_by_user');
if (unintentionallyDiscardedTracks.length > 0) {
// Let's give the user a notice/warning about discarded tracks so they aren't confused
console.warn('Some tracks had to be discarded from the conversion:', unintentionallyDiscardedTracks);
}
-
- if (this._options.computeProgress) {
- this._totalDuration = Math.min(
- await this._input.computeDuration() - this._startTimestamp,
- this._endTimestamp - this._startTimestamp,
- );
- this.onProgress?.(this.progress);
- }
}
- /** Starts the conversion process. */
+ /** Executes the conversion process. Resolves once conversion is complete. */
async execute() {
if (this._executed) {
throw new Error('Conversion cannot be executed twice.');
@@ -386,10 +370,28 @@ export class Conversion {
this._executed = true;
+ if (this.onProgress) {
+ this._computeProgress = true;
+ this._totalDuration = Math.min(
+ await this._input.computeDuration() - this._startTimestamp,
+ this._endTimestamp - this._startTimestamp,
+ );
+ this.onProgress?.(0);
+ }
+
await this._output.start();
this._start();
- await Promise.all(this._trackPromises);
+ try {
+ await Promise.all(this._trackPromises);
+ } catch (error) {
+ if (!this._canceled) {
+ // Make sure to cancel to stop other encoding processes and clean up resources
+ await this.cancel();
+ }
+
+ throw error;
+ }
if (this._canceled) {
await new Promise(() => {}); // Never resolve
@@ -397,9 +399,8 @@ export class Conversion {
await this._output.finalize();
- if (this._options.computeProgress && this.progress !== 1) {
- this.progress = 1;
- this.onProgress?.(this.progress);
+ if (this._computeProgress) {
+ this.onProgress?.(1);
}
}
@@ -424,7 +425,7 @@ export class Conversion {
if (!sourceCodec) {
this.discardedTracks.push({
track,
- reason: 'unknownSourceCodec',
+ reason: 'unknown_source_codec',
});
return;
}
@@ -508,7 +509,7 @@ export class Conversion {
if (!canDecode) {
this.discardedTracks.push({
track,
- reason: 'undecodableSourceCodec',
+ reason: 'undecodable_source_codec',
});
return;
}
@@ -517,18 +518,20 @@ export class Conversion {
videoCodecs = videoCodecs.filter(codec => codec === this._options.video?.codec);
}
- const encodableCodecs = await getEncodableVideoCodecs(videoCodecs, { width, height });
+ const bitrate = this._options.video?.bitrate ?? QUALITY_HIGH;
+
+ const encodableCodecs = await getEncodableVideoCodecs(videoCodecs, { width, height, bitrate });
if (encodableCodecs.length === 0) {
this.discardedTracks.push({
track,
- reason: 'noEncodableTargetCodec',
+ reason: 'no_encodable_target_codec',
});
return;
}
const encodingConfig: VideoEncodingConfig = {
codec: encodableCodecs[0]!,
- bitrate: this._options.video?.bitrate ?? QUALITY_HIGH,
+ bitrate,
onEncodedPacket: sample => this._reportProgress(track.id, sample.timestamp + sample.duration),
};
@@ -612,7 +615,7 @@ export class Conversion {
if (!sourceCodec) {
this.discardedTracks.push({
track,
- reason: 'unknownSourceCodec',
+ reason: 'unknown_source_codec',
});
return;
}
@@ -677,7 +680,7 @@ export class Conversion {
if (!canDecode) {
this.discardedTracks.push({
track,
- reason: 'undecodableSourceCodec',
+ reason: 'undecodable_source_codec',
});
return;
}
@@ -688,9 +691,12 @@ export class Conversion {
audioCodecs = audioCodecs.filter(codec => codec === this._options.audio!.codec);
}
+ const bitrate = this._options.audio?.bitrate ?? QUALITY_HIGH;
+
const encodableCodecs = await getEncodableAudioCodecs(audioCodecs, {
numberOfChannels,
sampleRate,
+ bitrate,
});
if (
@@ -705,6 +711,7 @@ export class Conversion {
const encodableCodecsWithDefaultParams = await getEncodableAudioCodecs(audioCodecs, {
numberOfChannels: FALLBACK_NUMBER_OF_CHANNELS,
sampleRate: FALLBACK_SAMPLE_RATE,
+ bitrate,
});
if (
@@ -724,17 +731,17 @@ export class Conversion {
if (codecOfChoice === null) {
this.discardedTracks.push({
track,
- reason: 'noEncodableTargetCodec',
+ reason: 'no_encodable_target_codec',
});
return;
}
if (needsResample) {
- audioSource = this._resampleAudio(track, codecOfChoice, numberOfChannels, sampleRate);
+ audioSource = this._resampleAudio(track, codecOfChoice, numberOfChannels, sampleRate, bitrate);
} else {
const source = new AudioSampleSource({
codec: codecOfChoice,
- bitrate: this._options.audio?.bitrate ?? QUALITY_HIGH,
+ bitrate,
onEncodedPacket: packet => this._reportProgress(track.id, packet.timestamp + packet.duration),
});
audioSource = source;
@@ -777,10 +784,11 @@ export class Conversion {
codec: AudioCodec,
targetNumberOfChannels: number,
targetSampleRate: number,
+ bitrate: number | Quality,
) {
const source = new AudioSampleSource({
codec,
- bitrate: this._options.audio?.bitrate ?? QUALITY_HIGH,
+ bitrate,
onEncodedPacket: packet => this._reportProgress(track.id, packet.timestamp + packet.duration),
});
@@ -823,7 +831,7 @@ export class Conversion {
/** @internal */
_reportProgress(trackId: number, endTimestamp: number) {
- if (!this._options.computeProgress) {
+ if (!this._computeProgress) {
return;
}
assert(this._totalDuration !== null);
@@ -838,9 +846,9 @@ export class Conversion {
const averageTimestamp = totalTimestamps / this._totalTrackCount;
const newProgress = clamp(averageTimestamp / this._totalDuration, 0, 1);
- if (newProgress !== this.progress) {
- this.progress = newProgress;
- this.onProgress?.(this.progress);
+ if (newProgress !== this._lastProgress) {
+ this._lastProgress = newProgress;
+ this.onProgress?.(newProgress);
}
}
}
diff --git a/src/input.ts b/src/input.ts
index 8a12fc2..eb724c2 100644
--- a/src/input.ts
+++ b/src/input.ts
@@ -84,8 +84,8 @@ export class Input {
}
/**
- * Computes the duration of the longest track in this input file, in seconds. More precisely, returns the largest
- * end timestamp among all tracks.
+ * Computes the duration of the input file, in seconds. More precisely, returns the largest end timestamp among
+ * all tracks.
*/
async computeDuration() {
const demuxer = await this._getDemuxer();
diff --git a/src/media-source.ts b/src/media-source.ts
index 3269d23..009fd7d 100644
--- a/src/media-source.ts
+++ b/src/media-source.ts
@@ -16,7 +16,7 @@ import {
VideoCodec,
} from './codec';
import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from './output';
-import { assert, CallSerializer, clamp, promiseWithResolvers, setInt24, setUint24 } from './misc';
+import { assert, CallSerializer, clamp, setInt24, setUint24 } from './misc';
import { Muxer } from './muxer';
import { SubtitleParser } from './subtitles';
import { toAlaw, toUlaw } from './pcm';
@@ -345,85 +345,82 @@ class VideoEncoderWrapper {
return;
}
- const { promise, resolve } = promiseWithResolvers();
- this.ensureEncoderPromise = promise;
+ return this.ensureEncoderPromise = (async () => {
+ const width = videoSample.codedWidth;
+ const height = videoSample.codedHeight;
+ const bitrate = this.encodingConfig.bitrate instanceof Quality
+ ? this.encodingConfig.bitrate._toVideoBitrate(this.encodingConfig.codec, width, height)
+ : this.encodingConfig.bitrate;
- const width = videoSample.codedWidth;
- const height = videoSample.codedHeight;
- const bitrate = this.encodingConfig.bitrate instanceof Quality
- ? this.encodingConfig.bitrate._toVideoBitrate(this.encodingConfig.codec, width, height)
- : this.encodingConfig.bitrate;
-
- const encoderConfig: VideoEncoderConfig = {
- codec: this.encodingConfig.fullCodecString ?? buildVideoCodecString(
- this.encodingConfig.codec,
+ const encoderConfig: VideoEncoderConfig = {
+ codec: this.encodingConfig.fullCodecString ?? buildVideoCodecString(
+ this.encodingConfig.codec,
+ width,
+ height,
+ bitrate,
+ ),
width,
height,
bitrate,
- ),
- width,
- height,
- bitrate,
- framerate: this.source._connectedTrack?.metadata.frameRate,
- latencyMode: this.encodingConfig.latencyMode,
- ...getVideoEncoderConfigExtension(this.encodingConfig.codec),
- };
- this.encodingConfig.onEncoderConfig?.(encoderConfig);
-
- const MatchingCustomEncoder = customVideoEncoders.find(x => x.supports(
- this.encodingConfig.codec,
- encoderConfig,
- ));
-
- if (MatchingCustomEncoder) {
- // @ts-expect-error "Can't create instance of abstract class 🤓"
- this.customEncoder = new MatchingCustomEncoder() as CustomVideoEncoder;
- this.customEncoder.codec = this.encodingConfig.codec;
- this.customEncoder.config = encoderConfig;
- this.customEncoder.onPacket = (packet, meta) => {
- if (!(packet instanceof EncodedPacket)) {
- throw new TypeError('The first argument passed to onPacket must be an EncodedPacket.');
- }
- if (meta !== undefined && (!meta || typeof meta !== 'object')) {
- throw new TypeError('The second argument passed to onPacket must be an object or undefined.');
- }
-
- this.encodingConfig.onEncodedPacket?.(packet, meta);
- void this.muxer!.addEncodedVideoPacket(this.source._connectedTrack!, packet, meta);
+ framerate: this.source._connectedTrack?.metadata.frameRate,
+ latencyMode: this.encodingConfig.latencyMode,
+ ...getVideoEncoderConfigExtension(this.encodingConfig.codec),
};
+ this.encodingConfig.onEncoderConfig?.(encoderConfig);
- await this.customEncoder.init();
- } else {
- if (typeof VideoEncoder === 'undefined') {
- throw new Error('VideoEncoder is not supported by this browser.');
- }
+ const MatchingCustomEncoder = customVideoEncoders.find(x => x.supports(
+ this.encodingConfig.codec,
+ encoderConfig,
+ ));
- const support = await VideoEncoder.isConfigSupported(encoderConfig);
- if (!support.supported) {
- throw new Error(
- 'This specific encoder configuration is not supported by this browser. Consider using another codec'
- + ' or changing your video parameters.',
- );
- }
-
- this.encoder = new VideoEncoder({
- output: (chunk, meta) => {
- const packet = EncodedPacket.fromEncodedChunk(chunk);
+ if (MatchingCustomEncoder) {
+ // @ts-expect-error "Can't create instance of abstract class 🤓"
+ this.customEncoder = new MatchingCustomEncoder() as CustomVideoEncoder;
+ this.customEncoder.codec = this.encodingConfig.codec;
+ this.customEncoder.config = encoderConfig;
+ this.customEncoder.onPacket = (packet, meta) => {
+ if (!(packet instanceof EncodedPacket)) {
+ throw new TypeError('The first argument passed to onPacket must be an EncodedPacket.');
+ }
+ if (meta !== undefined && (!meta || typeof meta !== 'object')) {
+ throw new TypeError('The second argument passed to onPacket must be an object or undefined.');
+ }
this.encodingConfig.onEncodedPacket?.(packet, meta);
void this.muxer!.addEncodedVideoPacket(this.source._connectedTrack!, packet, meta);
- },
- error: this.encodingConfig.onEncoderError ?? (error => console.error('VideoEncoder error:', error)),
- });
- this.encoder.configure(encoderConfig);
- }
+ };
- assert(this.source._connectedTrack);
- this.muxer = this.source._connectedTrack.output._muxer;
+ await this.customEncoder.init();
+ } else {
+ if (typeof VideoEncoder === 'undefined') {
+ throw new Error('VideoEncoder is not supported by this browser.');
+ }
- this.encoderInitialized = true;
+ const support = await VideoEncoder.isConfigSupported(encoderConfig);
+ if (!support.supported) {
+ throw new Error(
+ 'This specific encoder configuration is not supported by this browser. Consider using another'
+ + ' codec or changing your video parameters.',
+ );
+ }
- resolve();
+ this.encoder = new VideoEncoder({
+ output: (chunk, meta) => {
+ const packet = EncodedPacket.fromEncodedChunk(chunk);
+
+ this.encodingConfig.onEncodedPacket?.(packet, meta);
+ void this.muxer!.addEncodedVideoPacket(this.source._connectedTrack!, packet, meta);
+ },
+ error: this.encodingConfig.onEncoderError ?? (error => console.error('VideoEncoder error:', error)),
+ });
+ this.encoder.configure(encoderConfig);
+ }
+
+ assert(this.source._connectedTrack);
+ this.muxer = this.source._connectedTrack.output._muxer;
+
+ this.encoderInitialized = true;
+ })();
}
async flushAndClose() {
@@ -891,87 +888,85 @@ class AudioEncoderWrapper {
}
}
- private async ensureEncoder(audioSample: AudioSample) {
+ private ensureEncoder(audioSample: AudioSample) {
if (this.encoderInitialized) {
return;
}
- const { promise, resolve } = promiseWithResolvers();
- this.ensureEncoderPromise = promise;
+ return this.ensureEncoderPromise = (async () => {
+ const { numberOfChannels, sampleRate } = audioSample;
+ const bitrate = this.encodingConfig.bitrate instanceof Quality
+ ? this.encodingConfig.bitrate._toAudioBitrate(this.encodingConfig.codec)
+ : this.encodingConfig.bitrate;
- const { numberOfChannels, sampleRate } = audioSample;
- const bitrate = this.encodingConfig.bitrate instanceof Quality
- ? this.encodingConfig.bitrate._toAudioBitrate(this.encodingConfig.codec)
- : this.encodingConfig.bitrate;
-
- const encoderConfig: AudioEncoderConfig = {
- codec: this.encodingConfig.fullCodecString ?? buildAudioCodecString(
- this.encodingConfig.codec,
+ const encoderConfig: AudioEncoderConfig = {
+ codec: this.encodingConfig.fullCodecString ?? buildAudioCodecString(
+ this.encodingConfig.codec,
+ numberOfChannels,
+ sampleRate,
+ ),
numberOfChannels,
sampleRate,
- ),
- numberOfChannels,
- sampleRate,
- bitrate,
- ...getAudioEncoderConfigExtension(this.encodingConfig.codec),
- };
- this.encodingConfig.onEncoderConfig?.(encoderConfig);
-
- const MatchingCustomEncoder = customAudioEncoders.find(x => x.supports(
- this.encodingConfig.codec,
- encoderConfig,
- ));
-
- if (MatchingCustomEncoder) {
- // @ts-expect-error "Can't create instance of abstract class 🤓"
- this.customEncoder = new MatchingCustomEncoder() as CustomAudioEncoder;
- this.customEncoder.codec = this.encodingConfig.codec;
- this.customEncoder.config = encoderConfig;
- this.customEncoder.onPacket = (packet, meta) => {
- if (!(packet instanceof EncodedPacket)) {
- throw new TypeError('The first argument passed to onPacket must be an EncodedPacket.');
- }
- if (meta !== undefined && (!meta || typeof meta !== 'object')) {
- throw new TypeError('The second argument passed to onPacket must be an object or undefined.');
- }
-
- this.encodingConfig.onEncodedPacket?.(packet, meta);
- void this.muxer!.addEncodedAudioPacket(this.source._connectedTrack!, packet, meta);
+ bitrate,
+ ...getAudioEncoderConfigExtension(this.encodingConfig.codec),
};
+ this.encodingConfig.onEncoderConfig?.(encoderConfig);
- await this.customEncoder.init();
- } else if ((PCM_AUDIO_CODECS as readonly string[]).includes(this.encodingConfig.codec)) {
- this.initPcmEncoder();
- } else {
- if (typeof AudioEncoder === 'undefined') {
- throw new Error('AudioEncoder is not supported by this browser.');
- }
+ const MatchingCustomEncoder = customAudioEncoders.find(x => x.supports(
+ this.encodingConfig.codec,
+ encoderConfig,
+ ));
- const support = await AudioEncoder.isConfigSupported(encoderConfig);
- if (!support.supported) {
- throw new Error(
- 'This specific encoder configuration not supported by this browser. Consider using another codec or'
- + ' changing your audio parameters.',
- );
- }
-
- this.encoder = new AudioEncoder({
- output: (chunk, meta) => {
- const packet = EncodedPacket.fromEncodedChunk(chunk);
+ if (MatchingCustomEncoder) {
+ // @ts-expect-error "Can't create instance of abstract class 🤓"
+ this.customEncoder = new MatchingCustomEncoder() as CustomAudioEncoder;
+ this.customEncoder.codec = this.encodingConfig.codec;
+ this.customEncoder.config = encoderConfig;
+ this.customEncoder.onPacket = (packet, meta) => {
+ if (!(packet instanceof EncodedPacket)) {
+ throw new TypeError('The first argument passed to onPacket must be an EncodedPacket.');
+ }
+ if (meta !== undefined && (!meta || typeof meta !== 'object')) {
+ throw new TypeError('The second argument passed to onPacket must be an object or undefined.');
+ }
this.encodingConfig.onEncodedPacket?.(packet, meta);
void this.muxer!.addEncodedAudioPacket(this.source._connectedTrack!, packet, meta);
- },
- error: this.encodingConfig.onEncoderError ?? (error => console.error('AudioEncoder error:', error)),
- });
- this.encoder.configure(encoderConfig);
- }
+ };
- assert(this.source._connectedTrack);
- this.muxer = this.source._connectedTrack.output._muxer;
+ await this.customEncoder.init();
+ } else if ((PCM_AUDIO_CODECS as readonly string[]).includes(this.encodingConfig.codec)) {
+ this.initPcmEncoder();
+ } else {
+ if (typeof AudioEncoder === 'undefined') {
+ throw new Error('AudioEncoder is not supported by this browser.');
+ }
- this.encoderInitialized = true;
- resolve();
+ const support = await AudioEncoder.isConfigSupported(encoderConfig);
+ if (!support.supported) {
+ throw new Error(
+ 'This specific encoder configuration not supported by this browser. Consider using another'
+ + ' codec or changing your audio parameters.',
+ );
+ }
+
+ this.encoder = new AudioEncoder({
+ output: (chunk, meta) => {
+ const packet = EncodedPacket.fromEncodedChunk(chunk);
+
+ this.encodingConfig.onEncodedPacket?.(packet, meta);
+ void this.muxer!.addEncodedAudioPacket(this.source._connectedTrack!, packet, meta);
+ },
+ error: this.encodingConfig.onEncoderError ?? (error => console.error('AudioEncoder error:', error)),
+ });
+ this.encoder.configure(encoderConfig);
+ }
+
+ assert(this.source._connectedTrack);
+ this.muxer = this.source._connectedTrack.output._muxer;
+
+ this.encoderInitialized = true;
+ })();
}
private initPcmEncoder() {