const SUPPORTED = [ "avc1", // H.264 "hev1", // HEVC (aka h.265) // "av01", // TDOO support AV1 ] export interface EncoderSupported { codecs: string[] } export class Encoder { #encoder!: VideoEncoder #encoderConfig: VideoEncoderConfig #decoderConfig?: VideoDecoderConfig // true if we should insert a keyframe, undefined when the encoder should decide #keyframeNext: true | undefined = true // Count the number of frames without a keyframe. #keyframeCounter = 0 // Converts raw rames to encoded frames. frames: TransformStream constructor(config: VideoEncoderConfig) { config.bitrateMode ??= "constant" config.latencyMode ??= "realtime" this.#encoderConfig = config this.frames = new TransformStream({ start: this.#start.bind(this), transform: this.#transform.bind(this), flush: this.#flush.bind(this), }) } static async isSupported(config: VideoEncoderConfig) { // Check if we support a specific codec family const short = config.codec.substring(0, 4) if (!SUPPORTED.includes(short)) return false // Default to hardware encoding config.hardwareAcceleration ??= "prefer-hardware" // Default to CBR config.bitrateMode ??= "constant" // Default to realtime encoding config.latencyMode ??= "realtime" const res = await VideoEncoder.isConfigSupported(config) return !!res.supported } #start(controller: TransformStreamDefaultController) { this.#encoder = new VideoEncoder({ output: (frame, metadata) => { this.#enqueue(controller, frame, metadata) }, error: (err) => { throw err }, }) this.#encoder.configure(this.#encoderConfig) } #transform(frame: VideoFrame) { const encoder = this.#encoder // Set keyFrame to undefined when we're not sure so the encoder can decide. encoder.encode(frame, { keyFrame: this.#keyframeNext }) this.#keyframeNext = undefined frame.close() } #enqueue( controller: TransformStreamDefaultController, frame: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata, ) { if (!this.#decoderConfig) { const config = metadata?.decoderConfig if (!config) throw new Error("missing decoder config") controller.enqueue(config) this.#decoderConfig = config } if (frame.type === "key") { this.#keyframeCounter = 0 } else { this.#keyframeCounter += 1 if (this.#keyframeCounter + this.#encoder.encodeQueueSize >= 2 * this.#encoderConfig.framerate!) { this.#keyframeNext = true } } controller.enqueue(frame) } #flush() { this.#encoder.close() } get config() { return this.#encoderConfig } }