/// import * as Message from "./worker/message" import { Audio } from "./audio" import MediaWorker from "./worker?worker" import { RingShared } from "../common/ring" import { Root, isAudioTrack } from "../media/catalog" import { GroupHeader } from "../transport/objects" export interface PlayerConfig { canvas: OffscreenCanvas catalog: Root } // This is a non-standard way of importing worklet/workers. // Unfortunately, it's the only option because of a Vite bug: https://github.com/vitejs/vite/issues/11823 // Responsible for sending messages to the worker and worklet. export default class Backend { // General worker #worker: Worker // The audio context, which must be created on the main thread. #audio?: Audio constructor(config: PlayerConfig) { // TODO does this block the main thread? If so, make this async // @ts-expect-error: The Vite typing is wrong https://github.com/vitejs/vite/blob/22bd67d70a1390daae19ca33d7de162140d533d6/packages/vite/client.d.ts#L182 this.#worker = new MediaWorker({ format: "es" }) this.#worker.addEventListener("message", this.on.bind(this)) let sampleRate: number | undefined let channels: number | undefined for (const track of config.catalog.tracks) { if (isAudioTrack(track)) { if (sampleRate && track.selectionParams.samplerate !== sampleRate) { throw new Error(`TODO multiple audio tracks with different sample rates`) } sampleRate = track.selectionParams.samplerate // TODO properly handle weird channel configs channels = Math.max(+track.selectionParams.channelConfig, channels ?? 0) } } const msg: Message.Config = {} // Only configure audio is we have an audio track if (sampleRate && channels) { msg.audio = { channels: channels, sampleRate: sampleRate, ring: new RingShared(2, sampleRate / 10), // 100ms } this.#audio = new Audio(msg.audio) } // TODO only send the canvas if we have a video track msg.video = { canvas: config.canvas, } this.send({ config: msg }, msg.video.canvas) } async play() { await this.#audio?.context.resume() } init(init: Init) { this.send({ init }) } segment(segment: Segment) { this.send({ segment }, segment.stream) } async close() { this.#worker.terminate() await this.#audio?.context.close() } // Enforce we're sending valid types to the worker private send(msg: Message.ToWorker, ...transfer: Transferable[]) { //console.log("sent message from main to worker", msg) this.#worker.postMessage(msg, transfer) } private on(e: MessageEvent) { const msg = e.data as Message.FromWorker // Don't print the verbose timeline message. if (!msg.timeline) { //console.log("received message from worker to main", msg) } } } export interface Init { name: string // name of the init track data: Uint8Array } export interface Segment { init: string // name of the init track kind: "audio" | "video" header: GroupHeader buffer: Uint8Array stream: ReadableStream }