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.

304 lines 9.33 kB
// 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