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.
271 lines (270 loc) • 8.53 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/utils/can-encode.ts
var can_encode_exports = {};
__export(can_encode_exports, {
canEncode: () => canEncode,
canEncodeWithProfile: () => canEncodeWithProfile,
generateAvcCodecString: () => generateAvcCodecString,
generateSupportedAvcCodecString: () => generateSupportedAvcCodecString
});
module.exports = __toCommonJS(can_encode_exports);
async function canEncode(options) {
try {
if (!isWebCodecsSupported()) {
return false;
}
if (!options) {
return await testDefaultConfiguration();
}
const hasVideoConfig = options.video && typeof options.video === "object";
const hasVideo = hasVideoConfig || !options.audio;
if (hasVideo) {
const videoCodec = hasVideoConfig ? options.video.codec || "avc" : "avc";
const videoSupported = await testVideoCodecSupport(videoCodec, options);
if (!videoSupported) {
return false;
}
}
const hasAudioConfig = options.audio && typeof options.audio === "object";
if (hasAudioConfig) {
const audioCodec = options.audio.codec || "aac";
const audioSupported = await testAudioCodecSupport(audioCodec, options);
if (!audioSupported) {
return false;
}
} else if (options.audio === void 0 && !hasVideoConfig) {
const audioSupported = await testAudioCodecSupport("aac", options);
if (!audioSupported) {
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 = 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;
}
const support = await VideoEncoder.isConfigSupported(config);
return support.supported || false;
} catch {
return false;
}
}
async function testAudioCodecSupport(codec, options) {
try {
const codecString = getAudioCodecString(codec);
const audioOptions = typeof options?.audio === "object" ? options.audio : {};
const config = {
codec: codecString,
sampleRate: audioOptions.sampleRate || 48e3,
numberOfChannels: audioOptions.channels || 2,
bitrate: audioOptions.bitrate || 128e3
};
if (codec === "aac" && audioOptions.bitrateMode) {
config.bitrateMode = audioOptions.bitrateMode;
}
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 "hev1.1.6.L93.B0";
// H.265 Main Profile
case "vp9":
return "vp09.00.10.08";
// VP9 Profile 0
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
default:
return codec;
}
}
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;
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
canEncode,
canEncodeWithProfile,
generateAvcCodecString,
generateSupportedAvcCodecString
});
//# sourceMappingURL=can-encode.cjs.map