mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 19:03:46 +02:00
Add file compression example
This commit is contained in:
+21
-13
@@ -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<VideoCodec[]> => {
|
||||
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<AudioCodec[]> => {
|
||||
const bools = await Promise.all(checkedCodecs.map(codec => canEncodeAudio(codec, options)));
|
||||
|
||||
+64
-56
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -84,8 +84,8 @@ export class Input<S extends Source = Source> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
+128
-133
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user