webcodecs-encoder
Version:
A TypeScript library for browser environments to encode video (H.264/AVC, VP9, VP8) and audio (AAC, Opus) using the WebCodecs API and mux them into MP4 or WebM containers with real-time streaming support. New function-first API design.
304 lines • 9.33 kB
JavaScript
// src/utils/can-encode.ts
async function canEncode(options) {
try {
if (!isWebCodecsSupported()) {
return false;
}
if (!options) {
return await testDefaultConfiguration();
}
const hasVideoConfig = options.video && typeof options.video === "object";
const videoEnabled = options.video !== false;
if (videoEnabled) {
const videoConfig = hasVideoConfig ? options.video : void 0;
const videoCodec = videoConfig?.codec ?? "avc";
const videoSupported = await testVideoCodecSupport(videoCodec, options);
if (!videoSupported) {
return false;
}
}
const hasAudioConfig = options.audio && typeof options.audio === "object";
const audioEnabled = options.audio !== false;
if (audioEnabled) {
if (hasAudioConfig) {
const audioCodec = options.audio.codec || "aac";
const audioSupported = await testAudioCodecSupport(audioCodec, options);
if (!audioSupported) {
return false;
}
} else {
const fallbackCodecs = getDefaultAudioProbeOrder(options.container);
let foundSupportedAudioCodec = false;
for (const codec of fallbackCodecs) {
if (await testAudioCodecSupport(codec, options)) {
foundSupportedAudioCodec = true;
break;
}
}
if (!foundSupportedAudioCodec) {
return false;
}
}
}
return true;
} catch (error) {
console.warn("canEncode error:", error);
return false;
}
}
function isWebCodecsSupported() {
try {
return typeof VideoEncoder !== "undefined" && typeof AudioEncoder !== "undefined" && typeof VideoFrame !== "undefined" && typeof AudioData !== "undefined";
} catch {
return false;
}
}
async function testDefaultConfiguration() {
try {
const defaultWidth = 640;
const defaultHeight = 480;
const defaultFrameRate = 30;
const videoConfig = {
codec: generateAvcCodecString(
defaultWidth,
defaultHeight,
defaultFrameRate
),
width: defaultWidth,
height: defaultHeight,
bitrate: 1e6,
framerate: defaultFrameRate
};
const videoSupport = await VideoEncoder.isConfigSupported(videoConfig);
if (!videoSupport.supported) {
return false;
}
const audioConfig = {
codec: "mp4a.40.2",
// AAC-LC
sampleRate: 48e3,
numberOfChannels: 2,
bitrate: 128e3
};
const audioSupport = await AudioEncoder.isConfigSupported(audioConfig);
return audioSupport.supported || false;
} catch {
return false;
}
}
async function testVideoCodecSupport(codec, options) {
try {
const videoOptions = options?.video && typeof options.video === "object" ? options.video : {};
const codecString = videoOptions.codecString || getVideoCodecString(
codec,
options?.width || 640,
options?.height || 480,
options?.frameRate || 30
);
const config = {
codec: codecString,
width: options?.width || 640,
height: options?.height || 480,
bitrate: videoOptions.bitrate || 1e6,
framerate: options?.frameRate || 30
};
if (videoOptions.hardwareAcceleration) {
config.hardwareAcceleration = videoOptions.hardwareAcceleration;
}
if (videoOptions.latencyMode) {
config.latencyMode = videoOptions.latencyMode;
}
if (typeof videoOptions.quantizer === "number") {
config.quantizer = videoOptions.quantizer;
}
if (codec === "avc" && videoOptions.avc?.format) {
config.avc = { format: videoOptions.avc.format };
}
if (codec === "hevc" && videoOptions.hevc?.format) {
config.hevc = { format: videoOptions.hevc.format };
}
const support = await VideoEncoder.isConfigSupported(config);
return support.supported || false;
} catch {
return false;
}
}
async function testAudioCodecSupport(codec, options) {
try {
const audioOptions = typeof options?.audio === "object" ? options.audio : {};
const codecString = audioOptions.codecString || getAudioCodecString(codec);
const isTelephonyCodec = codec === "ulaw" || codec === "alaw";
const isPcmCodec = codec === "pcm";
const defaultSampleRate = audioOptions.sampleRate || (isTelephonyCodec ? 8e3 : 48e3);
const defaultChannels = audioOptions.channels || (isTelephonyCodec ? 1 : 2);
let defaultBitrate = audioOptions.bitrate;
if (defaultBitrate == null) {
if (codec === "flac") {
defaultBitrate = 512e3;
} else if (codec === "mp3") {
defaultBitrate = 128e3;
} else if (codec === "vorbis") {
defaultBitrate = 128e3;
} else if (isTelephonyCodec) {
defaultBitrate = 64e3;
} else if (isPcmCodec) {
defaultBitrate = defaultSampleRate * defaultChannels * 16;
} else {
defaultBitrate = 128e3;
}
}
const config = {
codec: codecString,
sampleRate: defaultSampleRate,
numberOfChannels: defaultChannels,
bitrate: defaultBitrate
};
if (codec === "aac" && audioOptions.bitrateMode) {
config.bitrateMode = audioOptions.bitrateMode;
}
if (codec === "aac" && audioOptions.aac?.format) {
config.aac = { format: audioOptions.aac.format };
}
const support = await AudioEncoder.isConfigSupported(config);
return support.supported || false;
} catch {
return false;
}
}
function getVideoCodecString(codec, width = 640, height = 480, frameRate = 30) {
switch (codec) {
case "avc":
return generateAvcCodecString(width, height, frameRate);
case "hevc":
return "hvc1";
// Align with worker default
case "vp9":
return "vp09.00.50.08";
// Align with worker fallback string
case "vp8":
return "vp8";
// VP8
case "av1":
return "av01.0.04M.08";
// AV1 Main Profile Level 4.0
default:
return codec;
}
}
function generateAvcCodecString(width, height, frameRate, profile) {
const mbPerSec = Math.ceil(width / 16) * Math.ceil(height / 16) * frameRate;
let level;
if (mbPerSec <= 108e3) level = 31;
else if (mbPerSec <= 216e3) level = 32;
else if (mbPerSec <= 245760) level = 40;
else if (mbPerSec <= 589824) level = 50;
else if (mbPerSec <= 983040) level = 51;
else level = 52;
const chosenProfile = profile ?? (width >= 1280 || height >= 720 ? "high" : "baseline");
const profileHex = chosenProfile === "high" ? "64" : chosenProfile === "main" ? "4d" : "42";
const levelHex = level.toString(16).padStart(2, "0");
return `avc1.${profileHex}00${levelHex}`;
}
async function generateSupportedAvcCodecString(width, height, frameRate, bitrate, preferredProfile) {
const profiles = preferredProfile ? [preferredProfile, "main", "baseline", "high"].filter(
(p, i, arr) => arr.indexOf(p) === i
// remove duplicates
) : ["high", "main", "baseline"];
for (const profile of profiles) {
const codecString = generateAvcCodecString(
width,
height,
frameRate,
profile
);
try {
const config = {
codec: codecString,
width,
height,
bitrate,
framerate: frameRate
};
const support = await VideoEncoder.isConfigSupported(config);
if (support.supported) {
return codecString;
}
} catch (error) {
console.warn(
`Failed to check support for AVC profile ${profile}:`,
error
);
}
}
return null;
}
function getAudioCodecString(codec) {
switch (codec) {
case "aac":
return "mp4a.40.2";
// AAC-LC
case "opus":
return "opus";
// Opus
case "flac":
return "flac";
case "mp3":
return "mp3";
case "vorbis":
return "vorbis";
case "pcm":
return "pcm";
case "ulaw":
return "ulaw";
case "alaw":
return "alaw";
default:
return codec;
}
}
function getDefaultAudioProbeOrder(container) {
if (container === "webm") {
return ["opus", "vorbis", "flac"];
}
return ["aac", "mp3"];
}
async function canEncodeWithProfile(videoCodec, audioCodec, profile) {
const result = { video: false, audio: false, overall: false };
try {
if (videoCodec) {
const videoConfig = {
codec: getVideoCodecString(videoCodec),
width: profile?.width || 1920,
height: profile?.height || 1080,
bitrate: profile?.videoBitrate || 2e6,
framerate: profile?.framerate || 30
};
const videoSupport = await VideoEncoder.isConfigSupported(videoConfig);
result.video = videoSupport.supported || false;
}
if (audioCodec) {
const audioConfig = {
codec: getAudioCodecString(audioCodec),
sampleRate: 48e3,
numberOfChannels: 2,
bitrate: profile?.audioBitrate || 128e3
};
const audioSupport = await AudioEncoder.isConfigSupported(audioConfig);
result.audio = audioSupport.supported || false;
} else {
result.audio = true;
}
result.overall = result.video && result.audio;
return result;
} catch (error) {
console.warn("canEncodeWithProfile error:", error);
return result;
}
}
export {
canEncode,
canEncodeWithProfile,
generateAvcCodecString,
generateSupportedAvcCodecString
};
//# sourceMappingURL=can-encode.js.map