UNPKG

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
"use strict"; 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