diff --git a/packages/core/package.json b/packages/core/package.json index 6f1a65d8b..c4f0dc6e5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -50,6 +50,7 @@ "@videojs/spf": "workspace:*", "@videojs/store": "workspace:*", "@videojs/utils": "workspace:*", + "@moq/watch": "^0.2.2", "hls.js": "^1.6.7" }, "devDependencies": { diff --git a/packages/core/src/dom/media/moq/canvas.ts b/packages/core/src/dom/media/moq/canvas.ts new file mode 100644 index 000000000..695753acd --- /dev/null +++ b/packages/core/src/dom/media/moq/canvas.ts @@ -0,0 +1,335 @@ +import * as Watch from '@moq/watch'; + +const Moq = Watch.Lite; + +const EMPTY_TIME_RANGES = createTimeRanges([]); + +const TEMPLATE = document.createElement('template'); + +TEMPLATE.innerHTML = /*html*/ ` + + +`; + +// Close everything when this element is garbage collected. +// There's no destructor for web components so this is the best we can do. +const cleanup = new FinalizationRegistry((signals) => signals.close()); + +/** + * WebCodecs-backed MoQ media element. + * + * Creates a `` in shadow DOM and uses the MoQ JS API to manage + * connection, broadcast, and decoding. Synthesizes the HTMLMediaElement + * interface from MoQ signals so Video.js can treat it as a media element. + */ +export class MoqCanvas extends (globalThis.HTMLElement ?? class {}) { + static readonly observedAttributes = ['src', 'name', 'muted', 'volume', 'autoplay'] as const; + + // A MoQ connection that is automatically re-established on drop. + #connection = new Moq.Connection.Reload({ + // Immediately start connecting once a URL is set, even if not in the DOM. + enabled: true, + }); + + // The MoQ broadcast being fetched. + #broadcast = new Watch.Broadcast({ + connection: this.#connection.established, + // Start fetching the catalog even if not in the DOM. + enabled: true, + // Default to an empty namespace, so the player can work with just a URL. + name: Moq.Path.empty(), + }); + + // NOTE: We're using the advanced WebCodecs backend to improve tree-shaking. + // ex. A moq-audio element would omit the video stuff. + + // Used to synchronize audio and video playback. + // NOTE: Sync will be pushed into the props next version. + #sync = new Watch.Sync(); + + // Create a source, decoder, and renderer for video. + #videoSource = new Watch.Video.Source(this.#sync, { broadcast: this.#broadcast }); + #videoDecoder = new Watch.Video.Decoder(this.#videoSource); + #videoRenderer: Watch.Video.Renderer; + + // Create a source, decoder, and emitter for audio. + #audioSource = new Watch.Audio.Source(this.#sync, { broadcast: this.#broadcast }); + #audioDecoder = new Watch.Audio.Decoder(this.#audioSource); + #audioEmitter = new Watch.Audio.Emitter(this.#audioDecoder, { paused: false }); + + #canvas: HTMLCanvasElement; + + #signals = new Watch.Signals.Effect(); + + constructor() { + super(); + + // Mark as media element for container discovery. + this.setAttribute('data-media', ''); + + const shadow = this.attachShadow({ mode: 'open' }); + shadow.appendChild(TEMPLATE.content.cloneNode(true)); + + const canvas = shadow.querySelector('canvas'); + if (!canvas) throw new Error('Missing in shadow DOM template'); + this.#canvas = canvas; + this.#videoRenderer = new Watch.Video.Renderer(this.#videoDecoder, { + canvas: this.#canvas, + paused: false, + }); + + cleanup.register(this, this.#signals); + this.#signals.cleanup(() => { + this.#connection.close(); + this.#broadcast.close(); + this.#sync.close(); + this.#videoSource.close(); + this.#videoDecoder.close(); + this.#audioSource.close(); + this.#audioDecoder.close(); + this.#audioEmitter.close(); + this.#videoRenderer.close(); + }); + + this.#signals.subscribe(this.#broadcast.status, (status) => { + if (status === 'live') { + this.dispatchEvent(new Event('loadedmetadata')); + this.dispatchEvent(new Event('loadeddata')); + this.dispatchEvent(new Event('canplay')); + this.dispatchEvent(new Event('canplaythrough')); + } + }); + + this.#signals.subscribe(this.#videoRenderer.paused, (paused) => { + this.dispatchEvent(new Event(paused ? 'pause' : 'play')); + }); + + this.#signals.subscribe(this.#videoDecoder.timestamp, () => { + this.dispatchEvent(new Event('timeupdate')); + }); + + this.#signals.subscribe(this.#videoDecoder.stalled, (stalled) => { + this.dispatchEvent(new Event(stalled ? 'waiting' : 'playing')); + }); + + this.#signals.subscribe(this.#audioEmitter.volume, () => { + this.dispatchEvent(new Event('volumechange')); + }); + + this.#signals.subscribe(this.#audioEmitter.muted, () => { + this.dispatchEvent(new Event('volumechange')); + }); + + this.#signals.subscribe(this.#videoDecoder.buffered, () => { + this.dispatchEvent(new Event('progress')); + }); + } + + connectedCallback(): void { + this.dispatchEvent(new Event('loadstart')); + } + + attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string | null): void { + if (oldValue === newValue) return; + + if (attrName === 'src') { + this.src = newValue ?? ''; + } else if (attrName === 'name') { + this.#broadcast.name.set(newValue ? Moq.Path.from(newValue) : Moq.Path.empty()); + } else if (attrName === 'muted') { + this.#audioEmitter.muted.set(newValue !== null); + } else if (attrName === 'volume') { + this.#audioEmitter.volume.set(newValue ? Number.parseFloat(newValue) : 1); + } else if (attrName === 'autoplay') { + if (newValue !== null) { + this.play(); + } + } + } + + // --- HTMLMediaElement-like interface --- + + get src(): string { + return this.#connection.url.peek()?.toString() ?? ''; + } + + set src(value: string) { + this.#connection.url.set(value ? new URL(value) : undefined); + if (value) this.dispatchEvent(new Event('loadstart')); + } + + get currentSrc(): string { + return this.src; + } + + get paused(): boolean { + return this.#videoRenderer.paused.peek(); + } + + play(): Promise { + this.#videoRenderer.paused.set(false); + this.#audioEmitter.paused.set(false); + return Promise.resolve(); + } + + pause(): void { + this.#videoRenderer.paused.set(true); + this.#audioEmitter.paused.set(true); + } + + load(): void { + // No-op — MoQ manages the connection lifecycle. + } + + get volume(): number { + return this.#audioEmitter.volume.peek(); + } + + set volume(value: number) { + this.#audioEmitter.volume.set(value); + } + + get muted(): boolean { + return this.#audioEmitter.muted.peek(); + } + + set muted(value: boolean) { + this.#audioEmitter.muted.set(value); + } + + get currentTime(): number { + return (this.#videoDecoder.timestamp.peek() ?? 0) / 1000; + } + + set currentTime(_value: number) { + // Live-only — seeking is not supported. + } + + get duration(): number { + return Number.POSITIVE_INFINITY; + } + + get readyState(): number { + const status = this.#broadcast.status.peek(); + if (status === 'live') { + return this.#videoDecoder.stalled.peek() ? 2 : 4; + } + if (status === 'loading') return 1; + return 0; + } + + get networkState(): number { + const status = this.#broadcast.status.peek(); + if (status === 'live' || status === 'loading') return 2; + return 0; + } + + get buffered(): TimeRanges { + const ranges = this.#videoDecoder.buffered.peek(); + if (!ranges.length) return EMPTY_TIME_RANGES; + return createTimeRanges(ranges); + } + + get seekable(): TimeRanges { + return EMPTY_TIME_RANGES; + } + + get played(): TimeRanges { + return EMPTY_TIME_RANGES; + } + + get ended(): boolean { + return false; + } + + get playbackRate(): number { + return 1; + } + + set playbackRate(_value: number) {} + + get defaultPlaybackRate(): number { + return 1; + } + + set defaultPlaybackRate(_value: number) {} + + get defaultMuted(): boolean { + return this.hasAttribute('muted'); + } + + set defaultMuted(value: boolean) { + this.toggleAttribute('muted', value); + } + + get autoplay(): boolean { + return this.hasAttribute('autoplay'); + } + + set autoplay(value: boolean) { + this.toggleAttribute('autoplay', value); + } + + get loop(): boolean { + return false; + } + + set loop(_value: boolean) {} + + get controls(): boolean { + return false; + } + + get preload(): string { + return 'none'; + } + + get error(): MediaError | null { + return null; + } + + get videoWidth(): number { + return this.#canvas.width; + } + + get videoHeight(): number { + return this.#canvas.height; + } + + get poster(): string { + return ''; + } + + set poster(_value: string) {} +} + +function createTimeRanges(ranges: Array<{ start: number; end: number }>): TimeRanges { + return { + get length() { + return ranges.length; + }, + start(index: number) { + const range = ranges.at(index); + if (!range) throw new DOMException('Index out of bounds', 'IndexSizeError'); + return range.start / 1000; + }, + end(index: number) { + const range = ranges.at(index); + if (!range) throw new DOMException('Index out of bounds', 'IndexSizeError'); + return range.end / 1000; + }, + }; +} diff --git a/packages/core/src/dom/media/moq/index.ts b/packages/core/src/dom/media/moq/index.ts new file mode 100644 index 000000000..8b165d00c --- /dev/null +++ b/packages/core/src/dom/media/moq/index.ts @@ -0,0 +1,2 @@ +export { MoqCanvas } from './canvas'; +export { MoqMseCustomMedia, MoqMseDelegateBase, MoqMseMedia } from './mse'; diff --git a/packages/core/src/dom/media/moq/mse.ts b/packages/core/src/dom/media/moq/mse.ts new file mode 100644 index 000000000..d7534af19 --- /dev/null +++ b/packages/core/src/dom/media/moq/mse.ts @@ -0,0 +1,126 @@ +import * as Watch from '@moq/watch'; + +import { type MediaDelegate, MediaDelegateMixin } from '../../../core/media/delegate'; +import { MediaProxyMixin } from '../../../core/media/proxy'; +import { CustomMediaMixin } from '../custom-media-element'; + +const Moq = Watch.Lite; + +const cleanup = new FinalizationRegistry((signals) => signals.close()); + +export class MoqMseDelegateBase implements MediaDelegate { + #connection = new Moq.Connection.Reload({ + enabled: true, + }); + + #broadcast = new Watch.Broadcast({ + connection: this.#connection.established, + enabled: true, + name: Moq.Path.empty(), + }); + + #sync = new Watch.Sync(); + + #muxer = new Watch.Mse.Muxer(this.#sync, { paused: false }); + + #videoSource = new Watch.Video.Source(this.#sync, { broadcast: this.#broadcast }); + #videoMse = new Watch.Video.Mse(this.#muxer, this.#videoSource); + + #audioSource = new Watch.Audio.Source(this.#sync, { broadcast: this.#broadcast }); + #audioMse = new Watch.Audio.Mse(this.#muxer, this.#audioSource); + + #signals = new Watch.Signals.Effect(); + + constructor() { + cleanup.register(this, this.#signals); + this.#signals.cleanup(() => { + this.#connection.close(); + this.#broadcast.close(); + this.#sync.close(); + this.#muxer.close(); + this.#videoSource.close(); + this.#videoMse.close(); + this.#audioSource.close(); + this.#audioMse.close(); + }); + } + + attach(target: EventTarget): void { + this.#muxer.element.set(target as HTMLMediaElement); + } + + detach(): void { + this.#muxer.element.set(undefined); + } + + destroy(): void { + this.#signals.close(); + } + + get src(): string { + return this.#connection.url.peek()?.toString() ?? ''; + } + + set src(value: string) { + this.#connection.url.set(value ? new URL(value) : undefined); + } + + get name(): string { + return this.#broadcast.name.peek()?.toString() ?? ''; + } + + set name(value: string) { + this.#broadcast.name.set(value ? Moq.Path.from(value) : Moq.Path.empty()); + } + + get paused(): boolean { + return this.#muxer.paused.peek(); + } + + get volume(): number { + return this.#audioMse.volume.peek(); + } + + set volume(value: number) { + this.#audioMse.volume.set(value); + } + + get muted(): boolean { + return this.#audioMse.muted.peek(); + } + + set muted(value: boolean) { + this.#audioMse.muted.set(value); + } + + get currentTime(): number { + return this.#videoMse.timestamp.peek() / 1000; + } + + get duration(): number { + return Number.POSITIVE_INFINITY; + } + + play(): Promise { + this.#muxer.paused.set(false); + return Promise.resolve(); + } + + pause(): void { + this.#muxer.paused.set(true); + } +} + +export class MoqMseCustomMedia extends MediaDelegateMixin( + CustomMediaMixin(globalThis.HTMLElement ?? class {}, { tag: 'video' }), + MoqMseDelegateBase +) {} + +export class MoqMseMedia extends MediaDelegateMixin( + MediaProxyMixin( + globalThis.HTMLVideoElement ?? class {}, + globalThis.HTMLMediaElement ?? class {}, + globalThis.EventTarget ?? class {} + ), + MoqMseDelegateBase +) {} diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts index 7d7d5bcd3..fc3604f90 100644 --- a/packages/core/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -11,6 +11,7 @@ const createConfig = (mode: BuildMode): UserConfig => ({ dom: './src/dom/index.ts', 'dom/media/hls/index': './src/dom/media/hls/index.ts', 'dom/media/custom-media-element/index': './src/dom/media/custom-media-element/index.ts', + 'dom/media/moq/index': './src/dom/media/moq/index.ts', 'dom/media/simple-hls/index': './src/dom/media/simple-hls/index.ts', }, platform: 'neutral', diff --git a/packages/html/package.json b/packages/html/package.json index c10409806..57324dcf8 100644 --- a/packages/html/package.json +++ b/packages/html/package.json @@ -89,6 +89,7 @@ "clean": "rm -rf dist cdn types" }, "dependencies": { + "@moq/watch": "^0.2.2", "@videojs/spf": "workspace:*", "@videojs/core": "workspace:*", "@videojs/element": "workspace:*", diff --git a/packages/html/src/define/media/moq-canvas.ts b/packages/html/src/define/media/moq-canvas.ts new file mode 100644 index 000000000..1a236dcef --- /dev/null +++ b/packages/html/src/define/media/moq-canvas.ts @@ -0,0 +1,14 @@ +import { MoqCanvas } from '../../media/moq-canvas'; +import { safeDefine } from '../safe-define'; + +export class MoqCanvasElement extends MoqCanvas { + static readonly tagName = 'moq-canvas'; +} + +safeDefine(MoqCanvasElement); + +declare global { + interface HTMLElementTagNameMap { + [MoqCanvasElement.tagName]: MoqCanvasElement; + } +} diff --git a/packages/html/src/define/media/moq-video.ts b/packages/html/src/define/media/moq-video.ts new file mode 100644 index 000000000..e8424a6f6 --- /dev/null +++ b/packages/html/src/define/media/moq-video.ts @@ -0,0 +1,14 @@ +import { MoqVideo } from '../../media/moq-video'; +import { safeDefine } from '../safe-define'; + +export class MoqVideoElement extends MoqVideo { + static readonly tagName = 'moq-video'; +} + +safeDefine(MoqVideoElement); + +declare global { + interface HTMLElementTagNameMap { + [MoqVideoElement.tagName]: MoqVideoElement; + } +} diff --git a/packages/html/src/media/moq-canvas/index.ts b/packages/html/src/media/moq-canvas/index.ts new file mode 100644 index 000000000..35b9edbb9 --- /dev/null +++ b/packages/html/src/media/moq-canvas/index.ts @@ -0,0 +1 @@ +export { MoqCanvas } from '@videojs/core/dom/media/moq'; diff --git a/packages/html/src/media/moq-video/index.ts b/packages/html/src/media/moq-video/index.ts new file mode 100644 index 000000000..47c4c3c63 --- /dev/null +++ b/packages/html/src/media/moq-video/index.ts @@ -0,0 +1,38 @@ +import { MoqMseCustomMedia } from '@videojs/core/dom/media/moq'; + +export class MoqVideo extends MoqMseCustomMedia { + static getTemplateHTML(attrs: Record): string { + const { src, name, ...rest } = attrs; + // biome-ignore lint/complexity/noThisInStatic: intentional use of super + return super.getTemplateHTML(rest); + } + + static get observedAttributes(): string[] { + return [...MoqMseCustomMedia.observedAttributes, 'name']; + } + + constructor() { + super(); + this.attach(this.target); + } + + attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string | null): void { + if (attrName !== 'src' && attrName !== 'name') { + super.attributeChangedCallback(attrName, oldValue, newValue); + } + + if (attrName === 'src' && oldValue !== newValue) { + this.src = newValue ?? ''; + } else if (attrName === 'name' && oldValue !== newValue) { + this.name = newValue ?? ''; + } + } + + disconnectedCallback(): void { + super.disconnectedCallback?.(); + + if (!this.hasAttribute('keep-alive')) { + this.destroy(); + } + } +} diff --git a/packages/html/src/store/container-mixin.ts b/packages/html/src/store/container-mixin.ts index 6210c205f..e2f8caa0f 100644 --- a/packages/html/src/store/container-mixin.ts +++ b/packages/html/src/store/container-mixin.ts @@ -93,7 +93,8 @@ export function createContainerMixin(context: PlayerC if (!store) return; const media = - this.querySelector('video, audio, [data-media-element]') ?? this.#getSlottedMedia(); + this.querySelector('video, audio, [data-media-element], [data-media]') ?? + this.#getSlottedMedia(); if (!media) { this.#detach(); @@ -121,7 +122,10 @@ export function createContainerMixin(context: PlayerC } function isMediaNode(node: Node): boolean { - return node instanceof HTMLMediaElement || (node instanceof Element && node.hasAttribute('data-media-element')); + return ( + node instanceof HTMLMediaElement || + (node instanceof Element && (node.hasAttribute('data-media-element') || node.hasAttribute('data-media'))) + ); } function hasMediaNode(record: MutationRecord): boolean { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49d0c6f2e..5f7f1ad5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: packages/core: dependencies: + '@moq/watch': + specifier: ^0.2.2 + version: 0.2.3(@svta/cml-utils@1.4.0)(@types/react@19.2.14)(react@19.2.4)(zod@4.3.6) '@videojs/spf': specifier: workspace:* version: link:../spf @@ -118,6 +121,9 @@ importers: packages/html: dependencies: + '@moq/watch': + specifier: ^0.2.2 + version: 0.2.3(@svta/cml-utils@1.4.0)(@types/react@19.2.14)(react@19.2.4)(zod@4.3.6) '@videojs/core': specifier: workspace:* version: link:../core @@ -1753,12 +1759,21 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@kixelated/libavjs-webcodecs-polyfill@0.5.5': + resolution: {integrity: sha512-Q1zgnTMMQ2F7IE9ylx3C1XzVbg5vYN18jiDINO5U3kNPBOHdYuUlJsMhtBoqr1M6ocLtoiqdHmLs7tHFgrw5KA==} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@libav.js/types@6.8.8': + resolution: {integrity: sha512-Lbik/0Q3x2R8cI7mOtRgt+nUWLqGXh7UinMndmpdXSDY4YEjYyVUDsq6fxkuriL78+LCYx8frZIN1r+oDsvYCQ==} + + '@libav.js/variant-opus-af@6.8.8': + resolution: {integrity: sha512-8KBQyA8n5goN7lyctOaPxpcx7dapOgqKh8dWW/NAcl87AgM/WoUGSex3fFc46oCtTHYrUKEm1OmZUrtkt3Q56A==} + '@lit-labs/ssr-dom-shim@1.5.1': resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==} @@ -1779,6 +1794,37 @@ packages: '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@moq/hang@0.2.0': + resolution: {integrity: sha512-uW5hGVWGPY7uFD3w3kt1CNafbB5bge4O5/vZm756SgejihHkmqcFjSUfgabXxIJCXMr/1qtDXOVwIUs8QILZng==} + + '@moq/lite@0.1.5': + resolution: {integrity: sha512-lbMBm7Sm4J7jQ9uWI+ITNGk6kqVNRL1pUEFArXNBUS2OEgHUUosG+DWsuRpiun2OWLiRi5nfx7bmR/3b5hH+ww==} + peerDependencies: + zod: ^4.1.0 + + '@moq/signals@0.1.3': + resolution: {integrity: sha512-tPuLnC6INqydJEore/FJPg+9rdzEb6BsUvr3P7gMErEpdHjdeQbhpn7ramrN2YmOhaACkY9Dk8U3IudzOxSF/A==} + peerDependencies: + '@types/react': ^19.1.8 + react: ^19.0.0 + solid-js: ^1.9.7 + peerDependenciesMeta: + react: + optional: true + solid-js: + optional: true + + '@moq/ui-core@0.1.0': + resolution: {integrity: sha512-DJNBpUNQDyh7Tou324fbJ5/pT08UPghH3OxcVdLEo9IQeX//8NiEzJwcX7iuacR32nzgdiBThIbIpeFa60U3/g==} + peerDependencies: + '@moq/signals': ^0.1.2 + + '@moq/watch@0.2.3': + resolution: {integrity: sha512-niAxQdmsNO7ELYYW6XgsH3/dsdp1PHt6EBPH74sR1/bNBQtMC0soZRZjrUQatD9374+MSFL2+NpV61/m1uv1nA==} + + '@moq/web-transport-ws@0.1.2': + resolution: {integrity: sha512-mYha+AkLNPT3uOGnTA5YWjpxc9LO/yriFSoWzKkR0zN3UMZb9RXbsD8Gbhg1pJZod6QD4tevHoOWTBADYN7yAQ==} + '@mux/mux-node@12.8.1': resolution: {integrity: sha512-ey2eKn7iwVrjRVJfB/yF9HPDVpEl+fZV00ydx0kc9/BMs+wjBi0KgU0HMqUjTgEoyMUTlHmJ3U3BfDvreyg5iQ==} @@ -3043,6 +3089,16 @@ packages: peerDependencies: '@svgr/core': '*' + '@svta/cml-iso-bmff@1.0.1': + resolution: {integrity: sha512-MOhATJYQ6cVrIcoY3nj8p/vGYDpG3wjQIIhBPHNt9yjFijdwFdBNqdZbCXv3aFhRjdx5Saca5TkgNJusKhnI/w==} + engines: {node: '>=20'} + peerDependencies: + '@svta/cml-utils': 1.4.0 + + '@svta/cml-utils@1.4.0': + resolution: {integrity: sha512-vNtHtv/z+9I9ysxFwNrgwxic1oceVPr8TpcpV/NA1l8Gy4phynwtOppkCIBB+PmoyKDcqE4lO85g+lfsuSTBBA==} + engines: {node: '>=20'} + '@tailwindcss/node@4.2.1': resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} @@ -3317,6 +3373,9 @@ packages: resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/global-this@0.4.4': + resolution: {integrity: sha512-mHkm6FvepJECMNthFuIgpAEFmPOk71UyXuIxYfjytvFTnSDBIz7jmViO+LfHI/AjrazWije0PnSP3+/NlwzqtA==} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -3618,6 +3677,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + async-sema@3.1.1: resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} @@ -8891,6 +8953,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@kixelated/libavjs-webcodecs-polyfill@0.5.5': + dependencies: + '@libav.js/types': 6.8.8 + '@ungap/global-this': 0.4.4 + '@kwsites/file-exists@1.1.1': dependencies: debug: 4.4.3 @@ -8899,6 +8966,10 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} + '@libav.js/types@6.8.8': {} + + '@libav.js/variant-opus-af@6.8.8': {} + '@lit-labs/ssr-dom-shim@1.5.1': {} '@lit/context@1.1.6': @@ -8954,6 +9025,57 @@ snapshots: '@mixmark-io/domino@2.2.0': {} + '@moq/hang@0.2.0(@svta/cml-utils@1.4.0)(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@kixelated/libavjs-webcodecs-polyfill': 0.5.5 + '@libav.js/variant-opus-af': 6.8.8 + '@moq/lite': 0.1.5(@types/react@19.2.14)(react@19.2.4)(zod@4.3.6) + '@moq/signals': 0.1.3(@types/react@19.2.14)(react@19.2.4) + '@svta/cml-iso-bmff': 1.0.1(@svta/cml-utils@1.4.0) + zod: 4.3.6 + transitivePeerDependencies: + - '@svta/cml-utils' + - '@types/react' + - react + - solid-js + + '@moq/lite@0.1.5(@types/react@19.2.14)(react@19.2.4)(zod@4.3.6)': + dependencies: + '@moq/signals': 0.1.3(@types/react@19.2.14)(react@19.2.4) + '@moq/web-transport-ws': 0.1.2 + async-mutex: 0.5.0 + zod: 4.3.6 + transitivePeerDependencies: + - '@types/react' + - react + - solid-js + + '@moq/signals@0.1.3(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@types/react': 19.2.14 + dequal: 2.0.3 + optionalDependencies: + react: 19.2.4 + + '@moq/ui-core@0.1.0(@moq/signals@0.1.3(@types/react@19.2.14)(react@19.2.4))': + dependencies: + '@moq/signals': 0.1.3(@types/react@19.2.14)(react@19.2.4) + + '@moq/watch@0.2.3(@svta/cml-utils@1.4.0)(@types/react@19.2.14)(react@19.2.4)(zod@4.3.6)': + dependencies: + '@moq/hang': 0.2.0(@svta/cml-utils@1.4.0)(@types/react@19.2.14)(react@19.2.4) + '@moq/lite': 0.1.5(@types/react@19.2.14)(react@19.2.4)(zod@4.3.6) + '@moq/signals': 0.1.3(@types/react@19.2.14)(react@19.2.4) + '@moq/ui-core': 0.1.0(@moq/signals@0.1.3(@types/react@19.2.14)(react@19.2.4)) + transitivePeerDependencies: + - '@svta/cml-utils' + - '@types/react' + - react + - solid-js + - zod + + '@moq/web-transport-ws@0.1.2': {} + '@mux/mux-node@12.8.1': dependencies: '@types/node': 18.19.130 @@ -10346,6 +10468,12 @@ snapshots: transitivePeerDependencies: - typescript + '@svta/cml-iso-bmff@1.0.1(@svta/cml-utils@1.4.0)': + dependencies: + '@svta/cml-utils': 1.4.0 + + '@svta/cml-utils@1.4.0': {} + '@tailwindcss/node@4.2.1': dependencies: '@jridgewell/remapping': 2.3.5 @@ -10632,6 +10760,8 @@ snapshots: '@typescript-eslint/types': 8.56.1 eslint-visitor-keys: 5.0.1 + '@ungap/global-this@0.4.4': {} + '@ungap/structured-clone@1.3.0': {} '@vercel/nft@0.29.4(rollup@4.59.0)': @@ -11157,6 +11287,10 @@ snapshots: async-function@1.0.0: {} + async-mutex@0.5.0: + dependencies: + tslib: 2.8.1 + async-sema@3.1.1: {} async@3.2.6: {}