mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
We are hosting a [MoQ](https://quic.video) relay on a remote (bare metal) server on Hetzner With a lot of help from @victorpahuus
112 lines
2.6 KiB
TypeScript
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
|
|
}
|
|
}
|