Files
netris-nestri/packages/moq/contribute/video.ts
Wanjohi bae089e223 feat: Host a relay on Hetzner (#114)
We are hosting a [MoQ](https://quic.video) relay on a remote (bare
metal) server on Hetzner

With a lot of help from @victorpahuus
2024-09-26 21:34:42 +03:00

112 lines
2.6 KiB
TypeScript

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<VideoFrame, VideoDecoderConfig | EncodedVideoChunk>
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<EncodedVideoChunk>) {
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<VideoDecoderConfig | EncodedVideoChunk>,
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
}
}