mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +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:
84
packages/moq/playback/worker/video.ts
Normal file
84
packages/moq/playback/worker/video.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Frame, Component } from "./timeline"
|
||||
import * as MP4 from "../../media/mp4"
|
||||
import * as Message from "./message"
|
||||
|
||||
export class Renderer {
|
||||
#canvas: OffscreenCanvas
|
||||
#timeline: Component
|
||||
|
||||
#decoder!: VideoDecoder
|
||||
#queue: TransformStream<Frame, VideoFrame>
|
||||
|
||||
constructor(config: Message.ConfigVideo, timeline: Component) {
|
||||
this.#canvas = config.canvas
|
||||
this.#timeline = timeline
|
||||
|
||||
this.#queue = new TransformStream({
|
||||
start: this.#start.bind(this),
|
||||
transform: this.#transform.bind(this),
|
||||
})
|
||||
|
||||
this.#run().catch(console.error)
|
||||
}
|
||||
|
||||
async #run() {
|
||||
const reader = this.#timeline.frames.pipeThrough(this.#queue).getReader()
|
||||
for (;;) {
|
||||
const { value: frame, done } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
self.requestAnimationFrame(() => {
|
||||
this.#canvas.width = frame.displayWidth
|
||||
this.#canvas.height = frame.displayHeight
|
||||
|
||||
const ctx = this.#canvas.getContext("2d")
|
||||
if (!ctx) throw new Error("failed to get canvas context")
|
||||
|
||||
ctx.drawImage(frame, 0, 0, frame.displayWidth, frame.displayHeight) // TODO respect aspect ratio
|
||||
frame.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#start(controller: TransformStreamDefaultController<VideoFrame>) {
|
||||
this.#decoder = new VideoDecoder({
|
||||
output: (frame: VideoFrame) => {
|
||||
controller.enqueue(frame)
|
||||
},
|
||||
error: console.error,
|
||||
})
|
||||
}
|
||||
|
||||
#transform(frame: Frame) {
|
||||
// Configure the decoder with the first frame
|
||||
if (this.#decoder.state !== "configured") {
|
||||
const { sample, track } = frame
|
||||
|
||||
const desc = sample.description
|
||||
const box = desc.avcC ?? desc.hvcC ?? desc.vpcC ?? desc.av1C
|
||||
if (!box) throw new Error(`unsupported codec: ${track.codec}`)
|
||||
|
||||
const buffer = new MP4.Stream(undefined, 0, MP4.Stream.BIG_ENDIAN)
|
||||
box.write(buffer)
|
||||
const description = new Uint8Array(buffer.buffer, 8) // Remove the box header.
|
||||
|
||||
if (!MP4.isVideoTrack(track)) throw new Error("expected video track")
|
||||
|
||||
this.#decoder.configure({
|
||||
codec: track.codec,
|
||||
codedHeight: track.video.height,
|
||||
codedWidth: track.video.width,
|
||||
description,
|
||||
// optimizeForLatency: true
|
||||
})
|
||||
}
|
||||
|
||||
const chunk = new EncodedVideoChunk({
|
||||
type: frame.sample.is_sync ? "key" : "delta",
|
||||
data: frame.sample.data,
|
||||
timestamp: frame.sample.dts / frame.track.timescale,
|
||||
})
|
||||
|
||||
this.#decoder.decode(chunk)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user