mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
✨ 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
This commit is contained in:
111
packages/moq/contribute/video.ts
Normal file
111
packages/moq/contribute/video.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user