From f73894665cc2b0a96d4f83d766ec64a4f72fdb9d Mon Sep 17 00:00:00 2001 From: kixelated Date: Sat, 20 Apr 2024 06:00:53 -0700 Subject: [PATCH] Remove MSE support (#98) --- .vscode/settings.json | 2 +- lib/playback/backend.ts | 98 ++++++++++- lib/playback/{webcodecs => }/context.ts | 2 +- lib/playback/index.ts | 34 ++-- lib/playback/mse/index.ts | 157 ------------------ lib/playback/mse/segment.ts | 126 -------------- lib/playback/mse/source.ts | 129 -------------- lib/playback/mse/track.ts | 80 --------- lib/playback/tsconfig.json | 2 +- lib/playback/webcodecs/index.ts | 99 ----------- lib/playback/{webcodecs => worker}/audio.ts | 0 .../{webcodecs/worker.ts => worker/index.ts} | 0 lib/playback/{webcodecs => worker}/message.ts | 0 .../{webcodecs => worker}/timeline.ts | 0 lib/playback/{webcodecs => worker}/video.ts | 0 lib/playback/{webcodecs => }/worklet/index.ts | 2 +- .../{webcodecs => }/worklet/message.ts | 2 +- .../{webcodecs => }/worklet/tsconfig.json | 4 +- lib/tsconfig.json | 2 +- web/src/components/watch.tsx | 56 +------ 20 files changed, 117 insertions(+), 678 deletions(-) rename lib/playback/{webcodecs => }/context.ts (96%) delete mode 100644 lib/playback/mse/index.ts delete mode 100644 lib/playback/mse/segment.ts delete mode 100644 lib/playback/mse/source.ts delete mode 100644 lib/playback/mse/track.ts delete mode 100644 lib/playback/webcodecs/index.ts rename lib/playback/{webcodecs => worker}/audio.ts (100%) rename lib/playback/{webcodecs/worker.ts => worker/index.ts} (100%) rename lib/playback/{webcodecs => worker}/message.ts (100%) rename lib/playback/{webcodecs => worker}/timeline.ts (100%) rename lib/playback/{webcodecs => worker}/video.ts (100%) rename lib/playback/{webcodecs => }/worklet/index.ts (96%) rename lib/playback/{webcodecs => }/worklet/message.ts (72%) rename lib/playback/{webcodecs => }/worklet/tsconfig.json (69%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7122f2f..b9b1c97 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,7 @@ ], "eslint.run": "onSave", "editor.codeActionsOnSave": { - "source.fixAll": true + "source.fixAll": "explicit" }, "editor.formatOnSave": true, // Tell VSCode to format files on save "editor.defaultFormatter": "esbenp.prettier-vscode", diff --git a/lib/playback/backend.ts b/lib/playback/backend.ts index f9f13cd..dd26ba1 100644 --- a/lib/playback/backend.ts +++ b/lib/playback/backend.ts @@ -1,12 +1,102 @@ -import { Catalog } from "../media/catalog" -import { GroupHeader } from "../transport/objects" +/// + +import * as Message from "./worker/message" +import { Context } from "./context" -// TODO make an interface for backends +import MediaWorker from "./worker?worker" +import { RingShared } from "../common/ring" +import { Catalog, isAudioTrack } from "../media/catalog" +import { GroupHeader } from "../transport/objects" -export interface Config { +export interface PlayerConfig { + canvas: OffscreenCanvas catalog: Catalog } +// 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. + #context?: Context + + 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.sample_rate !== sampleRate) { + throw new Error(`TODO multiple audio tracks with different sample rates`) + } + + sampleRate = track.sample_rate + channels = Math.max(track.channel_count, 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 / 20), // 50ms + } + + this.#context = new Context(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) + } + + // TODO initialize context now since the user clicked + play() {} + + init(init: Init) { + this.send({ init }) + } + + segment(segment: Segment) { + this.send({ segment }, segment.stream) + } + + async close() { + this.#worker.terminate() + await this.#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 diff --git a/lib/playback/webcodecs/context.ts b/lib/playback/context.ts similarity index 96% rename from lib/playback/webcodecs/context.ts rename to lib/playback/context.ts index c8dc125..a617917 100644 --- a/lib/playback/webcodecs/context.ts +++ b/lib/playback/context.ts @@ -1,6 +1,6 @@ /// -import * as Message from "./message" +import * as Message from "./worker/message" // 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 diff --git a/lib/playback/index.ts b/lib/playback/index.ts index ca23816..6feb46b 100644 --- a/lib/playback/index.ts +++ b/lib/playback/index.ts @@ -1,12 +1,11 @@ -import * as Message from "./webcodecs/message" +import * as Message from "./worker/message" import { Connection } from "../transport/connection" -import { Catalog, isAudioTrack, isMp4Track, Mp4Track } from "../media/catalog" +import { Catalog, isMp4Track, Mp4Track } from "../media/catalog" import { asError } from "../common/error" -// We support two different playback implementations: -import Webcodecs from "./webcodecs" -import MSE from "./mse" +import Backend from "./backend" + import { Client } from "../transport/client" import { GroupReader } from "../transport/objects" @@ -17,12 +16,12 @@ export interface PlayerConfig { url: string namespace: string fingerprint?: string // URL to fetch TLS certificate fingerprint - element: HTMLCanvasElement | HTMLVideoElement + canvas: HTMLCanvasElement } // This class must be created on the main thread due to AudioContext. export class Player { - #backend: Webcodecs | MSE + #backend: Backend // A periodically updated timeline //#timeline = new Watch(undefined) @@ -36,7 +35,7 @@ export class Player { #close!: () => void #abort!: (err: Error) => void - private constructor(connection: Connection, catalog: Catalog, backend: Webcodecs | MSE) { + private constructor(connection: Connection, catalog: Catalog, backend: Backend) { this.#connection = connection this.#catalog = catalog this.#backend = backend @@ -57,14 +56,8 @@ export class Player { const catalog = new Catalog(config.namespace) await catalog.fetch(connection) - let backend - - if (config.element instanceof HTMLCanvasElement) { - const element = config.element.transferControlToOffscreen() - backend = new Webcodecs({ element, catalog }) - } else { - backend = new MSE({ element: config.element }) - } + const canvas = config.canvas.transferControlToOffscreen() + const backend = new Backend({ canvas, catalog }) return new Player(connection, catalog, backend) } @@ -78,11 +71,6 @@ export class Player { throw new Error(`expected CMAF track`) } - if (isAudioTrack(track) && this.#backend instanceof MSE) { - // TODO temporary hack to disable audio in MSE - continue - } - inits.add(track.init_track) tracks.push(track) } @@ -173,8 +161,8 @@ export class Player { } */ - async play() { - await this.#backend.play() + play() { + this.#backend.play() } /* diff --git a/lib/playback/mse/index.ts b/lib/playback/mse/index.ts deleted file mode 100644 index 1fc11a9..0000000 --- a/lib/playback/mse/index.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { Source } from "./source" -import { Segment } from "./segment" -import { Track } from "./track" -import * as MP4 from "../../media/mp4" -import * as Message from "../backend" -import { GroupReader, Reader } from "../../transport/objects" -import { Deferred } from "../../common/async" - -export interface PlayerConfig { - element: HTMLVideoElement -} - -export default class Player { - #source: MediaSource - - // A map of init tracks. - #inits = new Map>() - - #audio: Track - #video: Track - - #element: HTMLVideoElement - #interval: number - - constructor(config: PlayerConfig) { - this.#element = config.element - - this.#source = new MediaSource() - this.#element.src = URL.createObjectURL(this.#source) - this.#element.addEventListener("play", () => { - this.play().catch(console.warn) - }) - - this.#audio = new Track(new Source(this.#source)) - this.#video = new Track(new Source(this.#source)) - - this.#interval = setInterval(this.#tick.bind(this), 100) - this.#element.addEventListener("waiting", this.#tick.bind(this)) - } - - #tick() { - // Try skipping ahead if there's no data in the current buffer. - this.#trySeek() - - // Try skipping video if it would fix any desync. - this.#trySkip() - } - - // Seek to the end and then play - async play() { - const ranges = this.#element.buffered - if (!ranges.length) { - return - } - - this.#element.currentTime = ranges.end(ranges.length - 1) - await this.#element.play() - } - - // Try seeking ahead to the next buffered range if there's a gap - #trySeek() { - if (this.#element.readyState > 2) { - // HAVE_CURRENT_DATA - // No need to seek - return - } - - const ranges = this.#element.buffered - if (!ranges.length) { - // Video has not started yet - return - } - - for (let i = 0; i < ranges.length; i += 1) { - const pos = ranges.start(i) - - if (this.#element.currentTime >= pos) { - // This would involve seeking backwards - continue - } - - console.warn("seeking forward", pos - this.#element.currentTime) - - this.#element.currentTime = pos - return - } - } - - // Try dropping video frames if there is future data available. - #trySkip() { - let playhead: number | undefined - - if (this.#element.readyState > 2) { - // If we're not buffering, only skip video if it's before the current playhead - playhead = this.#element.currentTime - } - - this.#video.advance(playhead) - } - - init(msg: Message.Init) { - let init = this.#inits.get(msg.name) - if (!init) { - init = new Deferred() - this.#inits.set(msg.name, init) - } - - init.resolve(msg.data) - } - - segment(msg: Message.Segment) { - this.#runSegment(msg).catch((e) => console.warn("failed to run segment", e)) - } - - async #runSegment(msg: Message.Segment) { - let init = this.#inits.get(msg.init) - if (!init) { - init = new Deferred() - this.#inits.set(msg.init, init) - } - - const container = new MP4.Parser(await init.promise) - - let track: Track - if (container.info.videoTracks.length) { - track = this.#video - } else { - track = this.#audio - } - - const reader = new Reader(msg.buffer, msg.stream) - const group = new GroupReader(msg.header, reader) - - const segment = new Segment(track.source, await init.promise, group.header.group) - track.add(segment) - - for (;;) { - const chunk = await group.read() - if (!chunk) { - break - } - - const frames = container.decode(chunk.payload) - for (const frame of frames) { - segment.push(frame.sample) - } - - track.flush() - } - - segment.finish() - } - - close() { - clearInterval(this.#interval) - } -} diff --git a/lib/playback/mse/segment.ts b/lib/playback/mse/segment.ts deleted file mode 100644 index ea6765d..0000000 --- a/lib/playback/mse/segment.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Source } from "./source" -import * as MP4 from "../../media/mp4" - -// Manage a segment download, keeping a buffer of a single sample to potentially rewrite the duration. -export class Segment { - source: Source // The SourceBuffer used to decode media. - offset: number // The byte offset in the received file so far - samples: MP4.Sample[] // The samples ready to be flushed to the source. - init: Uint8Array - - sequence: number // The order within the track - dts?: number // The parsed DTS of the first sample - timescale?: number // The parsed timescale of the segment - - output: MP4.ISOFile // MP4Box file used to write the outgoing atoms after modification. - - done: boolean // The segment has been completed - - constructor(source: Source, init: Uint8Array, sequence: number) { - this.source = source - this.offset = 0 - this.done = false - this.init = init - this.sequence = sequence - - this.output = MP4.New() - this.samples = [] - - // For some reason we need to modify the underlying ArrayBuffer with offset - const copy = new Uint8Array(init) - const buffer = copy.buffer as MP4.ArrayBuffer - buffer.fileStart = 0 - - // Populate the output with our init segment so it knows about tracks - this.output.appendBuffer(buffer) - this.output.flush() - } - - push(sample: MP4.Sample) { - if (this.dts === undefined) { - this.dts = sample.dts - this.timescale = sample.timescale - } - - // Add the samples to a queue - this.samples.push(sample) - } - - // Flushes any pending samples, returning true if the stream has finished. - flush(): boolean { - const stream = new MP4.Stream(new ArrayBuffer(0), 0, false) // big-endian - - while (this.samples.length) { - // Keep a single sample if we're not done yet - if (!this.done && this.samples.length < 2) break - - const sample = this.samples.shift() - if (!sample) break - - const moof = this.output.createSingleSampleMoof(sample) - moof.write(stream) - - // adjusting the data_offset now that the moof size is known - // TODO find a better way to do this or remove it? - const trun = moof.trafs[0].truns[0] - if (trun.data_offset_position && moof.size) { - trun.data_offset = moof.size + 8 // 8 is mdat header - stream.adjustUint32(trun.data_offset_position, trun.data_offset) - } - - const mdat = new MP4.BoxParser.mdatBox() - mdat.data = sample.data - mdat.write(stream) - } - - if (stream.buffer.byteLength == 0) { - return this.done - } - - this.source.initialize(this.init) - this.source.append(stream.buffer) - - return this.done - } - - // The segment has completed - finish() { - this.done = true - this.flush() - - // Trim the buffer to 30s long after each segment. - this.source.trim(30) - } - - // Extend the last sample so it reaches the provided timestamp - skipTo(pts: number) { - if (this.samples.length == 0) return - const last = this.samples[this.samples.length - 1] - - const skip = pts - (last.dts + last.duration) - - if (skip == 0) return - if (skip < 0) throw "can't skip backwards" - - last.duration += skip - - if (this.timescale) { - console.warn("skipping video", skip / this.timescale) - } - } - - buffered() { - // Ignore if we have a single sample - if (this.samples.length <= 1) return undefined - if (!this.timescale) return undefined - - const first = this.samples[0] - const last = this.samples[this.samples.length - 1] - - return { - length: 1, - start: first.dts / this.timescale, - end: (last.dts + last.duration) / this.timescale, - } - } -} diff --git a/lib/playback/mse/source.ts b/lib/playback/mse/source.ts deleted file mode 100644 index 349506c..0000000 --- a/lib/playback/mse/source.ts +++ /dev/null @@ -1,129 +0,0 @@ -import * as MP4 from "../../media/mp4" - -// Create a SourceBuffer with convenience methods -export class Source { - sourceBuffer?: SourceBuffer - mediaSource: MediaSource - queue: Array - - constructor(mediaSource: MediaSource) { - this.mediaSource = mediaSource - this.queue = [] - } - - // (re)initialize the source using the provided init segment. - initialize(init: Uint8Array) { - // Add the init segment to the queue so we call addSourceBuffer or changeType - this.queue.push({ - kind: "init", - init: init, - }) - - this.queue.push({ - kind: "data", - data: init, - }) - - this.flush() - } - - // Append the segment data to the buffer. - append(data: Uint8Array | ArrayBuffer) { - if (data.byteLength == 0) { - throw new Error("empty append") - } - - this.queue.push({ - kind: "data", - data: data, - }) - - this.flush() - } - - // Return the buffered range. - buffered() { - if (!this.sourceBuffer) { - return { length: 0 } - } - - return this.sourceBuffer.buffered - } - - // Delete any media older than x seconds from the buffer. - trim(duration: number) { - this.queue.push({ - kind: "trim", - trim: duration, - }) - - this.flush() - } - - // Flush any queued instructions - flush() { - for (;;) { - // Check if the buffer is currently busy. - if (this.sourceBuffer && this.sourceBuffer.updating) { - break - } - - // Process the next item in the queue. - const next = this.queue.shift() - if (!next) { - break - } - - if (next.kind == "init") { - const parsed = new MP4.Parser(next.init) - if (!this.sourceBuffer) { - // Create a new source buffer. - this.sourceBuffer = this.mediaSource.addSourceBuffer(parsed.info.mime) - - // Call flush automatically after each update finishes. - this.sourceBuffer.addEventListener("updateend", this.flush.bind(this)) - } else { - this.sourceBuffer.changeType(parsed.info.mime) - } - } else if (next.kind == "data") { - if (!this.sourceBuffer) { - throw "failed to call initailize before append" - } - - this.sourceBuffer.appendBuffer(next.data) - } else if (next.kind == "trim") { - if (!this.sourceBuffer) { - throw "failed to call initailize before trim" - } - - if (this.sourceBuffer.buffered.length == 0) { - break - } - - const end = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length - 1) - next.trim - const start = this.sourceBuffer.buffered.start(0) - - if (end > start) { - this.sourceBuffer.remove(start, end) - } - } else { - throw "impossible; unknown SourceItem" - } - } - } -} - -interface SourceInit { - kind: "init" - init: Uint8Array -} - -interface SourceData { - kind: "data" - data: Uint8Array | ArrayBuffer -} - -interface SourceTrim { - kind: "trim" - trim: number -} diff --git a/lib/playback/mse/track.ts b/lib/playback/mse/track.ts deleted file mode 100644 index 9cb2334..0000000 --- a/lib/playback/mse/track.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Source } from "./source" -import { Segment } from "./segment" - -// An audio or video track that consists of multiple sequential segments. -// -// Instead of buffering, we want to drop video while audio plays uninterupted. -// Chrome actually plays up to 3s of audio without video before buffering when in low latency mode. -// Unforuntately, this does not recover correctly when there are gaps (pls fix). -// Our solution is to flush segments in decode order, buffering a single additional frame. -// We extend the duration of the buffered frame and flush it to cover any gaps. -export class Track { - source: Source - segments: Segment[] - - constructor(source: Source) { - this.source = source - this.segments = [] - } - - add(segment: Segment) { - // TODO don't add if the segment is out of date already - this.segments.push(segment) - - // Sort by timestamp ascending - // NOTE: The timestamp is in milliseconds, and we need to parse the media to get the accurate PTS/DTS. - this.segments.sort((a: Segment, b: Segment): number => { - return a.sequence - b.sequence - }) - } - - flush() { - for (;;) { - if (!this.segments.length) break - - const first = this.segments[0] - const done = first.flush() - if (!done) break - - this.segments.shift() - } - } - - // Given the current playhead, determine if we should drop any segments - // If playhead is undefined, it means we're buffering so skip to anything now. - advance(playhead: number | undefined) { - if (this.segments.length < 2) return - - while (this.segments.length > 1) { - const current = this.segments[0] - const next = this.segments[1] - - if (next.dts === undefined || next.timescale == undefined) { - // No samples have been parsed for the next segment yet. - break - } - - if (current.dts === undefined) { - // No samples have been parsed for the current segment yet. - // We can't cover the gap by extending the sample so we have to seek. - // TODO I don't think this can happen, but I guess we have to seek past the gap. - break - } - - if (playhead !== undefined) { - // Check if the next segment has playable media now. - // Otherwise give the current segment more time to catch up. - if (next.dts / next.timescale > playhead) { - return - } - } - - current.skipTo(next.dts || 0) // tell typescript that it's not undefined; we already checked - current.finish() - - // TODO cancel the QUIC stream to save bandwidth - - this.segments.shift() - } - } -} diff --git a/lib/playback/tsconfig.json b/lib/playback/tsconfig.json index 3666e8d..27001c0 100644 --- a/lib/playback/tsconfig.json +++ b/lib/playback/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig.json", "include": ["."], - "exclude": ["./webcodecs/worklet"], + "exclude": ["worklet"], "compilerOptions": { "types": ["dom-mediacapture-transform", "dom-webcodecs"] }, diff --git a/lib/playback/webcodecs/index.ts b/lib/playback/webcodecs/index.ts deleted file mode 100644 index c6d7dc2..0000000 --- a/lib/playback/webcodecs/index.ts +++ /dev/null @@ -1,99 +0,0 @@ -/// - -import * as Message from "./message" -import { Context } from "./context" - -import { Segment, Init } from "../../playback/backend" - -import MediaWorker from "./worker?worker" -import { RingShared } from "../../common/ring" -import { Catalog, isAudioTrack } from "../../media/catalog" - -export interface PlayerConfig { - element: OffscreenCanvas - catalog: Catalog -} - -// 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 Player { - // General worker - #worker: Worker - - // The audio context, which must be created on the main thread. - #context?: Context - - 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.sample_rate !== sampleRate) { - throw new Error(`TODO multiple audio tracks with different sample rates`) - } - - sampleRate = track.sample_rate - channels = Math.max(track.channel_count, 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 / 20), // 50ms - } - - this.#context = new Context(msg.audio) - } - - // TODO only send the canvas if we have a video track - msg.video = { - canvas: config.element, - } - - this.send({ config: msg }, msg.video.canvas) - } - - // TODO initialize context now since the user clicked - play() {} - - init(init: Init) { - this.send({ init }) - } - - segment(segment: Segment) { - this.send({ segment }, segment.stream) - } - - async close() { - this.#worker.terminate() - await this.#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) - } - } -} diff --git a/lib/playback/webcodecs/audio.ts b/lib/playback/worker/audio.ts similarity index 100% rename from lib/playback/webcodecs/audio.ts rename to lib/playback/worker/audio.ts diff --git a/lib/playback/webcodecs/worker.ts b/lib/playback/worker/index.ts similarity index 100% rename from lib/playback/webcodecs/worker.ts rename to lib/playback/worker/index.ts diff --git a/lib/playback/webcodecs/message.ts b/lib/playback/worker/message.ts similarity index 100% rename from lib/playback/webcodecs/message.ts rename to lib/playback/worker/message.ts diff --git a/lib/playback/webcodecs/timeline.ts b/lib/playback/worker/timeline.ts similarity index 100% rename from lib/playback/webcodecs/timeline.ts rename to lib/playback/worker/timeline.ts diff --git a/lib/playback/webcodecs/video.ts b/lib/playback/worker/video.ts similarity index 100% rename from lib/playback/webcodecs/video.ts rename to lib/playback/worker/video.ts diff --git a/lib/playback/webcodecs/worklet/index.ts b/lib/playback/worklet/index.ts similarity index 96% rename from lib/playback/webcodecs/worklet/index.ts rename to lib/playback/worklet/index.ts index 4df61fe..afe485d 100644 --- a/lib/playback/webcodecs/worklet/index.ts +++ b/lib/playback/worklet/index.ts @@ -1,5 +1,5 @@ // TODO add support for @/ to avoid relative imports -import { Ring } from "../../../common/ring" +import { Ring } from "../../common/ring" import * as Message from "./message" class Renderer extends AudioWorkletProcessor { diff --git a/lib/playback/webcodecs/worklet/message.ts b/lib/playback/worklet/message.ts similarity index 72% rename from lib/playback/webcodecs/worklet/message.ts rename to lib/playback/worklet/message.ts index 3695ae8..e55ebdd 100644 --- a/lib/playback/webcodecs/worklet/message.ts +++ b/lib/playback/worklet/message.ts @@ -1,4 +1,4 @@ -import { RingShared } from "../../../common/ring" +import { RingShared } from "../../common/ring" export interface From { config?: Config diff --git a/lib/playback/webcodecs/worklet/tsconfig.json b/lib/playback/worklet/tsconfig.json similarity index 69% rename from lib/playback/webcodecs/worklet/tsconfig.json rename to lib/playback/worklet/tsconfig.json index 9f63999..77c5a71 100644 --- a/lib/playback/webcodecs/worklet/tsconfig.json +++ b/lib/playback/worklet/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "../../tsconfig.json", "include": ["."], "exclude": ["./index"], "compilerOptions": { @@ -8,7 +8,7 @@ }, "references": [ { - "path": "../../../common" + "path": "../../common" } ] } diff --git a/lib/tsconfig.json b/lib/tsconfig.json index d44fb46..2ab7124 100644 --- a/lib/tsconfig.json +++ b/lib/tsconfig.json @@ -24,7 +24,7 @@ "path": "./playback" }, { - "path": "./playback/webcodecs/worklet" + "path": "./playback/worklet" }, { "path": "./contribute" diff --git a/web/src/components/watch.tsx b/web/src/components/watch.tsx index 4b91bdc..4e45d31 100644 --- a/web/src/components/watch.tsx +++ b/web/src/components/watch.tsx @@ -3,7 +3,7 @@ import { Player } from "@kixelated/moq/playback" import Fail from "./fail" -import { createEffect, createMemo, createSelector, createSignal, onCleanup } from "solid-js" +import { createEffect, createSignal, onCleanup } from "solid-js" export default function Watch(props: { name: string }) { // Use query params to allow overriding environment variables. @@ -11,26 +11,9 @@ export default function Watch(props: { name: string }) { const params = Object.fromEntries(urlSearchParams.entries()) const server = params.server ?? import.meta.env.PUBLIC_RELAY_HOST - const defaultMode = "VideoDecoder" in window ? "webcodecs" : "mse" - const [mode, setMode] = createSignal(defaultMode) const [error, setError] = createSignal() - const isMode = createSelector(mode) - // We create a new element each time the mode changes, to avoid SolidJS caching. - const useElement = createMemo(() => { - if (isMode("mse")) { - const video = document.createElement("video") - video.classList.add("w-full", "rounded-lg", "aspect-video") - video.muted = true // so we can autoplay - video.autoplay = true - video.controls = true - return video - } else { - const canvas = document.createElement("canvas") - canvas.classList.add("w-full", "rounded-lg", "aspect-video") - return canvas - } - }) + let canvas!: HTMLCanvasElement const [usePlayer, setPlayer] = createSignal() createEffect(() => { @@ -41,8 +24,7 @@ export default function Watch(props: { name: string }) { // TODO remove this when WebTransport correctly supports self-signed certificates const fingerprint = server.startsWith("localhost") ? `https://${server}/fingerprint` : undefined - const element = useElement() - Player.create({ url, fingerprint, element, namespace }).then(setPlayer).catch(setError) + Player.create({ url, fingerprint, canvas, namespace }).then(setPlayer).catch(setError) }) createEffect(() => { @@ -58,37 +40,7 @@ export default function Watch(props: { name: string }) { return ( <> - {useElement()} - -

Advanced

- - + ) }