mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
Add more options to Video/AudioEncodingConfig, restructure code (#61)
This commit is contained in:
@@ -47,9 +47,13 @@ All video sources that handle encoding internally require you to specify a `Vide
|
|||||||
type VideoEncodingConfig = {
|
type VideoEncodingConfig = {
|
||||||
codec: VideoCodec;
|
codec: VideoCodec;
|
||||||
bitrate: number | Quality;
|
bitrate: number | Quality;
|
||||||
|
bitrateMode?: 'constant' | 'variable';
|
||||||
latencyMode?: 'quality' | 'realtime';
|
latencyMode?: 'quality' | 'realtime';
|
||||||
keyFrameInterval?: number;
|
keyFrameInterval?: number;
|
||||||
fullCodecString?: string;
|
fullCodecString?: string;
|
||||||
|
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
|
||||||
|
scalabilityMode?: string;
|
||||||
|
contentHint?: string;
|
||||||
|
|
||||||
onEncodedPacket?: (
|
onEncodedPacket?: (
|
||||||
packet: EncodedPacket,
|
packet: EncodedPacket,
|
||||||
@@ -62,9 +66,13 @@ type VideoEncodingConfig = {
|
|||||||
```
|
```
|
||||||
- `codec`: The [video codec](./supported-formats-and-codecs#video-codecs) used for encoding.
|
- `codec`: The [video codec](./supported-formats-and-codecs#video-codecs) used for encoding.
|
||||||
- `bitrate`: The target number of bits per second. Alternatively, this can be a [subjective quality](#subjective-qualities).
|
- `bitrate`: The target number of bits per second. Alternatively, this can be a [subjective quality](#subjective-qualities).
|
||||||
|
- `bitrateMode`: Can be used to control constant vs. variable bitrate.
|
||||||
- `latencyMode`: The latency mode as specified by the WebCodecs API. Browsers default to `quality`. Media stream-driven video sources will automatically use the `realtime` setting.
|
- `latencyMode`: The latency mode as specified by the WebCodecs API. Browsers default to `quality`. Media stream-driven video sources will automatically use the `realtime` setting.
|
||||||
- `keyFrameInterval`: The maximum interval in seconds between two adjacent key frames. Defaults to 5 seconds. More frequent key frames improve seeking behavior but increase file size. When using multiple video tracks, this value should be set to the same value for all tracks.
|
- `keyFrameInterval`: The maximum interval in seconds between two adjacent key frames. Defaults to 5 seconds. More frequent key frames improve seeking behavior but increase file size. When using multiple video tracks, this value should be set to the same value for all tracks.
|
||||||
- `fullCodecString`: Allows you to optionally specify the full codec string used by the video encoder, as specified in the [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/). For example, you may set it to `'avc1.42001f'` when using AVC. Keep in mind that the codec string must still match the codec specified in `codec`. If you don't set this field, a codec string will be generated automatically.
|
- `fullCodecString`: Allows you to optionally specify the full codec string used by the video encoder, as specified in the [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/). For example, you may set it to `'avc1.42001f'` when using AVC. Keep in mind that the codec string must still match the codec specified in `codec`. If you don't set this field, a codec string will be generated automatically.
|
||||||
|
- `hardwareAcceleration`: A hint that configures the hardware acceleration method of this codec. This is best left on `'no-preference'`.
|
||||||
|
- `scalabilityMode`: An encoding scalability mode identifier as defined by [WebRTC-SVC](https://w3c.github.io/webrtc-svc/#scalabilitymodes*).
|
||||||
|
- `contentHint`: An encoding video content hint as defined by [mst-content-hint](https://w3c.github.io/mst-content-hint/#video-content-hints).
|
||||||
- `onEncodedPacket`: Called for each successfully encoded packet. Useful for determining encoding progress.
|
- `onEncodedPacket`: Called for each successfully encoded packet. Useful for determining encoding progress.
|
||||||
- `onEncoderConfig`: Called when the internal encoder config, as used by the WebCodecs API, is created. You can use this to introspect the full codec string.
|
- `onEncoderConfig`: Called when the internal encoder config, as used by the WebCodecs API, is created. You can use this to introspect the full codec string.
|
||||||
|
|
||||||
@@ -75,6 +83,7 @@ All audio sources that handle encoding internally require you to specify an `Aud
|
|||||||
type AudioEncodingConfig = {
|
type AudioEncodingConfig = {
|
||||||
codec: AudioCodec;
|
codec: AudioCodec;
|
||||||
bitrate?: number | Quality;
|
bitrate?: number | Quality;
|
||||||
|
bitrateMode?: 'constant' | 'variable';
|
||||||
fullCodecString?: string;
|
fullCodecString?: string;
|
||||||
|
|
||||||
onEncodedPacket?: (
|
onEncodedPacket?: (
|
||||||
@@ -88,6 +97,7 @@ type AudioEncodingConfig = {
|
|||||||
```
|
```
|
||||||
- `codec`: The [audio codec](./supported-formats-and-codecs#audio-codecs) used for encoding. Can be omitted for uncompressed PCM codecs.
|
- `codec`: The [audio codec](./supported-formats-and-codecs#audio-codecs) used for encoding. Can be omitted for uncompressed PCM codecs.
|
||||||
- `bitrate`: The target number of bits per second. Alternatively, this can be a [subjective quality](#subjective-qualities).
|
- `bitrate`: The target number of bits per second. Alternatively, this can be a [subjective quality](#subjective-qualities).
|
||||||
|
- `bitrateMode`: Can be used to control constant vs. variable bitrate.
|
||||||
- `fullCodecString`: Allows you to optionally specify the full codec string used by the audio encoder, as specified in the [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/). For example, you may set it to `'mp4a.40.2'` when using AAC. Keep in mind that the codec string must still match the codec specified in `codec`. If you don't set this field, a codec string will be generated automatically.
|
- `fullCodecString`: Allows you to optionally specify the full codec string used by the audio encoder, as specified in the [WebCodecs Codec Registry](https://www.w3.org/TR/webcodecs-codec-registry/). For example, you may set it to `'mp4a.40.2'` when using AAC. Keep in mind that the codec string must still match the codec specified in `codec`. If you don't set this field, a codec string will be generated automatically.
|
||||||
- `onEncodedPacket`: Called for each successfully encoded packet. Useful for determining encoding progress.
|
- `onEncodedPacket`: Called for each successfully encoded packet. Useful for determining encoding progress.
|
||||||
- `onEncoderConfig`: Called when the internal encoder config, as used by the WebCodecs API, is created. You can use this to introspect the full codec string.
|
- `onEncoderConfig`: Called when the internal encoder config, as used by the WebCodecs API, is created. You can use this to introspect the full codec string.
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ canEncodeAudio('aac', {
|
|||||||
}); // => Promise<boolean>
|
}); // => Promise<boolean>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Additionally, most properties of [`VideoEncodingConfig`](./media-sources#video-encoding-config) and [`AudioEncodingConfig`](./media-sources#audio-encoding-config) can be used here as well.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
In addition, you can use the following functions to check encodability for multiple codecs at once, getting back a list of supported codecs:
|
In addition, you can use the following functions to check encodability for multiple codecs at once, getting back a list of supported codecs:
|
||||||
|
|||||||
-269
@@ -12,7 +12,6 @@ import {
|
|||||||
HevcDecoderConfigurationRecord,
|
HevcDecoderConfigurationRecord,
|
||||||
Vp9CodecInfo,
|
Vp9CodecInfo,
|
||||||
} from './codec-data';
|
} from './codec-data';
|
||||||
import { customAudioEncoders, customVideoEncoders } from './custom-coder';
|
|
||||||
import {
|
import {
|
||||||
Bitstream,
|
Bitstream,
|
||||||
COLOR_PRIMARIES_MAP,
|
COLOR_PRIMARIES_MAP,
|
||||||
@@ -1124,271 +1123,3 @@ export const validateSubtitleMetadata = (metadata: SubtitleMetadata | undefined)
|
|||||||
throw new TypeError('Subtitle metadata config description must be a string.');
|
throw new TypeError('Subtitle metadata config description must be a string.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the browser is able to encode the given codec.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const canEncode = (codec: MediaCodec) => {
|
|
||||||
if ((VIDEO_CODECS as readonly string[]).includes(codec)) {
|
|
||||||
return canEncodeVideo(codec as VideoCodec);
|
|
||||||
} else if ((AUDIO_CODECS as readonly string[]).includes(codec)) {
|
|
||||||
return canEncodeAudio(codec as AudioCodec);
|
|
||||||
} else if ((SUBTITLE_CODECS as readonly string[]).includes(codec)) {
|
|
||||||
return canEncodeSubtitles(codec as SubtitleCodec);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new TypeError(`Unknown codec '${codec}'.`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the browser is able to encode the given video codec with the given parameters.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const canEncodeVideo = async (codec: VideoCodec, { width = 1280, height = 720, bitrate = 1e6 }: {
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
bitrate?: number | Quality;
|
|
||||||
} = {}) => {
|
|
||||||
if (!VIDEO_CODECS.includes(codec)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(width) || width <= 0) {
|
|
||||||
throw new TypeError('width must be a positive integer.');
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(height) || height <= 0) {
|
|
||||||
throw new TypeError('height 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,
|
|
||||||
resolvedBitrate,
|
|
||||||
),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
bitrate: resolvedBitrate,
|
|
||||||
...getVideoEncoderConfigExtension(codec),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (customVideoEncoders.some(x => x.supports(codec, encoderConfig))) {
|
|
||||||
// There's a custom encoder
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof VideoEncoder === 'undefined') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const support = await VideoEncoder.isConfigSupported({
|
|
||||||
codec: buildVideoCodecString(codec, width, height, resolvedBitrate),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
bitrate: resolvedBitrate,
|
|
||||||
...getVideoEncoderConfigExtension(codec),
|
|
||||||
});
|
|
||||||
|
|
||||||
return support.supported === true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the browser is able to encode the given audio codec with the given parameters.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const canEncodeAudio = async (codec: AudioCodec, { numberOfChannels = 2, sampleRate = 48000, bitrate = 128e3 }: {
|
|
||||||
numberOfChannels?: number;
|
|
||||||
sampleRate?: number;
|
|
||||||
bitrate?: number | Quality;
|
|
||||||
} = {}) => {
|
|
||||||
if (!AUDIO_CODECS.includes(codec)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(numberOfChannels) || numberOfChannels <= 0) {
|
|
||||||
throw new TypeError('numberOfChannels must be a positive integer.');
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(sampleRate) || sampleRate <= 0) {
|
|
||||||
throw new TypeError('sampleRate must be a positive integer.');
|
|
||||||
}
|
|
||||||
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(
|
|
||||||
codec,
|
|
||||||
numberOfChannels,
|
|
||||||
sampleRate,
|
|
||||||
),
|
|
||||||
numberOfChannels,
|
|
||||||
sampleRate,
|
|
||||||
bitrate: resolvedBitrate,
|
|
||||||
...getAudioEncoderConfigExtension(codec),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (customAudioEncoders.some(x => x.supports(codec, encoderConfig))) {
|
|
||||||
// There's a custom encoder
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
|
|
||||||
return true; // Because we encode these ourselves
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof AudioEncoder === 'undefined') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const support = await AudioEncoder.isConfigSupported({
|
|
||||||
codec: buildAudioCodecString(codec, numberOfChannels, sampleRate),
|
|
||||||
numberOfChannels,
|
|
||||||
sampleRate,
|
|
||||||
bitrate: resolvedBitrate,
|
|
||||||
...getAudioEncoderConfigExtension(codec),
|
|
||||||
});
|
|
||||||
|
|
||||||
return support.supported === true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the browser is able to encode the given subtitle codec.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const canEncodeSubtitles = async (codec: SubtitleCodec) => {
|
|
||||||
if (!SUBTITLE_CODECS.includes(codec)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the list of all media codecs that can be encoded by the browser.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const getEncodableCodecs = async (): Promise<MediaCodec[]> => {
|
|
||||||
const [videoCodecs, audioCodecs, subtitleCodecs] = await Promise.all([
|
|
||||||
getEncodableVideoCodecs(),
|
|
||||||
getEncodableAudioCodecs(),
|
|
||||||
getEncodableSubtitleCodecs(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [...videoCodecs, ...audioCodecs, ...subtitleCodecs];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the list of all video codecs that can be encoded by the browser.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const getEncodableVideoCodecs = async (
|
|
||||||
checkedCodecs = VIDEO_CODECS as unknown as VideoCodec[],
|
|
||||||
options?: {
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
bitrate?: number | Quality;
|
|
||||||
},
|
|
||||||
): Promise<VideoCodec[]> => {
|
|
||||||
const bools = await Promise.all(checkedCodecs.map(codec => canEncodeVideo(codec, options)));
|
|
||||||
return checkedCodecs.filter((_, i) => bools[i]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the list of all audio codecs that can be encoded by the browser.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const getEncodableAudioCodecs = async (
|
|
||||||
checkedCodecs = AUDIO_CODECS as unknown as AudioCodec[],
|
|
||||||
options?: {
|
|
||||||
numberOfChannels?: number;
|
|
||||||
sampleRate?: number;
|
|
||||||
bitrate?: number | Quality;
|
|
||||||
},
|
|
||||||
): Promise<AudioCodec[]> => {
|
|
||||||
const bools = await Promise.all(checkedCodecs.map(codec => canEncodeAudio(codec, options)));
|
|
||||||
return checkedCodecs.filter((_, i) => bools[i]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the list of all subtitle codecs that can be encoded by the browser.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const getEncodableSubtitleCodecs = async (
|
|
||||||
checkedCodecs = SUBTITLE_CODECS as unknown as SubtitleCodec[],
|
|
||||||
): Promise<SubtitleCodec[]> => {
|
|
||||||
const bools = await Promise.all(checkedCodecs.map(canEncodeSubtitles));
|
|
||||||
return checkedCodecs.filter((_, i) => bools[i]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the first video codec from the given list that can be encoded by the browser.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const getFirstEncodableVideoCodec = async (
|
|
||||||
checkedCodecs: VideoCodec[],
|
|
||||||
options?: {
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
bitrate?: number | Quality;
|
|
||||||
},
|
|
||||||
): Promise<VideoCodec | null> => {
|
|
||||||
for (const codec of checkedCodecs) {
|
|
||||||
if (await canEncodeVideo(codec, options)) {
|
|
||||||
return codec;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the first audio codec from the given list that can be encoded by the browser.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const getFirstEncodableAudioCodec = async (
|
|
||||||
checkedCodecs: AudioCodec[],
|
|
||||||
options?: {
|
|
||||||
numberOfChannels?: number;
|
|
||||||
sampleRate?: number;
|
|
||||||
bitrate?: number | Quality;
|
|
||||||
},
|
|
||||||
): Promise<AudioCodec | null> => {
|
|
||||||
for (const codec of checkedCodecs) {
|
|
||||||
if (await canEncodeAudio(codec, options)) {
|
|
||||||
return codec;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the first subtitle codec from the given list that can be encoded by the browser.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export const getFirstEncodableSubtitleCodec = async (
|
|
||||||
checkedCodecs: SubtitleCodec[],
|
|
||||||
): Promise<SubtitleCodec | null> => {
|
|
||||||
for (const codec of checkedCodecs) {
|
|
||||||
if (await canEncodeSubtitles(codec)) {
|
|
||||||
return codec;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|||||||
+6
-4
@@ -9,14 +9,18 @@
|
|||||||
import {
|
import {
|
||||||
AUDIO_CODECS,
|
AUDIO_CODECS,
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
getFirstEncodableVideoCodec,
|
|
||||||
getEncodableAudioCodecs,
|
|
||||||
NON_PCM_AUDIO_CODECS,
|
NON_PCM_AUDIO_CODECS,
|
||||||
Quality,
|
Quality,
|
||||||
QUALITY_HIGH,
|
QUALITY_HIGH,
|
||||||
VIDEO_CODECS,
|
VIDEO_CODECS,
|
||||||
VideoCodec,
|
VideoCodec,
|
||||||
} from './codec';
|
} from './codec';
|
||||||
|
import {
|
||||||
|
AudioEncodingConfig,
|
||||||
|
getEncodableAudioCodecs,
|
||||||
|
getFirstEncodableVideoCodec,
|
||||||
|
VideoEncodingConfig,
|
||||||
|
} from './encode';
|
||||||
import { Input } from './input';
|
import { Input } from './input';
|
||||||
import { InputAudioTrack, InputTrack, InputVideoTrack } from './input-track';
|
import { InputAudioTrack, InputTrack, InputVideoTrack } from './input-track';
|
||||||
import {
|
import {
|
||||||
@@ -26,11 +30,9 @@ import {
|
|||||||
VideoSampleSink,
|
VideoSampleSink,
|
||||||
} from './media-sink';
|
} from './media-sink';
|
||||||
import {
|
import {
|
||||||
AudioEncodingConfig,
|
|
||||||
AudioSource,
|
AudioSource,
|
||||||
EncodedVideoPacketSource,
|
EncodedVideoPacketSource,
|
||||||
EncodedAudioPacketSource,
|
EncodedAudioPacketSource,
|
||||||
VideoEncodingConfig,
|
|
||||||
VideoSource,
|
VideoSource,
|
||||||
VideoSampleSource,
|
VideoSampleSource,
|
||||||
AudioSampleSource,
|
AudioSampleSource,
|
||||||
|
|||||||
+546
@@ -0,0 +1,546 @@
|
|||||||
|
/*!
|
||||||
|
* Copyright (c) 2025-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 {
|
||||||
|
AUDIO_CODECS,
|
||||||
|
AudioCodec,
|
||||||
|
buildAudioCodecString,
|
||||||
|
buildVideoCodecString,
|
||||||
|
getAudioEncoderConfigExtension,
|
||||||
|
getVideoEncoderConfigExtension,
|
||||||
|
inferCodecFromCodecString,
|
||||||
|
MediaCodec,
|
||||||
|
PCM_AUDIO_CODECS,
|
||||||
|
Quality,
|
||||||
|
SUBTITLE_CODECS,
|
||||||
|
SubtitleCodec,
|
||||||
|
VIDEO_CODECS,
|
||||||
|
VideoCodec,
|
||||||
|
} from './codec';
|
||||||
|
import { customAudioEncoders, customVideoEncoders } from './custom-coder';
|
||||||
|
import { EncodedPacket } from './packet';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration object that controls video encoding. Can be used to set codec, quality, and more.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type VideoEncodingConfig = {
|
||||||
|
/** The video codec that should be used for encoding the video samples (frames). */
|
||||||
|
codec: VideoCodec;
|
||||||
|
/**
|
||||||
|
* The target bitrate for the encoded video, in bits per second. Alternatively, a subjective Quality can
|
||||||
|
* be provided.
|
||||||
|
*/
|
||||||
|
bitrate: number | Quality;
|
||||||
|
/**
|
||||||
|
* The interval, in seconds, of how often frames are encoded as a key frame. The default is 5 seconds. Frequent key
|
||||||
|
* frames improve seeking behavior but increase file size. When using multiple video tracks, you should give them
|
||||||
|
* all the same key frame interval.
|
||||||
|
*/
|
||||||
|
keyFrameInterval?: number;
|
||||||
|
|
||||||
|
/** Called for each successfully encoded packet. Both the packet and the encoding metadata are passed. */
|
||||||
|
onEncodedPacket?: (packet: EncodedPacket, meta: EncodedVideoChunkMetadata | undefined) => unknown;
|
||||||
|
/** Called when the internal encoder config, as used by the WebCodecs API, is created. */
|
||||||
|
onEncoderConfig?: (config: VideoEncoderConfig) => unknown;
|
||||||
|
} & VideoEncodingAdditionalOptions;
|
||||||
|
|
||||||
|
export const validateVideoEncodingConfig = (config: VideoEncodingConfig) => {
|
||||||
|
if (!config || typeof config !== 'object') {
|
||||||
|
throw new TypeError('Encoding config must be an object.');
|
||||||
|
}
|
||||||
|
if (!VIDEO_CODECS.includes(config.codec)) {
|
||||||
|
throw new TypeError(`Invalid video codec '${config.codec}'. Must be one of: ${VIDEO_CODECS.join(', ')}.`);
|
||||||
|
}
|
||||||
|
if (!(config.bitrate instanceof Quality) && (!Number.isInteger(config.bitrate) || config.bitrate <= 0)) {
|
||||||
|
throw new TypeError('config.bitrate must be a positive integer or a quality.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
config.keyFrameInterval !== undefined
|
||||||
|
&& (!Number.isFinite(config.keyFrameInterval) || config.keyFrameInterval < 0)
|
||||||
|
) {
|
||||||
|
throw new TypeError('config.keyFrameInterval, when provided, must be a non-negative number.');
|
||||||
|
}
|
||||||
|
if (config.onEncodedPacket !== undefined && typeof config.onEncodedPacket !== 'function') {
|
||||||
|
throw new TypeError('config.onEncodedChunk, when provided, must be a function.');
|
||||||
|
}
|
||||||
|
if (config.onEncoderConfig !== undefined && typeof config.onEncoderConfig !== 'function') {
|
||||||
|
throw new TypeError('config.onEncoderConfig, when provided, must be a function.');
|
||||||
|
}
|
||||||
|
|
||||||
|
validateVideoEncodingAdditionalOptions(config.codec, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional options that control audio encoding.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type VideoEncodingAdditionalOptions = {
|
||||||
|
/** Configures the bitrate mode. */
|
||||||
|
bitrateMode?: 'constant' | 'variable';
|
||||||
|
/** The latency mode used by the encoder; controls the performance-quality tradeoff. */
|
||||||
|
latencyMode?: VideoEncoderConfig['latencyMode'];
|
||||||
|
/**
|
||||||
|
* The full codec string as specified in the WebCodecs Codec Registry. This string must match the codec
|
||||||
|
* specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library.
|
||||||
|
*/
|
||||||
|
fullCodecString?: string;
|
||||||
|
/** A hint that configures the hardware acceleration method of this codec. This is best left on 'no-preference'. */
|
||||||
|
hardwareAcceleration?: VideoEncoderConfig['hardwareAcceleration'];
|
||||||
|
/**
|
||||||
|
* An encoding scalability mode identifier as defined by
|
||||||
|
* [WebRTC-SVC](https://w3c.github.io/webrtc-svc/#scalabilitymodes*).
|
||||||
|
*/
|
||||||
|
scalabilityMode?: VideoEncoderConfig['scalabilityMode'];
|
||||||
|
/**
|
||||||
|
* An encoding video content hint as defined by
|
||||||
|
* [mst-content-hint](https://w3c.github.io/mst-content-hint/#video-content-hints).
|
||||||
|
*/
|
||||||
|
contentHint?: VideoEncoderConfig['contentHint'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateVideoEncodingAdditionalOptions = (codec: VideoCodec, options: VideoEncodingAdditionalOptions) => {
|
||||||
|
if (!options || typeof options !== 'object') {
|
||||||
|
throw new TypeError('Encoding options must be an object.');
|
||||||
|
}
|
||||||
|
if (options.bitrateMode !== undefined && !['constant', 'variable'].includes(options.bitrateMode)) {
|
||||||
|
throw new TypeError('bitrateMode, when provided, must be \'constant\' or \'variable\'.');
|
||||||
|
}
|
||||||
|
if (options.latencyMode !== undefined && !['quality', 'realtime'].includes(options.latencyMode)) {
|
||||||
|
throw new TypeError('latencyMode, when provided, must be \'quality\' or \'realtime\'.');
|
||||||
|
}
|
||||||
|
if (options.fullCodecString !== undefined && typeof options.fullCodecString !== 'string') {
|
||||||
|
throw new TypeError('fullCodecString, when provided, must be a string.');
|
||||||
|
}
|
||||||
|
if (options.fullCodecString !== undefined && inferCodecFromCodecString(options.fullCodecString) !== codec) {
|
||||||
|
throw new TypeError(
|
||||||
|
`fullCodecString, when provided, must be a string that matches the specified codec (${codec}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.hardwareAcceleration !== undefined
|
||||||
|
&& !['no-preference', 'prefer-hardware', 'prefer-software'].includes(options.hardwareAcceleration)
|
||||||
|
) {
|
||||||
|
throw new TypeError(
|
||||||
|
'hardwareAcceleration, when provided, must be \'no-preference\', \'prefer-hardware\' or'
|
||||||
|
+ ' \'prefer-software\'.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (options.scalabilityMode !== undefined && typeof options.scalabilityMode !== 'string') {
|
||||||
|
throw new TypeError('scalabilityMode, when provided, must be a string.');
|
||||||
|
}
|
||||||
|
if (options.contentHint !== undefined && typeof options.contentHint !== 'string') {
|
||||||
|
throw new TypeError('contentHint, when provided, must be a string.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildVideoEncoderConfig = (options: {
|
||||||
|
codec: VideoCodec;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
bitrate: number | Quality;
|
||||||
|
framerate: number | undefined;
|
||||||
|
} & VideoEncodingAdditionalOptions): VideoEncoderConfig => {
|
||||||
|
const resolvedBitrate = options.bitrate instanceof Quality
|
||||||
|
? options.bitrate._toVideoBitrate(options.codec, options.width, options.height)
|
||||||
|
: options.bitrate;
|
||||||
|
|
||||||
|
return {
|
||||||
|
codec: options.fullCodecString ?? buildVideoCodecString(
|
||||||
|
options.codec,
|
||||||
|
options.width,
|
||||||
|
options.height,
|
||||||
|
resolvedBitrate,
|
||||||
|
),
|
||||||
|
width: options.width,
|
||||||
|
height: options.height,
|
||||||
|
bitrate: resolvedBitrate,
|
||||||
|
bitrateMode: options.bitrateMode,
|
||||||
|
framerate: options.framerate, // this.source._connectedTrack?.metadata.frameRate,
|
||||||
|
latencyMode: options.latencyMode,
|
||||||
|
hardwareAcceleration: options.hardwareAcceleration,
|
||||||
|
scalabilityMode: options.scalabilityMode,
|
||||||
|
contentHint: options.contentHint,
|
||||||
|
...getVideoEncoderConfigExtension(options.codec),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration object that controls audio encoding. Can be used to set codec, quality, and more.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type AudioEncodingConfig = {
|
||||||
|
/** The audio codec that should be used for encoding the audio samples. */
|
||||||
|
codec: AudioCodec;
|
||||||
|
/**
|
||||||
|
* The target bitrate for the encoded audio, in bits per second. Alternatively, a subjective Quality can
|
||||||
|
* be provided. Required for compressed audio codecs, unused for PCM codecs.
|
||||||
|
*/
|
||||||
|
bitrate?: number | Quality;
|
||||||
|
|
||||||
|
/** Called for each successfully encoded packet. Both the packet and the encoding metadata are passed. */
|
||||||
|
onEncodedPacket?: (packet: EncodedPacket, meta: EncodedAudioChunkMetadata | undefined) => unknown;
|
||||||
|
/** Called when the internal encoder config, as used by the WebCodecs API, is created. */
|
||||||
|
onEncoderConfig?: (config: AudioEncoderConfig) => unknown;
|
||||||
|
} & AudioEncodingAdditionalOptions;
|
||||||
|
|
||||||
|
export const validateAudioEncodingConfig = (config: AudioEncodingConfig) => {
|
||||||
|
if (!config || typeof config !== 'object') {
|
||||||
|
throw new TypeError('Encoding config must be an object.');
|
||||||
|
}
|
||||||
|
if (!AUDIO_CODECS.includes(config.codec)) {
|
||||||
|
throw new TypeError(`Invalid audio codec '${config.codec}'. Must be one of: ${AUDIO_CODECS.join(', ')}.`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
config.bitrate === undefined
|
||||||
|
&& (!(PCM_AUDIO_CODECS as readonly string[]).includes(config.codec) || config.codec === 'flac')
|
||||||
|
) {
|
||||||
|
throw new TypeError('config.bitrate must be provided for compressed audio codecs.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
config.bitrate !== undefined
|
||||||
|
&& !(config.bitrate instanceof Quality)
|
||||||
|
&& (!Number.isInteger(config.bitrate) || config.bitrate <= 0)
|
||||||
|
) {
|
||||||
|
throw new TypeError('config.bitrate, when provided, must be a positive integer or a quality.');
|
||||||
|
}
|
||||||
|
if (config.onEncodedPacket !== undefined && typeof config.onEncodedPacket !== 'function') {
|
||||||
|
throw new TypeError('config.onEncodedChunk, when provided, must be a function.');
|
||||||
|
}
|
||||||
|
if (config.onEncoderConfig !== undefined && typeof config.onEncoderConfig !== 'function') {
|
||||||
|
throw new TypeError('config.onEncoderConfig, when provided, must be a function.');
|
||||||
|
}
|
||||||
|
|
||||||
|
validateAudioEncodingAdditionalOptions(config.codec, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional options that control audio encoding.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type AudioEncodingAdditionalOptions = {
|
||||||
|
/** Configures the bitrate mode. */
|
||||||
|
bitrateMode?: 'constant' | 'variable';
|
||||||
|
/**
|
||||||
|
* The full codec string as specified in the WebCodecs Codec Registry. This string must match the codec
|
||||||
|
* specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library.
|
||||||
|
*/
|
||||||
|
fullCodecString?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateAudioEncodingAdditionalOptions = (codec: AudioCodec, options: AudioEncodingAdditionalOptions) => {
|
||||||
|
if (!options || typeof options !== 'object') {
|
||||||
|
throw new TypeError('Encoding options must be an object.');
|
||||||
|
}
|
||||||
|
if (options.bitrateMode !== undefined && !['constant', 'variable'].includes(options.bitrateMode)) {
|
||||||
|
throw new TypeError('bitrateMode, when provided, must be \'constant\' or \'variable\'.');
|
||||||
|
}
|
||||||
|
if (options.fullCodecString !== undefined && typeof options.fullCodecString !== 'string') {
|
||||||
|
throw new TypeError('fullCodecString, when provided, must be a string.');
|
||||||
|
}
|
||||||
|
if (options.fullCodecString !== undefined && inferCodecFromCodecString(options.fullCodecString) !== codec) {
|
||||||
|
throw new TypeError(
|
||||||
|
`fullCodecString, when provided, must be a string that matches the specified codec (${codec}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildAudioEncoderConfig = (options: {
|
||||||
|
codec: AudioCodec;
|
||||||
|
numberOfChannels: number;
|
||||||
|
sampleRate: number;
|
||||||
|
bitrate?: number | Quality;
|
||||||
|
} & AudioEncodingAdditionalOptions): AudioEncoderConfig => {
|
||||||
|
const resolvedBitrate = options.bitrate instanceof Quality
|
||||||
|
? options.bitrate._toAudioBitrate(options.codec)
|
||||||
|
: options.bitrate;
|
||||||
|
|
||||||
|
return {
|
||||||
|
codec: options.fullCodecString ?? buildAudioCodecString(
|
||||||
|
options.codec,
|
||||||
|
options.numberOfChannels,
|
||||||
|
options.sampleRate,
|
||||||
|
),
|
||||||
|
numberOfChannels: options.numberOfChannels,
|
||||||
|
sampleRate: options.sampleRate,
|
||||||
|
bitrate: resolvedBitrate,
|
||||||
|
bitrateMode: options.bitrateMode,
|
||||||
|
...getAudioEncoderConfigExtension(options.codec),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the browser is able to encode the given codec.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const canEncode = (codec: MediaCodec) => {
|
||||||
|
if ((VIDEO_CODECS as readonly string[]).includes(codec)) {
|
||||||
|
return canEncodeVideo(codec as VideoCodec);
|
||||||
|
} else if ((AUDIO_CODECS as readonly string[]).includes(codec)) {
|
||||||
|
return canEncodeAudio(codec as AudioCodec);
|
||||||
|
} else if ((SUBTITLE_CODECS as readonly string[]).includes(codec)) {
|
||||||
|
return canEncodeSubtitles(codec as SubtitleCodec);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError(`Unknown codec '${codec}'.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the browser is able to encode the given video codec with the given parameters.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const canEncodeVideo = async (codec: VideoCodec, {
|
||||||
|
width = 1280,
|
||||||
|
height = 720,
|
||||||
|
bitrate = 1e6,
|
||||||
|
...restOptions
|
||||||
|
}: {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
bitrate?: number | Quality;
|
||||||
|
} & VideoEncodingAdditionalOptions = {}) => {
|
||||||
|
if (!VIDEO_CODECS.includes(codec)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(width) || width <= 0) {
|
||||||
|
throw new TypeError('width must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(height) || height <= 0) {
|
||||||
|
throw new TypeError('height 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.');
|
||||||
|
}
|
||||||
|
validateVideoEncodingAdditionalOptions(codec, restOptions);
|
||||||
|
|
||||||
|
let encoderConfig: VideoEncoderConfig | null = null;
|
||||||
|
|
||||||
|
if (customVideoEncoders.length > 0) {
|
||||||
|
encoderConfig ??= buildVideoEncoderConfig({
|
||||||
|
codec,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
bitrate,
|
||||||
|
framerate: undefined,
|
||||||
|
...restOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (customVideoEncoders.some(x => x.supports(codec, encoderConfig!))) {
|
||||||
|
// There's a custom encoder
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof VideoEncoder === 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
encoderConfig ??= buildVideoEncoderConfig({
|
||||||
|
codec,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
bitrate,
|
||||||
|
framerate: undefined,
|
||||||
|
...restOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const support = await VideoEncoder.isConfigSupported(encoderConfig);
|
||||||
|
return support.supported === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the browser is able to encode the given audio codec with the given parameters.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const canEncodeAudio = async (codec: AudioCodec, {
|
||||||
|
numberOfChannels = 2,
|
||||||
|
sampleRate = 48000,
|
||||||
|
bitrate = 128e3,
|
||||||
|
...restOptions
|
||||||
|
}: {
|
||||||
|
numberOfChannels?: number;
|
||||||
|
sampleRate?: number;
|
||||||
|
bitrate?: number | Quality;
|
||||||
|
} & AudioEncodingAdditionalOptions = {}) => {
|
||||||
|
if (!AUDIO_CODECS.includes(codec)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(numberOfChannels) || numberOfChannels <= 0) {
|
||||||
|
throw new TypeError('numberOfChannels must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(sampleRate) || sampleRate <= 0) {
|
||||||
|
throw new TypeError('sampleRate must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
|
||||||
|
throw new TypeError('bitrate must be a positive integer.');
|
||||||
|
}
|
||||||
|
validateAudioEncodingAdditionalOptions(codec, restOptions);
|
||||||
|
|
||||||
|
let encoderConfig: AudioEncoderConfig | null = null;
|
||||||
|
|
||||||
|
if (customAudioEncoders.length > 0) {
|
||||||
|
encoderConfig ??= buildAudioEncoderConfig({
|
||||||
|
codec,
|
||||||
|
numberOfChannels,
|
||||||
|
sampleRate,
|
||||||
|
bitrate,
|
||||||
|
...restOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (customAudioEncoders.some(x => x.supports(codec, encoderConfig!))) {
|
||||||
|
// There's a custom encoder
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec)) {
|
||||||
|
return true; // Because we encode these ourselves
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof AudioEncoder === 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
encoderConfig ??= buildAudioEncoderConfig({
|
||||||
|
codec,
|
||||||
|
numberOfChannels,
|
||||||
|
sampleRate,
|
||||||
|
bitrate,
|
||||||
|
...restOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const support = await AudioEncoder.isConfigSupported(encoderConfig);
|
||||||
|
return support.supported === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the browser is able to encode the given subtitle codec.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const canEncodeSubtitles = async (codec: SubtitleCodec) => {
|
||||||
|
if (!SUBTITLE_CODECS.includes(codec)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of all media codecs that can be encoded by the browser.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const getEncodableCodecs = async (): Promise<MediaCodec[]> => {
|
||||||
|
const [videoCodecs, audioCodecs, subtitleCodecs] = await Promise.all([
|
||||||
|
getEncodableVideoCodecs(),
|
||||||
|
getEncodableAudioCodecs(),
|
||||||
|
getEncodableSubtitleCodecs(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [...videoCodecs, ...audioCodecs, ...subtitleCodecs];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of all video codecs that can be encoded by the browser.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const getEncodableVideoCodecs = async (
|
||||||
|
checkedCodecs = VIDEO_CODECS as unknown as VideoCodec[],
|
||||||
|
options?: {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
bitrate?: number | Quality;
|
||||||
|
},
|
||||||
|
): Promise<VideoCodec[]> => {
|
||||||
|
const bools = await Promise.all(checkedCodecs.map(codec => canEncodeVideo(codec, options)));
|
||||||
|
return checkedCodecs.filter((_, i) => bools[i]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of all audio codecs that can be encoded by the browser.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const getEncodableAudioCodecs = async (
|
||||||
|
checkedCodecs = AUDIO_CODECS as unknown as AudioCodec[],
|
||||||
|
options?: {
|
||||||
|
numberOfChannels?: number;
|
||||||
|
sampleRate?: number;
|
||||||
|
bitrate?: number | Quality;
|
||||||
|
},
|
||||||
|
): Promise<AudioCodec[]> => {
|
||||||
|
const bools = await Promise.all(checkedCodecs.map(codec => canEncodeAudio(codec, options)));
|
||||||
|
return checkedCodecs.filter((_, i) => bools[i]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of all subtitle codecs that can be encoded by the browser.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const getEncodableSubtitleCodecs = async (
|
||||||
|
checkedCodecs = SUBTITLE_CODECS as unknown as SubtitleCodec[],
|
||||||
|
): Promise<SubtitleCodec[]> => {
|
||||||
|
const bools = await Promise.all(checkedCodecs.map(canEncodeSubtitles));
|
||||||
|
return checkedCodecs.filter((_, i) => bools[i]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first video codec from the given list that can be encoded by the browser.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const getFirstEncodableVideoCodec = async (
|
||||||
|
checkedCodecs: VideoCodec[],
|
||||||
|
options?: {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
bitrate?: number | Quality;
|
||||||
|
},
|
||||||
|
): Promise<VideoCodec | null> => {
|
||||||
|
for (const codec of checkedCodecs) {
|
||||||
|
if (await canEncodeVideo(codec, options)) {
|
||||||
|
return codec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first audio codec from the given list that can be encoded by the browser.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const getFirstEncodableAudioCodec = async (
|
||||||
|
checkedCodecs: AudioCodec[],
|
||||||
|
options?: {
|
||||||
|
numberOfChannels?: number;
|
||||||
|
sampleRate?: number;
|
||||||
|
bitrate?: number | Quality;
|
||||||
|
},
|
||||||
|
): Promise<AudioCodec | null> => {
|
||||||
|
for (const codec of checkedCodecs) {
|
||||||
|
if (await canEncodeAudio(codec, options)) {
|
||||||
|
return codec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first subtitle codec from the given list that can be encoded by the browser.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const getFirstEncodableSubtitleCodec = async (
|
||||||
|
checkedCodecs: SubtitleCodec[],
|
||||||
|
): Promise<SubtitleCodec | null> => {
|
||||||
|
for (const codec of checkedCodecs) {
|
||||||
|
if (await canEncodeSubtitles(codec)) {
|
||||||
|
return codec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
+7
-3
@@ -41,8 +41,6 @@ export {
|
|||||||
InclusiveIntegerRange,
|
InclusiveIntegerRange,
|
||||||
} from './output-format';
|
} from './output-format';
|
||||||
export {
|
export {
|
||||||
VideoEncodingConfig,
|
|
||||||
AudioEncodingConfig,
|
|
||||||
MediaSource,
|
MediaSource,
|
||||||
VideoSource,
|
VideoSource,
|
||||||
EncodedVideoPacketSource,
|
EncodedVideoPacketSource,
|
||||||
@@ -73,6 +71,12 @@ export {
|
|||||||
QUALITY_MEDIUM,
|
QUALITY_MEDIUM,
|
||||||
QUALITY_HIGH,
|
QUALITY_HIGH,
|
||||||
QUALITY_VERY_HIGH,
|
QUALITY_VERY_HIGH,
|
||||||
|
} from './codec';
|
||||||
|
export {
|
||||||
|
VideoEncodingConfig,
|
||||||
|
VideoEncodingAdditionalOptions,
|
||||||
|
AudioEncodingConfig,
|
||||||
|
AudioEncodingAdditionalOptions,
|
||||||
canEncode,
|
canEncode,
|
||||||
canEncodeVideo,
|
canEncodeVideo,
|
||||||
canEncodeAudio,
|
canEncodeAudio,
|
||||||
@@ -84,7 +88,7 @@ export {
|
|||||||
getFirstEncodableVideoCodec,
|
getFirstEncodableVideoCodec,
|
||||||
getFirstEncodableAudioCodec,
|
getFirstEncodableAudioCodec,
|
||||||
getFirstEncodableSubtitleCodec,
|
getFirstEncodableSubtitleCodec,
|
||||||
} from './codec';
|
} from './encode';
|
||||||
export { Target, BufferTarget, StreamTarget, StreamTargetChunk, StreamTargetOptions } from './target';
|
export { Target, BufferTarget, StreamTarget, StreamTargetChunk, StreamTargetOptions } from './target';
|
||||||
export { Rotation, AnyIterable, SetRequired, MaybePromise } from './misc';
|
export { Rotation, AnyIterable, SetRequired, MaybePromise } from './misc';
|
||||||
export {
|
export {
|
||||||
|
|||||||
+16
-166
@@ -9,15 +9,9 @@
|
|||||||
import {
|
import {
|
||||||
AUDIO_CODECS,
|
AUDIO_CODECS,
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
buildAudioCodecString,
|
|
||||||
buildVideoCodecString,
|
|
||||||
getAudioEncoderConfigExtension,
|
|
||||||
getVideoEncoderConfigExtension,
|
|
||||||
inferCodecFromCodecString,
|
|
||||||
parsePcmCodec,
|
parsePcmCodec,
|
||||||
PCM_AUDIO_CODECS,
|
PCM_AUDIO_CODECS,
|
||||||
PcmAudioCodec,
|
PcmAudioCodec,
|
||||||
Quality,
|
|
||||||
SUBTITLE_CODECS,
|
SUBTITLE_CODECS,
|
||||||
SubtitleCodec,
|
SubtitleCodec,
|
||||||
VIDEO_CODECS,
|
VIDEO_CODECS,
|
||||||
@@ -36,6 +30,14 @@ import {
|
|||||||
} from './custom-coder';
|
} from './custom-coder';
|
||||||
import { EncodedPacket } from './packet';
|
import { EncodedPacket } from './packet';
|
||||||
import { AudioSample, VideoSample } from './sample';
|
import { AudioSample, VideoSample } from './sample';
|
||||||
|
import {
|
||||||
|
AudioEncodingConfig,
|
||||||
|
buildAudioEncoderConfig,
|
||||||
|
buildVideoEncoderConfig,
|
||||||
|
validateAudioEncodingConfig,
|
||||||
|
validateVideoEncodingConfig,
|
||||||
|
VideoEncodingConfig,
|
||||||
|
} from './encode';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for media sources. Media sources are used to add media samples to an output file.
|
* Base class for media sources. Media sources are used to add media samples to an output file.
|
||||||
@@ -183,74 +185,6 @@ export class EncodedVideoPacketSource extends VideoSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration object that controls video encoding. Can be used to set codec, quality, and more.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export type VideoEncodingConfig = {
|
|
||||||
/** The video codec that should be used for encoding the video samples (frames). */
|
|
||||||
codec: VideoCodec;
|
|
||||||
/**
|
|
||||||
* The target bitrate for the encoded video, in bits per second. Alternatively, a subjective Quality can
|
|
||||||
* be provided.
|
|
||||||
*/
|
|
||||||
bitrate: number | Quality;
|
|
||||||
/** The latency mode used by the encoder; controls the performance-quality tradeoff. */
|
|
||||||
latencyMode?: VideoEncoderConfig['latencyMode'];
|
|
||||||
/**
|
|
||||||
* The interval, in seconds, of how often frames are encoded as a key frame. The default is 5 seconds. Frequent key
|
|
||||||
* frames improve seeking behavior but increase file size. When using multiple video tracks, you should give them
|
|
||||||
* all the same key frame interval.
|
|
||||||
*/
|
|
||||||
keyFrameInterval?: number;
|
|
||||||
/**
|
|
||||||
* The full codec string as specified in the WebCodecs Codec Registry. This string must match the codec
|
|
||||||
* specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library.
|
|
||||||
*/
|
|
||||||
fullCodecString?: string;
|
|
||||||
|
|
||||||
/** Called for each successfully encoded packet. Both the packet and the encoding metadata are passed. */
|
|
||||||
onEncodedPacket?: (packet: EncodedPacket, meta: EncodedVideoChunkMetadata | undefined) => unknown;
|
|
||||||
/** Called when the internal encoder config, as used by the WebCodecs API, is created. */
|
|
||||||
onEncoderConfig?: (config: VideoEncoderConfig) => unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateVideoEncodingConfig = (config: VideoEncodingConfig) => {
|
|
||||||
if (!config || typeof config !== 'object') {
|
|
||||||
throw new TypeError('Encoding config must be an object.');
|
|
||||||
}
|
|
||||||
if (!VIDEO_CODECS.includes(config.codec)) {
|
|
||||||
throw new TypeError(`Invalid video codec '${config.codec}'. Must be one of: ${VIDEO_CODECS.join(', ')}.`);
|
|
||||||
}
|
|
||||||
if (!(config.bitrate instanceof Quality) && (!Number.isInteger(config.bitrate) || config.bitrate <= 0)) {
|
|
||||||
throw new TypeError('config.bitrate must be a positive integer or a quality.');
|
|
||||||
}
|
|
||||||
if (config.latencyMode !== undefined && !['quality', 'realtime'].includes(config.latencyMode)) {
|
|
||||||
throw new TypeError('config.latencyMode, when provided, must be \'quality\' or \'realtime\'.');
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
config.keyFrameInterval !== undefined
|
|
||||||
&& (!Number.isFinite(config.keyFrameInterval) || config.keyFrameInterval < 0)
|
|
||||||
) {
|
|
||||||
throw new TypeError('config.keyFrameInterval, when provided, must be a non-negative number.');
|
|
||||||
}
|
|
||||||
if (config.fullCodecString !== undefined && typeof config.fullCodecString !== 'string') {
|
|
||||||
throw new TypeError('config.fullCodecString, when provided, must be a string.');
|
|
||||||
}
|
|
||||||
if (config.fullCodecString !== undefined && inferCodecFromCodecString(config.fullCodecString) !== config.codec) {
|
|
||||||
throw new TypeError(
|
|
||||||
`config.fullCodecString, when provided, must be a string that matches the specified codec`
|
|
||||||
+ ` (${config.codec}).`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (config.onEncodedPacket !== undefined && typeof config.onEncodedPacket !== 'function') {
|
|
||||||
throw new TypeError('config.onEncodedChunk, when provided, must be a function.');
|
|
||||||
}
|
|
||||||
if (config.onEncoderConfig !== undefined && typeof config.onEncoderConfig !== 'function') {
|
|
||||||
throw new TypeError('config.onEncoderConfig, when provided, must be a function.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class VideoEncoderWrapper {
|
class VideoEncoderWrapper {
|
||||||
private ensureEncoderPromise: Promise<void> | null = null;
|
private ensureEncoderPromise: Promise<void> | null = null;
|
||||||
private encoderInitialized = false;
|
private encoderInitialized = false;
|
||||||
@@ -370,26 +304,12 @@ class VideoEncoderWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.ensureEncoderPromise = (async () => {
|
return this.ensureEncoderPromise = (async () => {
|
||||||
const width = videoSample.codedWidth;
|
const encoderConfig = buildVideoEncoderConfig({
|
||||||
const height = videoSample.codedHeight;
|
width: videoSample.codedWidth,
|
||||||
const bitrate = this.encodingConfig.bitrate instanceof Quality
|
height: videoSample.codedHeight,
|
||||||
? this.encodingConfig.bitrate._toVideoBitrate(this.encodingConfig.codec, width, height)
|
...this.encodingConfig,
|
||||||
: this.encodingConfig.bitrate;
|
|
||||||
|
|
||||||
const encoderConfig: VideoEncoderConfig = {
|
|
||||||
codec: this.encodingConfig.fullCodecString ?? buildVideoCodecString(
|
|
||||||
this.encodingConfig.codec,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
bitrate,
|
|
||||||
),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
bitrate,
|
|
||||||
framerate: this.source._connectedTrack?.metadata.frameRate,
|
framerate: this.source._connectedTrack?.metadata.frameRate,
|
||||||
latencyMode: this.encodingConfig.latencyMode,
|
});
|
||||||
...getVideoEncoderConfigExtension(this.encodingConfig.codec),
|
|
||||||
};
|
|
||||||
this.encodingConfig.onEncoderConfig?.(encoderConfig);
|
this.encodingConfig.onEncoderConfig?.(encoderConfig);
|
||||||
|
|
||||||
const MatchingCustomEncoder = customVideoEncoders.find(x => x.supports(
|
const MatchingCustomEncoder = customVideoEncoders.find(x => x.supports(
|
||||||
@@ -813,67 +733,6 @@ export class EncodedAudioPacketSource extends AudioSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration object that controls audio encoding. Can be used to set codec, quality, and more.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export type AudioEncodingConfig = {
|
|
||||||
/** The audio codec that should be used for encoding the audio samples. */
|
|
||||||
codec: AudioCodec;
|
|
||||||
/**
|
|
||||||
* The target bitrate for the encoded audio, in bits per second. Alternatively, a subjective Quality can
|
|
||||||
* be provided. Required for compressed audio codecs, unused for PCM codecs.
|
|
||||||
*/
|
|
||||||
bitrate?: number | Quality;
|
|
||||||
/**
|
|
||||||
* The full codec string as specified in the WebCodecs Codec Registry. This string must match the codec
|
|
||||||
* specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library.
|
|
||||||
*/
|
|
||||||
fullCodecString?: string;
|
|
||||||
|
|
||||||
/** Called for each successfully encoded packet. Both the packet and the encoding metadata are passed. */
|
|
||||||
onEncodedPacket?: (packet: EncodedPacket, meta: EncodedAudioChunkMetadata | undefined) => unknown;
|
|
||||||
/** Called when the internal encoder config, as used by the WebCodecs API, is created. */
|
|
||||||
onEncoderConfig?: (config: AudioEncoderConfig) => unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateAudioEncodingConfig = (config: AudioEncodingConfig) => {
|
|
||||||
if (!config || typeof config !== 'object') {
|
|
||||||
throw new TypeError('Encoding config must be an object.');
|
|
||||||
}
|
|
||||||
if (!AUDIO_CODECS.includes(config.codec)) {
|
|
||||||
throw new TypeError(`Invalid audio codec '${config.codec}'. Must be one of: ${AUDIO_CODECS.join(', ')}.`);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
config.bitrate === undefined
|
|
||||||
&& (!(PCM_AUDIO_CODECS as readonly string[]).includes(config.codec) || config.codec === 'flac')
|
|
||||||
) {
|
|
||||||
throw new TypeError('config.bitrate must be provided for compressed audio codecs.');
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
config.bitrate !== undefined
|
|
||||||
&& !(config.bitrate instanceof Quality)
|
|
||||||
&& (!Number.isInteger(config.bitrate) || config.bitrate <= 0)
|
|
||||||
) {
|
|
||||||
throw new TypeError('config.bitrate, when provided, must be a positive integer or a quality.');
|
|
||||||
}
|
|
||||||
if (config.fullCodecString !== undefined && typeof config.fullCodecString !== 'string') {
|
|
||||||
throw new TypeError('config.fullCodecString, when provided, must be a string.');
|
|
||||||
}
|
|
||||||
if (config.fullCodecString !== undefined && inferCodecFromCodecString(config.fullCodecString) !== config.codec) {
|
|
||||||
throw new TypeError(
|
|
||||||
`config.fullCodecString, when provided, must be a string that matches the specified codec`
|
|
||||||
+ ` (${config.codec}).`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (config.onEncodedPacket !== undefined && typeof config.onEncodedPacket !== 'function') {
|
|
||||||
throw new TypeError('config.onEncodedChunk, when provided, must be a function.');
|
|
||||||
}
|
|
||||||
if (config.onEncoderConfig !== undefined && typeof config.onEncoderConfig !== 'function') {
|
|
||||||
throw new TypeError('config.onEncoderConfig, when provided, must be a function.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class AudioEncoderWrapper {
|
class AudioEncoderWrapper {
|
||||||
private ensureEncoderPromise: Promise<void> | null = null;
|
private ensureEncoderPromise: Promise<void> | null = null;
|
||||||
private encoderInitialized = false;
|
private encoderInitialized = false;
|
||||||
@@ -1061,21 +920,12 @@ class AudioEncoderWrapper {
|
|||||||
|
|
||||||
return this.ensureEncoderPromise = (async () => {
|
return this.ensureEncoderPromise = (async () => {
|
||||||
const { numberOfChannels, sampleRate } = audioSample;
|
const { numberOfChannels, sampleRate } = audioSample;
|
||||||
const bitrate = this.encodingConfig.bitrate instanceof Quality
|
|
||||||
? this.encodingConfig.bitrate._toAudioBitrate(this.encodingConfig.codec)
|
|
||||||
: this.encodingConfig.bitrate;
|
|
||||||
|
|
||||||
const encoderConfig: AudioEncoderConfig = {
|
const encoderConfig = buildAudioEncoderConfig({
|
||||||
codec: this.encodingConfig.fullCodecString ?? buildAudioCodecString(
|
|
||||||
this.encodingConfig.codec,
|
|
||||||
numberOfChannels,
|
|
||||||
sampleRate,
|
|
||||||
),
|
|
||||||
numberOfChannels,
|
numberOfChannels,
|
||||||
sampleRate,
|
sampleRate,
|
||||||
bitrate,
|
...this.encodingConfig,
|
||||||
...getAudioEncoderConfigExtension(this.encodingConfig.codec),
|
});
|
||||||
};
|
|
||||||
this.encodingConfig.onEncoderConfig?.(encoderConfig);
|
this.encodingConfig.onEncoderConfig?.(encoderConfig);
|
||||||
|
|
||||||
const MatchingCustomEncoder = customAudioEncoders.find(x => x.supports(
|
const MatchingCustomEncoder = customAudioEncoders.find(x => x.supports(
|
||||||
|
|||||||
Reference in New Issue
Block a user