import * as Message from "./worker/message" import { Connection } from "../transport/connection" import * as Catalog from "../media/catalog" import { asError } from "../common/error" import Backend from "./backend" import { Client } from "../transport/client" import { GroupReader } from "../transport/objects" export type Range = Message.Range export type Timeline = Message.Timeline export interface PlayerConfig { url: string namespace: string fingerprint?: string // URL to fetch TLS certificate fingerprint canvas: HTMLCanvasElement } // This class must be created on the main thread due to AudioContext. export class Player { #backend: Backend // A periodically updated timeline //#timeline = new Watch(undefined) #connection: Connection #catalog: Catalog.Root // Running is a promise that resolves when the player is closed. // #close is called with no error, while #abort is called with an error. #running: Promise #close!: () => void #abort!: (err: Error) => void private constructor(connection: Connection, catalog: Catalog.Root, backend: Backend) { this.#connection = connection this.#catalog = catalog this.#backend = backend const abort = new Promise((resolve, reject) => { this.#close = resolve this.#abort = reject }) // Async work this.#running = Promise.race([this.#run(), abort]).catch(this.#close) } static async create(config: PlayerConfig): Promise { const client = new Client({ url: config.url, fingerprint: config.fingerprint, role: "subscriber" }) const connection = await client.connect() const catalog = await Catalog.fetch(connection, config.namespace) console.log("catalog", catalog) const canvas = config.canvas.transferControlToOffscreen() const backend = new Backend({ canvas, catalog }) return new Player(connection, catalog, backend) } async #run() { const inits = new Set<[string, string]>() const tracks = new Array() for (const track of this.#catalog.tracks) { if (!track.namespace) throw new Error("track has no namespace") if (track.initTrack) inits.add([track.namespace, track.initTrack]) tracks.push(track) } // Call #runInit on each unique init track // TODO do this in parallel with #runTrack to remove a round trip await Promise.all(Array.from(inits).map((init) => this.#runInit(...init))) // Call #runTrack on each track await Promise.all(tracks.map((track) => this.#runTrack(track))) } async #runInit(namespace: string, name: string) { const sub = await this.#connection.subscribe(namespace, name) try { const init = await Promise.race([sub.data(), this.#running]) if (!init) throw new Error("no init data") // We don't care what type of reader we get, we just want the payload. const chunk = await init.read() if (!chunk) throw new Error("no init chunk") if (!(chunk.payload instanceof Uint8Array)) throw new Error("invalid init chunk") this.#backend.init({ data: chunk.payload, name }) } finally { await sub.close() } } async #runTrack(track: Catalog.Track) { if (!track.namespace) throw new Error("track has no namespace") const sub = await this.#connection.subscribe(track.namespace, track.name) try { for (;;) { const segment = await Promise.race([sub.data(), this.#running]) if (!segment) break if (!(segment instanceof GroupReader)) { throw new Error(`expected group reader for segment: ${track.name}`) } const kind = Catalog.isVideoTrack(track) ? "video" : Catalog.isAudioTrack(track) ? "audio" : "unknown" if (kind == "unknown") { throw new Error(`unknown track kind: ${track.name}`) } if (!track.initTrack) { throw new Error(`no init track for segment: ${track.name}`) } const [buffer, stream] = segment.stream.release() this.#backend.segment({ init: track.initTrack, kind, header: segment.header, buffer, stream, }) } } catch (error) { console.error("Error in #runTrack:", error) } finally { await sub.close() } } getCatalog() { return this.#catalog } #onMessage(msg: Message.FromWorker) { if (msg.timeline) { //this.#timeline.update(msg.timeline) } } async close(err?: Error) { if (err) this.#abort(err) else this.#close() if (this.#connection) this.#connection.close() if (this.#backend) await this.#backend.close() } async closed(): Promise { try { await this.#running } catch (e) { return asError(e) } } /* play() { this.#backend.play({ minBuffer: 0.5 }) // TODO configurable } seek(timestamp: number) { this.#backend.seek({ timestamp }) } */ async play() { await this.#backend.play() } /* async *timeline() { for (;;) { const [timeline, next] = this.#timeline.value() if (timeline) yield timeline if (!next) break await next } } */ }