diff --git a/CLAUDE.md b/CLAUDE.md index 1d9a36c18..4a0fabb5e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ The project contains multiple layers of protocols: 4. **hang** - Media-specific encoding/decoding on top of `moq-lite`. Contains: - catalog: a JSON track containing a description of other tracks and their properties (for WebCodecs). - container: each frame consists of a timestamp and codec bitstream - - hang-ui: Solid.js Web Components for media playback/publishing UI + - watch/publish: dedicated packages for subscribing/publishing with optional SolidJS UI overlays 5. **application** - Users building on top of `moq-lite` or `hang` Key architectural rule: The CDN/relay does not know anything about media. Anything in the `moq` layer should be generic, using rules on the wire on how to deliver content. @@ -56,8 +56,10 @@ Key architectural rule: The CDN/relay does not know anything about media. Anythi signals/ # Reactive signals library (published as @moq/signals) token/ # JWT token generation (published as @moq/token) clock/ # Clock example (published as @moq/clock) - hang/ # Media layer (published as @moq/hang) - hang-ui/ # Web Components UI (published as @moq/hang-ui) + hang/ # Core media layer: catalog, container, support (published as @moq/hang) + ui-core/ # Shared UI components (published as @moq/ui-core) + watch/ # Watch/subscribe to streams + UI (published as @moq/watch) + publish/ # Publish media to streams + UI (published as @moq/publish) hang-demo/ # Demo applications /doc/ # Documentation site (VitePress, deployed via Cloudflare) @@ -77,7 +79,7 @@ Key architectural rule: The CDN/relay does not know anything about media. Anythi - **Common**: Use `just` for common development tasks - **Rust**: Use `cargo` for Rust-specific operations - **Formatting/Linting**: Biome for JS/TS formatting and linting -- **UI**: Solid.js for Web Components in `hang-ui` +- **UI**: Solid.js for Web Components in `@moq/watch/ui` and `@moq/publish/ui` - **Builds**: Nix flake for reproducible builds (optional) ## Testing Approach diff --git a/README.md b/README.md index ab56f86a8..c0478a667 100644 --- a/README.md +++ b/README.md @@ -127,9 +127,11 @@ This repository provides both [Rust](/rs) and [TypeScript](/js) libraries with s |------------------------------------------|--------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------| | **[@moq/lite](js/lite)** | The core pub/sub transport protocol. Intended for browsers, but can be run server-side with a WebTransport polyfill. | [![npm](https://img.shields.io/npm/v/@moq/lite)](https://www.npmjs.com/package/@moq/lite) | | **[@moq/token](js/token)** | Authentication library & CLI for JS/TS environments (see [Authentication](doc/concept/authentication.md)) | [![npm](https://img.shields.io/npm/v/@moq/token)](https://www.npmjs.com/package/@moq/token) | -| **[@moq/hang](js/hang)** | Media-specific encoding/streaming layered on top of `moq-lite`. Provides both a Javascript API and Web Components. | [![npm](https://img.shields.io/npm/v/@moq/hang)](https://www.npmjs.com/package/@moq/hang) | +| **[@moq/hang](js/hang)** | Core media library: catalog, container, and support. Shared by `@moq/watch` and `@moq/publish`. | [![npm](https://img.shields.io/npm/v/@moq/hang)](https://www.npmjs.com/package/@moq/hang) | | **[@moq/hang-demo](js/hang-demo)** | Examples using `@moq/hang`. | | -| **[@moq/hang-ui](js/hang-ui)**. | UI Components that interact with the Hang Web Components using SolidJS. | [![npm](https://img.shields.io/npm/v/@moq/hang-ui)](https://www.npmjs.com/package/@moq/hang-ui) | +| **[@moq/watch](js/watch)** | Subscribe to and render MoQ broadcasts (Web Component + JS API). | [![npm](https://img.shields.io/npm/v/@moq/watch)](https://www.npmjs.com/package/@moq/watch) | +| **[@moq/publish](js/publish)** | Publish media to MoQ broadcasts (Web Component + JS API). | [![npm](https://img.shields.io/npm/v/@moq/publish)](https://www.npmjs.com/package/@moq/publish) | +| **[@moq/ui-core](js/ui-core)** | Shared UI components (Button, Icon, Stats, CSS theme) used by `@moq/watch/ui` and `@moq/publish/ui`. | [![npm](https://img.shields.io/npm/v/@moq/ui-core)](https://www.npmjs.com/package/@moq/ui-core) | ## Documentation diff --git a/bun.lock b/bun.lock index fc302a543..4d33a699f 100644 --- a/bun.lock +++ b/bun.lock @@ -43,8 +43,6 @@ "@moq/lite": "workspace:^", "@moq/signals": "workspace:^", "@svta/cml-iso-bmff": "^1.0.0-alpha.9", - "async-mutex": "^0.5.0", - "comlink": "^4.4.2", "zod": "^4.1.5", }, "devDependencies": { @@ -60,7 +58,8 @@ "version": "0.1.0", "dependencies": { "@moq/hang": "workspace:^", - "@moq/hang-ui": "workspace:^", + "@moq/publish": "workspace:^", + "@moq/watch": "workspace:^", }, "devDependencies": { "@tailwindcss/typography": "^0.5.16", @@ -72,25 +71,6 @@ "vite-plugin-solid": "^2.11.10", }, }, - "js/hang-ui": { - "name": "@moq/hang-ui", - "version": "0.1.2", - "devDependencies": { - "@types/audioworklet": "^0.0.77", - "@typescript/lib-dom": "npm:@types/web@^0.0.241", - "rimraf": "^6.0.1", - "solid-element": "^1.9.1", - "solid-js": "^1.9.10", - "typescript": "^5.9.2", - "unplugin-solid": "^1.0.0", - "vite": "^7.3.1", - "vite-plugin-solid": "^2.11.10", - }, - "peerDependencies": { - "@moq/hang": "workspace:^0.1.0", - "@moq/signals": "workspace:^0.1.0", - }, - }, "js/lite": { "name": "@moq/lite", "version": "0.1.2", @@ -112,6 +92,26 @@ "zod": "^4.1.0", }, }, + "js/publish": { + "name": "@moq/publish", + "version": "0.1.0", + "dependencies": { + "@moq/hang": "workspace:^", + "@moq/lite": "workspace:^", + "@moq/signals": "workspace:^", + "@moq/ui-core": "workspace:^", + }, + "devDependencies": { + "@types/audioworklet": "^0.0.77", + "@typescript/lib-dom": "npm:@types/web@^0.0.241", + "rimraf": "^6.0.1", + "solid-element": "^1.9.1", + "solid-js": "^1.9.10", + "typescript": "^5.9.2", + "vite": "^7.3.1", + "vite-plugin-solid": "^2.11.10", + }, + }, "js/signals": { "name": "@moq/signals", "version": "0.1.2", @@ -152,6 +152,41 @@ "typescript": "^5.9.2", }, }, + "js/ui-core": { + "name": "@moq/ui-core", + "version": "0.1.0", + "devDependencies": { + "@typescript/lib-dom": "npm:@types/web@^0.0.241", + "rimraf": "^6.0.1", + "solid-js": "^1.9.10", + "typescript": "^5.9.2", + "vite": "^7.3.1", + "vite-plugin-solid": "^2.11.10", + }, + "peerDependencies": { + "@moq/signals": "workspace:^0.1.0", + }, + }, + "js/watch": { + "name": "@moq/watch", + "version": "0.1.0", + "dependencies": { + "@moq/hang": "workspace:^", + "@moq/lite": "workspace:^", + "@moq/signals": "workspace:^", + "@moq/ui-core": "workspace:^", + }, + "devDependencies": { + "@types/audioworklet": "^0.0.77", + "@typescript/lib-dom": "npm:@types/web@^0.0.241", + "rimraf": "^6.0.1", + "solid-element": "^1.9.1", + "solid-js": "^1.9.10", + "typescript": "^5.9.2", + "vite": "^7.3.1", + "vite-plugin-solid": "^2.11.10", + }, + }, }, "trustedDependencies": [ "@fails-components/webtransport-transport-http3-quiche", @@ -403,14 +438,18 @@ "@moq/hang-demo": ["@moq/hang-demo@workspace:js/hang-demo"], - "@moq/hang-ui": ["@moq/hang-ui@workspace:js/hang-ui"], - "@moq/lite": ["@moq/lite@workspace:js/lite"], + "@moq/publish": ["@moq/publish@workspace:js/publish"], + "@moq/signals": ["@moq/signals@workspace:js/signals"], "@moq/token": ["@moq/token@workspace:js/token"], + "@moq/ui-core": ["@moq/ui-core@workspace:js/ui-core"], + + "@moq/watch": ["@moq/watch@workspace:js/watch"], + "@moq/web-transport-ws": ["@moq/web-transport-ws@0.1.2", "", {}, "sha512-mYha+AkLNPT3uOGnTA5YWjpxc9LO/yriFSoWzKkR0zN3UMZb9RXbsD8Gbhg1pJZod6QD4tevHoOWTBADYN7yAQ=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -425,7 +464,7 @@ "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], - "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + "@rollup/pluginutils": ["@rollup/pluginutils@4.2.1", "", { "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" } }, "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="], @@ -733,8 +772,6 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "comlink": ["comlink@4.4.2", "", {}, "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g=="], - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], @@ -1349,10 +1386,6 @@ "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], - - "unplugin-solid": ["unplugin-solid@1.0.0", "", { "dependencies": { "@babel/core": "^7.28.3", "@rollup/pluginutils": "^5.2.0", "babel-preset-solid": "^1.9.9", "merge-anything": "^6.0.6", "solid-refresh": "^0.7.5", "unplugin": "^2.3.10", "vitefu": "^1.1.1" }, "peerDependencies": { "solid-js": "^1.9.9" } }, "sha512-pv1CS3XMtf3WwX8Dq9Bvo4qH6mfjN2xOgbaPcnqW1dLhyP/JQCvueGEsN0dYIZ4JvxaD/G/Ot1JnBzNQGHkfeA=="], - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="], @@ -1379,8 +1412,6 @@ "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="], @@ -1419,10 +1450,16 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@moq/hang-ui/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + "@moq/publish/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "@moq/ui-core/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "@moq/watch/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + "@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], @@ -1487,25 +1524,19 @@ "unenv/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "unplugin-solid/merge-anything": ["merge-anything@6.0.6", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-F3K1W45PvTjRZzbcYIhXntNr8cux00gUxR8IzNPPG+80gNlAHZGVBwFyN4x5yjw/7QkLPKDbRQBK4KrJKo69mw=="], - - "unplugin-solid/solid-refresh": ["solid-refresh@0.7.5", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-ZYMbjWsy7IwSF3+oZCNnReiTYSyCAFRvC7oLUKxxh1wPa6/6YIWqsxa+Ma2kM4F/ypWT69B1c0fmKeZRdLueGw=="], - - "vite-plugin-html/@rollup/pluginutils": ["@rollup/pluginutils@4.2.1", "", { "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" } }, "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ=="], - "vitepress/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], "wrangler/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@moq/hang-ui/vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "@moq/publish/vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], - "module-lookup-amd/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "@moq/ui-core/vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], - "unplugin-solid/merge-anything/is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], + "@moq/watch/vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], - "vite-plugin-html/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "module-lookup-amd/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "vitepress/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], @@ -1561,57 +1592,161 @@ "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], - "@moq/hang-ui/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + "@moq/publish/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "@moq/publish/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "@moq/publish/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "@moq/publish/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "@moq/publish/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "@moq/publish/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "@moq/publish/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "@moq/publish/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "@moq/publish/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "@moq/publish/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "@moq/publish/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "@moq/publish/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "@moq/publish/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "@moq/publish/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "@moq/publish/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "@moq/publish/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "@moq/publish/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "@moq/publish/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "@moq/publish/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "@moq/publish/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "@moq/publish/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "@moq/publish/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "@moq/publish/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "@moq/publish/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "@moq/publish/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "@moq/publish/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + + "@moq/ui-core/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "@moq/ui-core/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "@moq/ui-core/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "@moq/ui-core/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "@moq/ui-core/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "@moq/ui-core/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "@moq/ui-core/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "@moq/ui-core/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "@moq/ui-core/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "@moq/ui-core/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "@moq/ui-core/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "@moq/ui-core/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "@moq/ui-core/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "@moq/ui-core/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "@moq/ui-core/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "@moq/ui-core/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "@moq/ui-core/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "@moq/ui-core/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "@moq/ui-core/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "@moq/ui-core/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "@moq/ui-core/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "@moq/ui-core/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "@moq/ui-core/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "@moq/ui-core/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "@moq/ui-core/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "@moq/ui-core/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + + "@moq/watch/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], - "@moq/hang-ui/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + "@moq/watch/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], - "@moq/hang-ui/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + "@moq/watch/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], - "@moq/hang-ui/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + "@moq/watch/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], - "@moq/hang-ui/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + "@moq/watch/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], - "@moq/hang-ui/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + "@moq/watch/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], - "@moq/hang-ui/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + "@moq/watch/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], - "@moq/hang-ui/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + "@moq/watch/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], - "@moq/hang-ui/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + "@moq/watch/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], - "@moq/hang-ui/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + "@moq/watch/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], - "@moq/hang-ui/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + "@moq/watch/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], - "@moq/hang-ui/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + "@moq/watch/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], - "@moq/hang-ui/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + "@moq/watch/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], - "@moq/hang-ui/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + "@moq/watch/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], - "@moq/hang-ui/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + "@moq/watch/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], - "@moq/hang-ui/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + "@moq/watch/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], - "@moq/hang-ui/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + "@moq/watch/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], - "@moq/hang-ui/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + "@moq/watch/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], - "@moq/hang-ui/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + "@moq/watch/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], - "@moq/hang-ui/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + "@moq/watch/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], - "@moq/hang-ui/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + "@moq/watch/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], - "@moq/hang-ui/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + "@moq/watch/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], - "@moq/hang-ui/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + "@moq/watch/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], - "@moq/hang-ui/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + "@moq/watch/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], - "@moq/hang-ui/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + "@moq/watch/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], - "@moq/hang-ui/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "@moq/watch/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], "module-lookup-amd/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], diff --git a/doc/.vitepress/config.ts b/doc/.vitepress/config.ts index 0004d6a9e..544501341 100644 --- a/doc/.vitepress/config.ts +++ b/doc/.vitepress/config.ts @@ -133,7 +133,9 @@ export default defineConfig({ { text: "Publish", link: "/js/@moq/hang/publish" }, ], }, - { text: "@moq/hang-ui", link: "/js/@moq/hang-ui" }, + { text: "@moq/watch", link: "/js/@moq/watch" }, + { text: "@moq/publish", link: "/js/@moq/publish" }, + { text: "@moq/ui-core", link: "/js/@moq/ui-core" }, { text: "@moq/token", link: "/js/@moq/token" }, { text: "@moq/signals", link: "/js/@moq/signals" }, { text: "@moq/web-transport-ws", link: "/js/@moq/web-transport-ws" }, diff --git a/doc/index.md b/doc/index.md index 930b3c45e..952e7228e 100644 --- a/doc/index.md +++ b/doc/index.md @@ -122,5 +122,6 @@ Or run on [native](/js/env/native) with polyfills via Node/Bun/Deno. Some highlights: - [@moq/lite](/js/@moq/lite) - Performs the core asynchronous networking. - [@moq/hang](/js/@moq/hang/) - Performs any media stuff: capture, encode, transmux, decode, render. -- [@moq/hang-ui](/js/@moq/hang-ui) - A simple web UI for those too lazy to vibe code one. +- [@moq/watch](/js/@moq/watch) - Subscribe to and render MoQ broadcasts. +- [@moq/publish](/js/@moq/publish) - Publish media to MoQ broadcasts. - [...and more](/js/) diff --git a/doc/js/@moq/hang-ui.md b/doc/js/@moq/hang-ui.md deleted file mode 100644 index b1bf7684b..000000000 --- a/doc/js/@moq/hang-ui.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "@moq/hang-ui" -description: Ready-made UI components for MoQ playback and publishing ---- - -# @moq/hang-ui -A library of Web Components for MoQ media playback and publishing. -Drop them into your HTML and get a full-featured player or publisher with zero JavaScript required. - -## Installation -```bash -bun add @moq/hang-ui -# or -npm add @moq/hang-ui -``` - -TODO: Jsdelivr CDN - -## \ - -A video player with controls for watching MoQ streams. - -```html - - - - - -``` - -**Included controls:** -- Play/pause button -- Volume slider -- Latency slider -- Quality selector -- Fullscreen button -- Buffering indicator -- Stats panel - -## \ - -A publishing interface with source selection and controls. - -```html - - - - - -``` - -**Included controls:** -- Source selector (camera, screen, microphone, file) -- Camera picker (if multiple cameras available) -- Microphone picker (if multiple mics available) -- Publishing status indicator -- Stats panel - -## Full Control -If you want full control over the interface, use the underlying `` and `` elements from `@moq/hang` directly. - The `-ui` components just add the control overlay. - -```html - - - - -``` diff --git a/doc/js/@moq/hang/index.md b/doc/js/@moq/hang/index.md index e618a71d8..b2ad0c756 100644 --- a/doc/js/@moq/hang/index.md +++ b/doc/js/@moq/hang/index.md @@ -1,6 +1,6 @@ --- title: "@moq/hang" -description: Media library with Web Components +description: Core media library (catalog, container, support) --- # @moq/hang @@ -8,16 +8,16 @@ description: Media library with Web Components [![npm](https://img.shields.io/npm/v/@moq/hang)](https://www.npmjs.com/package/@moq/hang) [![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/) -High-level media library for real-time streaming using [Media over QUIC](https://moq.dev), built on top of [@moq/lite](/js/@moq/lite). +Core media library for [Media over QUIC](https://moq.dev), built on top of [@moq/lite](/js/@moq/lite). Provides shared primitives used by [`@moq/watch`](/js/@moq/watch) and [`@moq/publish`](/js/@moq/publish). ## Overview `@moq/hang` provides: -- **Web Components** - Easiest way to add MoQ to your page -- **JavaScript API** - Advanced control for custom applications -- **WebCodecs integration** - Hardware-accelerated encoding/decoding -- **Reactive state** - Built on `@moq/signals` +- **Catalog** - JSON track describing other tracks and their codec properties (audio, video, chat, location, etc.) +- **Container** - Media framing in two formats: CMAF (fMP4) and Legacy (varint-timestamp + raw codec bitstream) +- **Support** - Browser capability detection and `` Web Component +- **Utilities** - Hex encoding, Opus audio polyfill (libav), latency computation, browser detection workarounds ## Installation @@ -28,194 +28,50 @@ npm add @moq/hang pnpm add @moq/hang ``` -## Web Components +## Web Component -The fastest way to add MoQ to your web page. See [Web Components](/js/env/web) for full details. +### `` -### Publishing +Display browser support information: ```html - - - - -``` - -[Learn more about publishing](/js/@moq/hang/publish) - -### Watching - -```html - - - - - - + ``` ## JavaScript API -For advanced use cases: - ```typescript import * as Hang from "@moq/hang"; -// Create connection -const connection = new Hang.Connection("https://relay.example.com/anon"); - -// Publishing media -const publish = new Hang.Publish.Broadcast(connection, { - enabled: true, - name: "alice", - video: { enabled: true, device: "camera" }, - audio: { enabled: true }, -}); - -// Subscribing to media -const watch = new Hang.Watch.Broadcast(connection, { - enabled: true, - name: "alice", - video: { enabled: true }, - audio: { enabled: true }, -}); - -// Everything is reactive -publish.name.set("bob"); -watch.volume.set(0.8); -``` - -## Features - -### Real-time Latency - -Uses WebTransport and WebCodecs for sub-second latency: - -```typescript -const watch = new Hang.Watch.Broadcast(connection, { - name: "live-stream", - // Latency optimizations - video: { enabled: true }, -}); -``` - -### Device Selection - -Choose camera or screen: - -```typescript -const publish = new Hang.Publish.Broadcast(connection, { - name: "my-stream", - video: { - enabled: true, - device: "camera", // or "screen" - }, -}); - -// Switch devices -publish.video.device.set("screen"); -``` - -### Quality Control +// Catalog — describes tracks and their codec properties +import * as Catalog from "@moq/hang/catalog"; -Control encoding quality: +// Container — media framing (CMAF and Legacy formats) +import * as Container from "@moq/hang/container"; -```typescript -const publish = new Hang.Publish.Broadcast(connection, { - name: "my-stream", - video: { - enabled: true, - bitrate: 2_500_000, // 2.5 Mbps - framerate: 30, - }, -}); +// CMAF (fMP4) and Legacy (varint-timestamp + raw bitstream) are both available: +// Container.Cmaf — createVideoInitSegment, createAudioInitSegment, encodeDataSegment, decodeDataSegment, etc. +// Container.Legacy — Producer / Consumer classes ``` -### Playback Controls +For watching and publishing, use the dedicated packages: ```typescript -const watch = new Hang.Watch.Broadcast(connection, { - name: "stream", -}); - -// Pause/resume -watch.paused.set(true); -watch.paused.set(false); - -// Volume -watch.muted.set(false); -watch.volume.set(0.8); -``` - -## Reactive State - -Everything uses signals from `@moq/signals`: - -```typescript -import { react } from "@moq/signals/react"; - -const publish = document.querySelector("hang-publish") as HangPublish; - -// Convert to React signal -const videoSource = react(publish.video.media); - -useEffect(() => { - previewVideo.srcObject = videoSource(); -}, [videoSource]); +import * as Watch from "@moq/watch"; +import * as Publish from "@moq/publish"; ``` -## Supported Codecs - -**Video:** -- H.264 (AVC) - Best compatibility -- H.265 (HEVC) - Better compression -- VP8 / VP9 - Open codec -- AV1 - Latest, best compression - -**Audio:** -- Opus - Best for voice/music -- AAC - Good compatibility - -Codec selection is automatic based on browser support. - -## Browser Support - -Requires: -- **WebTransport** - Chrome 97+, Edge 97+ -- **WebCodecs** - Same browsers -- **WebAudio** - All modern browsers - -## Examples - -Check out [hang-demo](https://github.com/moq-dev/moq/tree/main/js/hang-demo) for: - -- Video conferencing -- Screen sharing -- Chat integration -- Quality selection UI - -[View more examples](https://github.com/moq-dev/moq/tree/main/js) - -## Framework Integration - -Works with any framework: +## Related Packages -- **React** - Via `@moq/signals/react` -- **SolidJS** - Via `@moq/signals/solid` or `@moq/hang-ui` -- **Vue** - Via `@moq/signals/vue` -- **Vanilla JS** - Direct Web Components +- **[@moq/watch](/js/@moq/watch)** — Subscribe to and render MoQ broadcasts +- **[@moq/publish](/js/@moq/publish)** — Publish media to MoQ broadcasts +- **[@moq/ui-core](/js/@moq/ui-core)** — Shared UI components +- **[@moq/lite](/js/@moq/lite)** — Core pub/sub transport protocol +- **[@moq/signals](/js/@moq/signals)** — Reactive signals library ## Protocol Specification diff --git a/doc/js/@moq/hang/publish.md b/doc/js/@moq/hang/publish.md index d5195d007..af5d092ef 100644 --- a/doc/js/@moq/hang/publish.md +++ b/doc/js/@moq/hang/publish.md @@ -5,7 +5,7 @@ description: Publish camera, microphone, or screen to MoQ # Publishing Streams -This guide covers how to publish media to MoQ relays using `@moq/hang`. +This guide covers how to publish media to MoQ relays using `@moq/publish`. ## Web Component @@ -13,7 +13,7 @@ The simplest way to publish: ```html (null); @@ -287,20 +287,19 @@ function Publisher({ url, path }) { ## SolidJS Integration -Use `@moq/hang-ui`: +Use `@moq/publish/ui` for the SolidJS UI overlay. The `` element wraps a nested ``: -```tsx -import { HangPublish } from "@moq/hang-ui/publish"; +```html + -function Publisher(props) { - return ( - - ); -} + + + + + ``` ## Authentication diff --git a/doc/js/@moq/hang/watch.md b/doc/js/@moq/hang/watch.md index 8007b62d7..8d8f52324 100644 --- a/doc/js/@moq/hang/watch.md +++ b/doc/js/@moq/hang/watch.md @@ -5,7 +5,7 @@ description: Subscribe to and render MoQ broadcasts # Watching Streams -This guide covers how to subscribe to and render MoQ broadcasts using `@moq/hang`. +This guide covers how to subscribe to and render MoQ broadcasts using `@moq/watch`. ## Web Component @@ -13,7 +13,7 @@ The simplest way to watch a stream: ```html { ```tsx import { useEffect, useRef } from "react"; -import "@moq/hang/watch/element"; -import type { HangWatch } from "@moq/hang"; +import "@moq/watch/element"; +import type HangWatch from "@moq/watch/element"; function VideoPlayer({ url, path }) { const watchRef = useRef(null); @@ -236,26 +236,25 @@ function VideoPlayer({ url, path }) { ## SolidJS Integration -Use `@moq/hang-ui` for native components: +Use `@moq/watch/ui` for the SolidJS UI overlay. The `` element wraps a nested ``: -```tsx -import { HangWatch } from "@moq/hang-ui/watch"; +```html + -function VideoPlayer(props) { - return ( - - ); -} + + + + + ``` Or use Web Components directly: ```tsx -import "@moq/hang/watch/element"; +import "@moq/watch/element"; function VideoPlayer(props) { return ( diff --git a/doc/js/@moq/publish.md b/doc/js/@moq/publish.md new file mode 100644 index 000000000..3684ecbe4 --- /dev/null +++ b/doc/js/@moq/publish.md @@ -0,0 +1,88 @@ +--- +title: "@moq/publish" +description: Publish media to MoQ broadcasts +--- + +# @moq/publish + +[![npm](https://img.shields.io/npm/v/@moq/publish)](https://www.npmjs.com/package/@moq/publish) +[![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/) + +Publish media to MoQ broadcasts. Provides both a JavaScript API and a `` Web Component, plus an optional `` SolidJS overlay. + +## Installation + +```bash +bun add @moq/publish +# or +npm add @moq/publish +``` + +## Web Component + +```html + + + + + +``` + +**Attributes:** +- `url` (required) — Relay server URL +- `path` (required) — Broadcast path/name +- `device` — "camera" or "screen" (default: "camera") +- `audio` — Enable audio capture (boolean) +- `video` — Enable video capture (boolean) +- `controls` — Show publishing controls (boolean) + +## UI Overlay + +Import `@moq/publish/ui` for a SolidJS-powered overlay with device selection and publishing controls: + +```html + + + + + + + +``` + +The `` element automatically discovers the nested `` and wires up reactive controls. + +## JavaScript API + +```typescript +import * as Publish from "@moq/publish"; + +const broadcast = new Publish.Broadcast(connection, { + enabled: true, + name: "alice", + video: { enabled: true, device: "camera" }, + audio: { enabled: true }, +}); + +// Reactive controls +broadcast.video.device.set("screen"); +broadcast.name.set("bob"); +``` + +## Related Packages + +- **[@moq/watch](/js/@moq/watch)** — Subscribe to and render MoQ broadcasts +- **[@moq/hang](/js/@moq/hang/)** — Core media library (catalog, container, support) +- **[@moq/ui-core](/js/@moq/ui-core)** — Shared UI primitives +- **[@moq/lite](/js/@moq/lite)** — Core pub/sub transport protocol diff --git a/doc/js/@moq/signals.md b/doc/js/@moq/signals.md index d2e3a1596..dca25eab0 100644 --- a/doc/js/@moq/signals.md +++ b/doc/js/@moq/signals.md @@ -146,7 +146,7 @@ const vueCount = vue(count); All `@moq/hang` properties are signals: ```typescript -import "@moq/hang/watch/element"; +import "@moq/watch/element"; import { react } from "@moq/signals/react"; const watch = document.querySelector("hang-watch") as HangWatch; diff --git a/doc/js/@moq/ui-core.md b/doc/js/@moq/ui-core.md new file mode 100644 index 000000000..f1c833a9e --- /dev/null +++ b/doc/js/@moq/ui-core.md @@ -0,0 +1,40 @@ +--- +title: "@moq/ui-core" +description: Shared UI primitives for MoQ components +--- + +# @moq/ui-core + +[![npm](https://img.shields.io/npm/v/@moq/ui-core)](https://www.npmjs.com/package/@moq/ui-core) +[![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/) + +Shared UI primitives used by `@moq/watch/ui` and `@moq/publish/ui`. Includes buttons, icons, stats panels, and a CSS theme. + +## Installation + +```bash +bun add @moq/ui-core +# or +npm add @moq/ui-core +``` + +## Components + +- **Button** — Styled button component +- **Icon** — SVG icon library (play, pause, mic, camera, etc.) +- **Stats** — Network statistics panel +- **CSS Theme** — Shared CSS variables and base styles + +## Usage + +This package is primarily consumed internally by `@moq/watch/ui` and `@moq/publish/ui`. You typically don't need to install it directly unless building custom UI on top of MoQ. + +```typescript +import { Button, Icon, Stats } from "@moq/ui-core"; +``` + +## Related Packages + +- **[@moq/watch](/js/@moq/watch)** — Subscribe to and render MoQ broadcasts +- **[@moq/publish](/js/@moq/publish)** — Publish media to MoQ broadcasts +- **[@moq/hang](/js/@moq/hang/)** — Core media library diff --git a/doc/js/@moq/watch.md b/doc/js/@moq/watch.md new file mode 100644 index 000000000..c21573a64 --- /dev/null +++ b/doc/js/@moq/watch.md @@ -0,0 +1,87 @@ +--- +title: "@moq/watch" +description: Subscribe to and render MoQ broadcasts +--- + +# @moq/watch + +[![npm](https://img.shields.io/npm/v/@moq/watch)](https://www.npmjs.com/package/@moq/watch) +[![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/) + +Subscribe to and render MoQ broadcasts. Provides both a JavaScript API and a `` Web Component, plus an optional `` SolidJS overlay. + +## Installation + +```bash +bun add @moq/watch +# or +npm add @moq/watch +``` + +## Web Component + +```html + + + + + +``` + +**Attributes:** +- `url` (required) — Relay server URL +- `path` (required) — Broadcast path/name +- `controls` — Show playback controls (boolean) +- `paused` — Pause playback (boolean) +- `muted` — Mute audio (boolean) +- `volume` — Audio volume (0–1, default: 1) + +## UI Overlay + +Import `@moq/watch/ui` for a SolidJS-powered overlay with buffering indicator, stats panel, and playback controls: + +```html + + + + + + + +``` + +The `` element automatically discovers the nested `` and wires up reactive controls. + +## JavaScript API + +```typescript +import * as Watch from "@moq/watch"; + +const broadcast = new Watch.Broadcast(connection, { + enabled: true, + name: "alice", + video: { enabled: true }, + audio: { enabled: true }, +}); + +// Reactive controls +broadcast.volume.set(0.8); +broadcast.paused.set(false); +``` + +## Related Packages + +- **[@moq/publish](/js/@moq/publish)** — Publish media to MoQ broadcasts +- **[@moq/hang](/js/@moq/hang/)** — Core media library (catalog, container, support) +- **[@moq/ui-core](/js/@moq/ui-core)** — Shared UI primitives +- **[@moq/lite](/js/@moq/lite)** — Core pub/sub transport protocol diff --git a/doc/js/env/web.md b/doc/js/env/web.md index c9cd32654..93e57b987 100644 --- a/doc/js/env/web.md +++ b/doc/js/env/web.md @@ -32,7 +32,7 @@ Publish camera/microphone or screen as a MoQ broadcast. ```html - import "@moq/hang/watch/element"; + import "@moq/watch/element"; ```tsx import { useEffect, useRef } from "react"; -import "@moq/hang/watch/element"; +import "@moq/watch/element"; function VideoPlayer({ url, path }) { const ref = useRef(null); @@ -163,10 +163,10 @@ function VideoPlayer({ url, path }) { ### SolidJS -Use `@moq/hang-ui` for native SolidJS components, or use Web Components directly: +Use `@moq/watch/ui` and `@moq/publish/ui` for SolidJS UI overlays, or use Web Components directly: ```tsx -import "@moq/hang/watch/element"; +import "@moq/watch/element"; function VideoPlayer(props) { return ( @@ -193,7 +193,7 @@ function VideoPlayer(props) { @@ -190,7 +200,7 @@ createEffect(() => { }); ``` -Use `@moq/hang-ui` for ready-made SolidJS components. +Use `@moq/watch/ui` and `@moq/publish/ui` for ready-made SolidJS UI overlays. ## Demo Application diff --git a/flake.nix b/flake.nix index 113c6721d..707124e55 100644 --- a/flake.nix +++ b/flake.nix @@ -50,6 +50,8 @@ rustDeps = with pkgs; [ rust-toolchain just + git + cmake pkg-config glib libressl diff --git a/js/hang-demo/package.json b/js/hang-demo/package.json index da5a01232..3a3ab6c53 100644 --- a/js/hang-demo/package.json +++ b/js/hang-demo/package.json @@ -13,7 +13,8 @@ }, "dependencies": { "@moq/hang": "workspace:^", - "@moq/hang-ui": "workspace:^" + "@moq/watch": "workspace:^", + "@moq/publish": "workspace:^" }, "devDependencies": { "@tailwindcss/typography": "^0.5.16", diff --git a/js/hang-demo/src/index.ts b/js/hang-demo/src/index.ts index 0cb9db563..c3a6390ce 100644 --- a/js/hang-demo/src/index.ts +++ b/js/hang-demo/src/index.ts @@ -1,7 +1,7 @@ import "./highlight"; -import "@moq/hang-ui/watch"; +import "@moq/watch/ui"; import HangSupport from "@moq/hang/support/element"; -import HangWatch from "@moq/hang/watch/element"; +import HangWatch from "@moq/watch/element"; import HangConfig from "./config"; export { HangSupport, HangWatch, HangConfig }; diff --git a/js/hang-demo/src/publish.ts b/js/hang-demo/src/publish.ts index da2665f20..d6f03c551 100644 --- a/js/hang-demo/src/publish.ts +++ b/js/hang-demo/src/publish.ts @@ -1,9 +1,9 @@ import "./highlight"; -import "@moq/hang-ui/publish"; +import "@moq/publish/ui"; -// We need to import Web Components with fully-qualified paths because of tree-shaking. -import HangPublish from "@moq/hang/publish/element"; import HangSupport from "@moq/hang/support/element"; +// We need to import Web Components with fully-qualified paths because of tree-shaking. +import HangPublish from "@moq/publish/element"; export { HangPublish, HangSupport }; diff --git a/js/hang-ui/README.md b/js/hang-ui/README.md deleted file mode 100644 index 776f58ea0..000000000 --- a/js/hang-ui/README.md +++ /dev/null @@ -1,110 +0,0 @@ -

- Media over QUIC -

- -# @moq/hang-ui - -[![npm version](https://img.shields.io/npm/v/@moq/hang-ui)](https://www.npmjs.com/package/@moq/hang-ui) -[![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/) - -A TypeScript library for interacting with @moq/hang Web Components. Provides methods to control playback and publish sources, as well as status of the connection. - -## Installation - -```bash -npm add @moq/hang-ui -# or -pnpm add @moq/hang-ui -yarn add @moq/hang-ui -bun add @moq/hang-ui -``` - -## Web Components - -Currently, there are two Web Components provided by @moq/hang-ui: - -- `` -- `` - -Here's how you can use them (see also @moq/hang-demo for a complete example): - -```html - - - - - -``` - -```html - - - - - -``` - -## Project Structure -The `@moq/hang-ui` package is organized into modular components and utilities: - -```text -src/ -├── publish/ # Publishing UI components -│ ├── components/ # UI controls for publishing -│ ├── hooks/ # Custom Solid hooks for publish UI -│ ├── styles/ # CSS styles for publish UI -│ ├── context.tsx # Context provider for publish state -│ ├── element.tsx # Main publish UI component -│ └── index.tsx # Entry point for publish UI -│ -├── watch/ # Watching/playback UI components -│ ├── components/ # UI controls for watching -│ ├── hooks/ # Custom Solid hooks for watch UI -│ ├── styles/ # CSS styles for watch UI -│ ├── context.tsx # Context provider for watch state -│ ├── element.tsx # Main watch UI component -│ └── index.tsx # Entry point for watch UI -│ -└── shared/ # Shared components and utilities - ├── components/ # Reusable UI components - │ ├── button/ # Button component - │ ├── icon/ # Icon component - │ └── stats/ # Statistics and monitoring components - ├── flex.css # Flexbox utilities - └── variables.css # CSS variables and theme - -``` - -### Module Overview - -#### **publish/** -Contains all UI components related to media publishing. It provides controls for selecting media sources (camera, screen, microphone, file) and managing the publishing state. - -- **MediaSourceSelector**: Allows users to choose their media source -- **PublishControls**: Main control panel for publishing -- **Source buttons**: Individual buttons for camera, screen, microphone, file, and "nothing" sources -- **PublishStatusIndicator**: Displays connection and publishing status - -#### **watch/** -Implements the video player UI with controls for watching live streams. Includes playback controls, quality selection, and buffering indicators. - -- **WatchControls**: Main control panel for the video player -- **PlayPauseButton**: Play/pause toggle -- **VolumeSlider**: Audio volume control -- **LatencySlider**: Adjust playback latency -- **QualitySelector**: Switch between quality levels -- **FullscreenButton**: Toggle fullscreen mode -- **BufferingIndicator**: Visual feedback during buffering -- **StatsButton**: Toggle statistics panel - -#### **shared/** -Common components and utilities used across the package. - -- **Button**: Reusable button component with consistent styling -- **Icon**: Icon wrapper component -- **Stats**: Provides real-time statistics monitoring for both audio and video streams. Uses a provider pattern to collect and display metrics. -- **CSS utilities**: Shared styles, variables, and flexbox utilities diff --git a/js/hang-ui/src/shared/components/stats/types.ts b/js/hang-ui/src/shared/components/stats/types.ts deleted file mode 100644 index 275d1ef79..000000000 --- a/js/hang-ui/src/shared/components/stats/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -export type KnownStatsProviders = "network" | "video" | "audio" | "buffer"; - -import type * as Hang from "@moq/hang"; - -/** - * Context passed to providers for updating display data - */ -export interface ProviderContext { - setDisplayData: (data: string) => void; -} - -/** - * Video resolution dimensions - */ -export interface VideoResolution { - width: number; - height: number; -} - -// TODO Don't re-export these types? -export type Signal = Hang.Moq.Signals.Getter; -export type AudioStats = Hang.Watch.Audio.Stats; -export type AudioSource = Hang.Watch.Audio.Backend; -export type AudioConfig = Hang.Catalog.AudioConfig; -export type VideoStats = Hang.Watch.Video.Stats; - -// TODO use Hang.Watch.Backend instead? -export type ProviderProps = { - audio: Hang.Watch.Audio.Backend; - video: Hang.Watch.Video.Backend; -}; diff --git a/js/hang/README.md b/js/hang/README.md index 61ae82a5c..0980e56df 100644 --- a/js/hang/README.md +++ b/js/hang/README.md @@ -7,19 +7,14 @@ [![npm version](https://img.shields.io/npm/v/@moq/hang)](https://www.npmjs.com/package/@moq/hang) [![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/) -A TypeScript library for real-time media streaming using [Media over QUIC](https://moq.dev/) (MoQ), supported by modern web browsers. - -**`@moq/hang`** provides high-level media components for live audio and video streaming, built on top of [`@moq/lite`](../moq). -It uses new web APIs like WebCodecs, WebTransport, and Web Components. - -> **Note:** This project is a [fork](https://moq.dev/blog/transfork) of the [IETF MoQ specification](https://datatracker.ietf.org/group/moq/documents/), optimized for practical deployment with a narrower focus and exponentially simpler implementation. +Core media library for [Media over QUIC](https://moq.dev/) (MoQ). Provides shared primitives used by [`@moq/watch`](../watch) and [`@moq/publish`](../publish), built on top of [`@moq/lite`](../lite). ## Features -- 🎥 **Real-time latency** via WebTransport and WebCodecs. -- 🎵 **Low-level API** for advanced use cases, such as processing individual frames. -- 🧩 **Web Components** for easy integration. -- 🔄 **Reactive** Easy to use with React and SolidJS adapters. +- **Catalog** — JSON track describing other tracks and their codec properties (audio, video, chat, location, etc.) +- **Container** — Media framing in two formats: CMAF (fMP4) and Legacy (varint-timestamp + raw codec bitstream) +- **Support** — Browser capability detection and `` Web Component +- **Utilities** — Hex encoding, Opus audio polyfill (libav), latency computation, browser detection workarounds ## Installation @@ -31,192 +26,50 @@ yarn add @moq/hang bun add @moq/hang ``` -## Web Components (Easiest) - -The fastest way to add MoQ to your web page. -Check out the [hang-demo](../hang-demo) folder for working examples. - -There's also a Javascript API for more advanced use cases; see below. - -```html - - - - - - - - - - - - - - - - - - - -``` - -### Tree-Shaking -Javascript bundlers often perform dead code elimination. -This can have unfortunate side effects, as it can remove the code that registers these components. - -To attempt to mitigate this, you have to explicitly import components with the `/element` suffix. -Your bundler *should* be smart enough to avoid tree-shaking but you may need to `export` any types just to ensure they are not removed. - -### Attributes -All of the web components support setting HTTML attributes and Javascript properties. -...what's the difference? - -HTML Attributes are strings. -Javacript properties are typed and reactive. - -`` will work, but it's not type-safe. -You can use DOM callbacks to detect when the attribute changes but it's not as convenient. - -Alternatively, you could perform the same thing with Javascript properties: -```tsx -const watch = document.querySelector("hang-watch") as HangWatch; -watch.volume.set(0.8); -``` - -This will actually set the `volume="0.8"` attribute on the element mostly because it's cool and useful when debugging. -But it's also useful because you can use the `.subscribe` method to receive an event on change. - - -### `` - -Subscribes to a hang broadcast and renders it. - -**Attributes:** -- `url` (required): The URL of the server, potentially authenticated via a `?jwt` token. -- `name` (required): The name of the broadcast. -- `controls`: Show simple playback controls. -- `paused`: Pause playback. -- `muted`: Mute audio playback. -- `volume`: Set the audio volume, only when `!muted`. - - -```html - - - - - - - -``` - - -### `` - -Publishes a microphone/camera or screen as a hang broadcast. - -**Attributes:** -- `url` (required): The URL of the server, potentially authenticated via a `?jwt` token. -- `name` (required): The name of the broadcast. -- `device`: "camera" or "screen". -- `audio`: Enable audio capture. -- `video`: Enable video capture -- `controls`: Show simple publishing controls - -```html - - - - - - -``` +## Web Component ### `` -A simple element that displays browser support. +Display browser support information: ```html - - + ``` - -## Javascript API - -**NOTE** This API is still evolving and may change in the future. -You're on your own when it comes to documentation... for now. +## JavaScript API ```typescript import * as Hang from "@moq/hang"; -// Create a new connection, available via `.established` -const connection = new Hang.Connection("https://cdn.moq.dev/anon"); - -// Publishing media, with (optional) initial settings -const publish = new Hang.Publish.Broadcast(connection, { - enabled: true, - name: "bob", - video: { enabled: true, device: "camera" }, -}); - -// Subscribing to media, with (optional) initial settings -const watch = new Hang.Watch.Broadcast(connection, { - enabled: true, - name: "bob", - video: { enabled: true }, -}); - -// Note that virtually everything is reactive, so you can change settings at any time. -publish.name.set("alice"); -watch.audio.enabled.set(true); -``` +// Catalog — describes tracks and their codec properties +import * as Catalog from "@moq/hang/catalog"; -## Browser Compatibility +// Container — media framing (CMAF and Legacy formats) +import * as Container from "@moq/hang/container"; -This library requires modern browser features. -We're currently only testing the most recent versions of Chrome and sometimes Firefox. - -## Framework Integration +// CMAF (fMP4) and Legacy (varint-timestamp + raw bitstream) are both available: +// Container.Cmaf — createVideoInitSegment, createAudioInitSegment, encodeDataSegment, decodeDataSegment, etc. +// Container.Legacy — Producer / Consumer classes +``` -The Reactive API contains helpers to convert into React and SolidJS signals: +For watching and publishing, use the dedicated packages: -```ts -import react from "@moq/signals/react"; -// same for solid +```typescript +import * as Watch from "@moq/watch"; +import * as Publish from "@moq/publish"; +``` -const publish = document.querySelector("hang-publish") as HangPublish; -const media = react(publish.video.media); +## Related Packages -/// Now you have a `react` signal that changes when the video source changes. -useEffect(() => { - video.srcObject = media(); -}, [media]); -``` +- **[@moq/watch](../watch)** — Subscribe to and render MoQ broadcasts +- **[@moq/publish](../publish)** — Publish media to MoQ broadcasts +- **[@moq/ui-core](../ui-core)** — Shared UI components +- **[@moq/lite](../lite)** — Core pub/sub transport protocol +- **[@moq/signals](../signals)** — Reactive signals library ## License diff --git a/js/hang/package.json b/js/hang/package.json index c68765750..884efa6a1 100644 --- a/js/hang/package.json +++ b/js/hang/package.json @@ -7,23 +7,18 @@ "repository": "github:moq-dev/moq", "exports": { ".": "./src/index.ts", - "./publish": "./src/publish/index.ts", - "./publish/element": "./src/publish/element.ts", - "./watch": "./src/watch/index.ts", - "./watch/element": "./src/watch/element.ts", "./catalog": "./src/catalog/index.ts", + "./container": "./src/container/index.ts", + "./util": "./src/util/index.ts", "./support": "./src/support/index.ts", "./support/element": "./src/support/element.ts" }, "sideEffects": [ - "./src/publish/element.ts", - "./src/watch/element.ts", "./src/support/element.ts" ], "scripts": { "build": "rimraf dist && tsc -b && bun ../scripts/package.ts", "check": "tsc --noEmit", - "test": "bun test --only-failures", "release": "bun ../scripts/release.ts" }, "dependencies": { @@ -32,8 +27,6 @@ "@moq/lite": "workspace:^", "@moq/signals": "workspace:^", "@svta/cml-iso-bmff": "^1.0.0-alpha.9", - "async-mutex": "^0.5.0", - "comlink": "^4.4.2", "zod": "^4.1.5" }, "devDependencies": { diff --git a/js/hang/src/index.ts b/js/hang/src/index.ts index fb940ca2b..5144c019f 100644 --- a/js/hang/src/index.ts +++ b/js/hang/src/index.ts @@ -2,6 +2,4 @@ export * as Moq from "@moq/lite"; export * as Signals from "@moq/signals"; export * as Catalog from "./catalog"; export * as Container from "./container"; -export * as Publish from "./publish"; export * as Support from "./support"; -export * as Watch from "./watch"; diff --git a/js/hang/src/util/index.ts b/js/hang/src/util/index.ts new file mode 100644 index 000000000..6ebd215ce --- /dev/null +++ b/js/hang/src/util/index.ts @@ -0,0 +1,4 @@ +export * as Hacks from "./hacks"; +export * as Hex from "./hex"; +export * as Latency from "./latency"; +export * as Libav from "./libav"; diff --git a/js/publish/README.md b/js/publish/README.md new file mode 100644 index 000000000..69f5feb6f --- /dev/null +++ b/js/publish/README.md @@ -0,0 +1,100 @@ +

+ Media over QUIC +

+ +# @moq/publish + +[![npm](https://img.shields.io/npm/v/@moq/publish)](https://www.npmjs.com/package/@moq/publish) +[![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/) + +Publish media to [Media over QUIC](https://moq.dev/) (MoQ) broadcasts, built on top of [@moq/hang](../hang) and [@moq/lite](../lite). + +## Installation + +```bash +bun add @moq/publish +# or +npm add @moq/publish +``` + +## Web Component + +The simplest way to publish a stream: + +```html + + + + + +``` + +### Attributes + +| Attribute | Type | Default | Description | +|------------|---------|----------|---------------------------------| +| `url` | string | required | Relay server URL | +| `path` | string | required | Broadcast path | +| `source` | string | — | `"camera"`, `"screen"`, `"file"` | +| `audio` | boolean | false | Enable audio capture | +| `video` | boolean | false | Enable video capture | +| `controls` | boolean | false | Show simple publishing controls | + +## JavaScript API + +For more control: + +```typescript +import * as Publish from "@moq/publish"; + +const publish = new Publish.Broadcast(connection, { + enabled: true, + name: "alice", + video: { enabled: true }, + audio: { enabled: true }, +}); + +// Change source at runtime +publish.source.camera.enabled.set(true); +``` + +## UI Web Component + +`@moq/publish` includes a SolidJS-powered UI overlay (``) with source selection (camera, screen, file, microphone) and status indicator. It depends on [`@moq/ui-core`](../ui-core) for shared UI primitives. + +```html + + + + + + + +``` + +The `` element automatically discovers the nested `` element and wires up reactive controls. + +## Features + +- **Camera & microphone** — Capture from user devices +- **Screen sharing** — Capture display or window +- **File playback** — Publish from a media file +- **WebCodecs encoding** — Hardware-accelerated video and audio encoding +- **Reactive state** — All properties are signals from `@moq/signals` +- **Chat** — Publish text chat messages +- **Location** — Publish peer position and window tracking + +## License + +Licensed under either: + +- Apache License, Version 2.0 ([LICENSE-APACHE](../../LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](../../LICENSE-MIT) or http://opensource.org/licenses/MIT) diff --git a/js/publish/package.json b/js/publish/package.json new file mode 100644 index 000000000..e0da531ba --- /dev/null +++ b/js/publish/package.json @@ -0,0 +1,38 @@ +{ + "name": "@moq/publish", + "type": "module", + "version": "0.1.0", + "description": "Publish media to Media over QUIC streams", + "license": "(MIT OR Apache-2.0)", + "repository": "github:moq-dev/moq", + "exports": { + ".": "./src/index.ts", + "./element": "./src/element.ts", + "./ui": "./src/ui/index.tsx" + }, + "sideEffects": [ + "./src/element.ts", + "./src/ui/index.tsx" + ], + "scripts": { + "build": "rimraf dist && tsc -b tsconfig.build.json && vite build && bun ../scripts/package.ts", + "check": "tsc --noEmit", + "release": "bun ../scripts/release.ts" + }, + "dependencies": { + "@moq/hang": "workspace:^", + "@moq/lite": "workspace:^", + "@moq/signals": "workspace:^", + "@moq/ui-core": "workspace:^" + }, + "devDependencies": { + "@types/audioworklet": "^0.0.77", + "@typescript/lib-dom": "npm:@types/web@^0.0.241", + "rimraf": "^6.0.1", + "solid-element": "^1.9.1", + "solid-js": "^1.9.10", + "typescript": "^5.9.2", + "vite": "^7.3.1", + "vite-plugin-solid": "^2.11.10" + } +} diff --git a/js/hang/src/publish/audio/capture-worklet.ts b/js/publish/src/audio/capture-worklet.ts similarity index 100% rename from js/hang/src/publish/audio/capture-worklet.ts rename to js/publish/src/audio/capture-worklet.ts diff --git a/js/hang/src/publish/audio/capture.ts b/js/publish/src/audio/capture.ts similarity index 100% rename from js/hang/src/publish/audio/capture.ts rename to js/publish/src/audio/capture.ts diff --git a/js/hang/src/publish/audio/encoder.ts b/js/publish/src/audio/encoder.ts similarity index 97% rename from js/hang/src/publish/audio/encoder.ts rename to js/publish/src/audio/encoder.ts index 706f3a425..6396163c5 100644 --- a/js/hang/src/publish/audio/encoder.ts +++ b/js/publish/src/audio/encoder.ts @@ -1,9 +1,9 @@ +import * as Catalog from "@moq/hang/catalog"; +import * as Container from "@moq/hang/container"; +import * as Util from "@moq/hang/util"; import type * as Moq from "@moq/lite"; import { Time } from "@moq/lite"; import { Effect, type Getter, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; -import * as Container from "../../container"; -import * as libav from "../../util/libav"; import type * as Capture from "./capture"; import type { Source } from "./types"; @@ -158,7 +158,7 @@ export class Encoder { effect.spawn(async () => { // We're using an async polyfill temporarily for Safari support. - await libav.polyfill(); + await Util.Libav.polyfill(); const encoder = new AudioEncoder({ output: (frame) => { diff --git a/js/hang/src/publish/audio/index.ts b/js/publish/src/audio/index.ts similarity index 100% rename from js/hang/src/publish/audio/index.ts rename to js/publish/src/audio/index.ts diff --git a/js/hang/src/publish/audio/types.ts b/js/publish/src/audio/types.ts similarity index 100% rename from js/hang/src/publish/audio/types.ts rename to js/publish/src/audio/types.ts diff --git a/js/hang/src/publish/broadcast.ts b/js/publish/src/broadcast.ts similarity index 98% rename from js/hang/src/publish/broadcast.ts rename to js/publish/src/broadcast.ts index bc0731f32..0d030f5b8 100644 --- a/js/hang/src/publish/broadcast.ts +++ b/js/publish/src/broadcast.ts @@ -1,6 +1,6 @@ +import * as Catalog from "@moq/hang/catalog"; import * as Moq from "@moq/lite"; import { Effect, Signal } from "@moq/signals"; -import * as Catalog from "../catalog"; import * as Audio from "./audio"; import * as Chat from "./chat"; import * as Location from "./location"; diff --git a/js/hang/src/publish/chat/index.ts b/js/publish/src/chat/index.ts similarity index 94% rename from js/hang/src/publish/chat/index.ts rename to js/publish/src/chat/index.ts index 0d97df30e..84adaa0b2 100644 --- a/js/hang/src/publish/chat/index.ts +++ b/js/publish/src/chat/index.ts @@ -1,5 +1,5 @@ +import type * as Catalog from "@moq/hang/catalog"; import { Effect, type Getter, Signal } from "@moq/signals"; -import type * as Catalog from "../../catalog"; import { Message, type MessageProps } from "./message"; import { Typing, type TypingProps } from "./typing"; diff --git a/js/hang/src/publish/chat/message.ts b/js/publish/src/chat/message.ts similarity index 95% rename from js/hang/src/publish/chat/message.ts rename to js/publish/src/chat/message.ts index 91010e63a..0b6640797 100644 --- a/js/hang/src/publish/chat/message.ts +++ b/js/publish/src/chat/message.ts @@ -1,6 +1,6 @@ +import * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import { Effect, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; export type MessageProps = { enabled?: boolean | Signal; diff --git a/js/hang/src/publish/chat/typing.ts b/js/publish/src/chat/typing.ts similarity index 95% rename from js/hang/src/publish/chat/typing.ts rename to js/publish/src/chat/typing.ts index d6de88ef9..f01109a40 100644 --- a/js/hang/src/publish/chat/typing.ts +++ b/js/publish/src/chat/typing.ts @@ -1,6 +1,6 @@ +import * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import { Effect, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; export type TypingProps = { enabled?: boolean | Signal; diff --git a/js/hang/src/publish/element.ts b/js/publish/src/element.ts similarity index 100% rename from js/hang/src/publish/element.ts rename to js/publish/src/element.ts diff --git a/js/hang/src/publish/index.ts b/js/publish/src/index.ts similarity index 82% rename from js/hang/src/publish/index.ts rename to js/publish/src/index.ts index 79ccab233..52ebdbf89 100644 --- a/js/hang/src/publish/index.ts +++ b/js/publish/src/index.ts @@ -8,4 +8,4 @@ export * as User from "./user"; export * as Video from "./video"; // NOTE: element is not exported from this module -// You have to import it from @moq/hang/publish/element instead. +// You have to import it from @moq/publish/element instead. diff --git a/js/hang/src/publish/location/index.ts b/js/publish/src/location/index.ts similarity index 94% rename from js/hang/src/publish/location/index.ts rename to js/publish/src/location/index.ts index 1c4cb3136..d1dfc6338 100644 --- a/js/hang/src/publish/location/index.ts +++ b/js/publish/src/location/index.ts @@ -1,5 +1,5 @@ +import type * as Catalog from "@moq/hang/catalog"; import { Effect, Signal } from "@moq/signals"; -import type { Catalog } from "../.."; import { Peers, type PeersProps } from "./peers"; import { Window, type WindowProps } from "./window"; diff --git a/js/hang/src/publish/location/peers.ts b/js/publish/src/location/peers.ts similarity index 96% rename from js/hang/src/publish/location/peers.ts rename to js/publish/src/location/peers.ts index 2a0222fe0..e5d46bb1f 100644 --- a/js/hang/src/publish/location/peers.ts +++ b/js/publish/src/location/peers.ts @@ -1,7 +1,7 @@ +import * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import * as Zod from "@moq/lite/zod"; import { Effect, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; export interface PeersProps { enabled?: boolean | Signal; diff --git a/js/hang/src/publish/location/window.ts b/js/publish/src/location/window.ts similarity index 97% rename from js/hang/src/publish/location/window.ts rename to js/publish/src/location/window.ts index e7a2d3529..65ba06c08 100644 --- a/js/hang/src/publish/location/window.ts +++ b/js/publish/src/location/window.ts @@ -1,7 +1,7 @@ +import * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import * as Zod from "@moq/lite/zod"; import { Effect, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; export type WindowProps = { // If true, then we'll publish our position to the broadcast. diff --git a/js/hang/src/publish/preview.ts b/js/publish/src/preview.ts similarity index 95% rename from js/hang/src/publish/preview.ts rename to js/publish/src/preview.ts index 375e60994..437172c86 100644 --- a/js/hang/src/publish/preview.ts +++ b/js/publish/src/preview.ts @@ -1,6 +1,6 @@ +import * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import { Effect, Signal } from "@moq/signals"; -import * as Catalog from "../catalog"; export type PreviewProps = { enabled?: boolean | Signal; diff --git a/js/hang/src/publish/source/camera.ts b/js/publish/src/source/camera.ts similarity index 100% rename from js/hang/src/publish/source/camera.ts rename to js/publish/src/source/camera.ts diff --git a/js/hang/src/publish/source/device.ts b/js/publish/src/source/device.ts similarity index 100% rename from js/hang/src/publish/source/device.ts rename to js/publish/src/source/device.ts diff --git a/js/hang/src/publish/source/file.ts b/js/publish/src/source/file.ts similarity index 100% rename from js/hang/src/publish/source/file.ts rename to js/publish/src/source/file.ts diff --git a/js/hang/src/publish/source/index.ts b/js/publish/src/source/index.ts similarity index 100% rename from js/hang/src/publish/source/index.ts rename to js/publish/src/source/index.ts diff --git a/js/hang/src/publish/source/microphone.ts b/js/publish/src/source/microphone.ts similarity index 100% rename from js/hang/src/publish/source/microphone.ts rename to js/publish/src/source/microphone.ts diff --git a/js/hang/src/publish/source/screen.ts b/js/publish/src/source/screen.ts similarity index 100% rename from js/hang/src/publish/source/screen.ts rename to js/publish/src/source/screen.ts diff --git a/js/hang-ui/src/publish/components/CameraSourceButton.tsx b/js/publish/src/ui/components/CameraSourceButton.tsx similarity index 91% rename from js/hang-ui/src/publish/components/CameraSourceButton.tsx rename to js/publish/src/ui/components/CameraSourceButton.tsx index 12d58c3b9..f871804da 100644 --- a/js/hang-ui/src/publish/components/CameraSourceButton.tsx +++ b/js/publish/src/ui/components/CameraSourceButton.tsx @@ -1,6 +1,5 @@ +import { Button, Icon } from "@moq/ui-core"; import { Show } from "solid-js"; -import Button from "../../shared/components/button/button"; -import * as Icon from "../../shared/components/icon/icon"; import usePublishUIContext from "../hooks/use-publish-ui"; import MediaSourceSourceSelector from "./MediaSourceSelector"; diff --git a/js/hang-ui/src/publish/components/FileSourceButton.tsx b/js/publish/src/ui/components/FileSourceButton.tsx similarity index 88% rename from js/hang-ui/src/publish/components/FileSourceButton.tsx rename to js/publish/src/ui/components/FileSourceButton.tsx index c0a5fa0b9..ce3325f1c 100644 --- a/js/hang-ui/src/publish/components/FileSourceButton.tsx +++ b/js/publish/src/ui/components/FileSourceButton.tsx @@ -1,6 +1,5 @@ +import { Button, Icon } from "@moq/ui-core"; import { createSignal } from "solid-js"; -import Button from "../../shared/components/button/button"; -import * as Icon from "../../shared/components/icon/icon"; import usePublishUIContext from "../hooks/use-publish-ui"; export default function FileSourceButton() { diff --git a/js/hang-ui/src/publish/components/MediaSourceSelector.tsx b/js/publish/src/ui/components/MediaSourceSelector.tsx similarity index 91% rename from js/hang-ui/src/publish/components/MediaSourceSelector.tsx rename to js/publish/src/ui/components/MediaSourceSelector.tsx index 8668e1b7c..3cc534dea 100644 --- a/js/hang-ui/src/publish/components/MediaSourceSelector.tsx +++ b/js/publish/src/ui/components/MediaSourceSelector.tsx @@ -1,6 +1,5 @@ +import { Button, Icon } from "@moq/ui-core"; import { createSignal, For, Show } from "solid-js"; -import Button from "../../shared/components/button/button"; -import * as Icon from "../../shared/components/icon/icon"; type MediaSourceSelectorProps = { sources?: MediaDeviceInfo[]; diff --git a/js/hang-ui/src/publish/components/MicrophoneSourceButton.tsx b/js/publish/src/ui/components/MicrophoneSourceButton.tsx similarity index 91% rename from js/hang-ui/src/publish/components/MicrophoneSourceButton.tsx rename to js/publish/src/ui/components/MicrophoneSourceButton.tsx index bed25cffc..981dbddc3 100644 --- a/js/hang-ui/src/publish/components/MicrophoneSourceButton.tsx +++ b/js/publish/src/ui/components/MicrophoneSourceButton.tsx @@ -1,6 +1,5 @@ +import { Button, Icon } from "@moq/ui-core"; import { Show } from "solid-js"; -import Button from "../../shared/components/button/button"; -import * as Icon from "../../shared/components/icon/icon"; import usePublishUIContext from "../hooks/use-publish-ui"; import MediaSourceSourceSelector from "./MediaSourceSelector"; diff --git a/js/hang-ui/src/publish/components/NothingSourceButton.tsx b/js/publish/src/ui/components/NothingSourceButton.tsx similarity index 82% rename from js/hang-ui/src/publish/components/NothingSourceButton.tsx rename to js/publish/src/ui/components/NothingSourceButton.tsx index 19dc6526c..568e2cdfc 100644 --- a/js/hang-ui/src/publish/components/NothingSourceButton.tsx +++ b/js/publish/src/ui/components/NothingSourceButton.tsx @@ -1,5 +1,4 @@ -import Button from "../../shared/components/button/button"; -import * as Icon from "../../shared/components/icon/icon"; +import { Button, Icon } from "@moq/ui-core"; import usePublishUIContext from "../hooks/use-publish-ui"; export default function NothingSourceButton() { diff --git a/js/hang-ui/src/publish/components/PublishControls.tsx b/js/publish/src/ui/components/PublishControls.tsx similarity index 100% rename from js/hang-ui/src/publish/components/PublishControls.tsx rename to js/publish/src/ui/components/PublishControls.tsx diff --git a/js/hang-ui/src/publish/components/PublishStatusIndicator.tsx b/js/publish/src/ui/components/PublishStatusIndicator.tsx similarity index 100% rename from js/hang-ui/src/publish/components/PublishStatusIndicator.tsx rename to js/publish/src/ui/components/PublishStatusIndicator.tsx diff --git a/js/hang-ui/src/publish/components/ScreenSourceButton.tsx b/js/publish/src/ui/components/ScreenSourceButton.tsx similarity index 81% rename from js/hang-ui/src/publish/components/ScreenSourceButton.tsx rename to js/publish/src/ui/components/ScreenSourceButton.tsx index 530fd673e..f0b462b1d 100644 --- a/js/hang-ui/src/publish/components/ScreenSourceButton.tsx +++ b/js/publish/src/ui/components/ScreenSourceButton.tsx @@ -1,5 +1,4 @@ -import Button from "../../shared/components/button/button"; -import * as Icon from "../../shared/components/icon/icon"; +import { Button, Icon } from "@moq/ui-core"; import usePublishUIContext from "../hooks/use-publish-ui"; export default function ScreenSourceButton() { diff --git a/js/hang-ui/src/publish/context.tsx b/js/publish/src/ui/context.tsx similarity index 99% rename from js/hang-ui/src/publish/context.tsx rename to js/publish/src/ui/context.tsx index 472dbc582..ce88aaab0 100644 --- a/js/hang-ui/src/publish/context.tsx +++ b/js/publish/src/ui/context.tsx @@ -1,6 +1,6 @@ -import type HangPublish from "@moq/hang/publish/element"; import type { JSX } from "solid-js"; import { createContext, createEffect, createSignal } from "solid-js"; +import type HangPublish from "../element"; export type PublishStatus = | "no-url" diff --git a/js/hang-ui/src/publish/element.tsx b/js/publish/src/ui/element.tsx similarity index 87% rename from js/hang-ui/src/publish/element.tsx rename to js/publish/src/ui/element.tsx index 91974f173..f21c7ef93 100644 --- a/js/hang-ui/src/publish/element.tsx +++ b/js/publish/src/ui/element.tsx @@ -1,4 +1,4 @@ -import type HangPublish from "@moq/hang/publish/element"; +import type HangPublish from "../element"; import PublishControls from "./components/PublishControls"; import PublishControlsContextProvider from "./context"; import styles from "./styles/index.css?inline"; diff --git a/js/hang-ui/src/publish/hooks/use-publish-ui.ts b/js/publish/src/ui/hooks/use-publish-ui.ts similarity index 100% rename from js/hang-ui/src/publish/hooks/use-publish-ui.ts rename to js/publish/src/ui/hooks/use-publish-ui.ts diff --git a/js/hang-ui/src/publish/index.tsx b/js/publish/src/ui/index.tsx similarity index 92% rename from js/hang-ui/src/publish/index.tsx rename to js/publish/src/ui/index.tsx index 509ab33a8..477e0848d 100644 --- a/js/hang-ui/src/publish/index.tsx +++ b/js/publish/src/ui/index.tsx @@ -1,7 +1,7 @@ -import type HangPublish from "@moq/hang/publish/element"; import { customElement } from "solid-element"; import { createSignal, onMount } from "solid-js"; import { Show } from "solid-js/web"; +import type HangPublish from "../element"; import { PublishUI } from "./element.tsx"; customElement("hang-publish-ui", (_, { element }) => { diff --git a/js/hang-ui/src/publish/styles/controls.css b/js/publish/src/ui/styles/controls.css similarity index 100% rename from js/hang-ui/src/publish/styles/controls.css rename to js/publish/src/ui/styles/controls.css diff --git a/js/hang-ui/src/publish/styles/index.css b/js/publish/src/ui/styles/index.css similarity index 58% rename from js/hang-ui/src/publish/styles/index.css rename to js/publish/src/ui/styles/index.css index 820c6c7a1..179a981a9 100644 --- a/js/hang-ui/src/publish/styles/index.css +++ b/js/publish/src/ui/styles/index.css @@ -1,7 +1,7 @@ /* Shared dependencies */ -@import "../../shared/variables.css"; -@import "../../shared/flex.css"; -@import "../../shared/components/button/button.css"; +@import "@moq/ui-core/variables.css"; +@import "@moq/ui-core/flex.css"; +@import "@moq/ui-core/button/button.css"; /* Component styles */ @import "./controls.css"; diff --git a/js/hang-ui/src/publish/styles/media-selector.css b/js/publish/src/ui/styles/media-selector.css similarity index 100% rename from js/hang-ui/src/publish/styles/media-selector.css rename to js/publish/src/ui/styles/media-selector.css diff --git a/js/hang-ui/src/publish/styles/source-button.css b/js/publish/src/ui/styles/source-button.css similarity index 100% rename from js/hang-ui/src/publish/styles/source-button.css rename to js/publish/src/ui/styles/source-button.css diff --git a/js/hang-ui/src/publish/styles/status-indicator.css b/js/publish/src/ui/styles/status-indicator.css similarity index 100% rename from js/hang-ui/src/publish/styles/status-indicator.css rename to js/publish/src/ui/styles/status-indicator.css diff --git a/js/publish/src/ui/tsconfig.json b/js/publish/src/ui/tsconfig.json new file mode 100644 index 000000000..cd51e8ac3 --- /dev/null +++ b/js/publish/src/ui/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "jsx": "preserve", + "jsxImportSource": "solid-js" + }, + "include": ["."], + "exclude": [] +} diff --git a/js/hang/src/publish/user.ts b/js/publish/src/user.ts similarity index 95% rename from js/hang/src/publish/user.ts rename to js/publish/src/user.ts index 15f46e577..2857a105a 100644 --- a/js/hang/src/publish/user.ts +++ b/js/publish/src/user.ts @@ -1,5 +1,5 @@ +import type * as Catalog from "@moq/hang/catalog"; import { Effect, Signal } from "@moq/signals"; -import type * as Catalog from "../catalog"; export type Props = { enabled?: boolean | Signal; diff --git a/js/hang/src/publish/video/encoder.ts b/js/publish/src/video/encoder.ts similarity index 98% rename from js/hang/src/publish/video/encoder.ts rename to js/publish/src/video/encoder.ts index 5925ef52f..246caee1d 100644 --- a/js/hang/src/publish/video/encoder.ts +++ b/js/publish/src/video/encoder.ts @@ -1,9 +1,9 @@ +import * as Catalog from "@moq/hang/catalog"; +import * as Container from "@moq/hang/container"; +import * as Util from "@moq/hang/util"; import type * as Moq from "@moq/lite"; import { Time } from "@moq/lite"; import { Effect, type Getter, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; -import * as Container from "../../container"; -import { isFirefox } from "../../util/hacks"; import type { Source } from "./types"; export interface EncoderProps { @@ -302,7 +302,7 @@ export class Encoder { // Try hardware encoding first. // We can't reliably detect hardware encoding on Firefox: https://github.com/w3c/webcodecs/issues/896 - if (!isFirefox) { + if (!Util.Hacks.isFirefox) { for (const codec of HARDWARE_CODECS) { if (!codec.startsWith(required)) continue; diff --git a/js/hang/src/publish/video/index.ts b/js/publish/src/video/index.ts similarity index 98% rename from js/hang/src/publish/video/index.ts rename to js/publish/src/video/index.ts index 2f254a786..39b3edf14 100644 --- a/js/hang/src/publish/video/index.ts +++ b/js/publish/src/video/index.ts @@ -1,5 +1,5 @@ +import * as Catalog from "@moq/hang/catalog"; import { Effect, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; import { Encoder, type EncoderProps } from "./encoder"; import { TrackProcessor } from "./polyfill"; import type { Source } from "./types"; diff --git a/js/hang/src/publish/video/polyfill.ts b/js/publish/src/video/polyfill.ts similarity index 100% rename from js/hang/src/publish/video/polyfill.ts rename to js/publish/src/video/polyfill.ts diff --git a/js/hang/src/publish/video/types.ts b/js/publish/src/video/types.ts similarity index 100% rename from js/hang/src/publish/video/types.ts rename to js/publish/src/video/types.ts diff --git a/js/hang-ui/src/vite-env.d.ts b/js/publish/src/vite-env.d.ts similarity index 100% rename from js/hang-ui/src/vite-env.d.ts rename to js/publish/src/vite-env.d.ts diff --git a/js/publish/src/worklet.d.ts b/js/publish/src/worklet.d.ts new file mode 100644 index 000000000..7bb79c31d --- /dev/null +++ b/js/publish/src/worklet.d.ts @@ -0,0 +1,4 @@ +declare module "*-worklet.ts?worker&url" { + const url: string; + export default url; +} diff --git a/js/publish/tsconfig.build.json b/js/publish/tsconfig.build.json new file mode 100644 index 000000000..ca0a3803e --- /dev/null +++ b/js/publish/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": true + }, + "exclude": ["src/ui"] +} diff --git a/js/publish/tsconfig.json b/js/publish/tsconfig.json new file mode 100644 index 000000000..66214c1f4 --- /dev/null +++ b/js/publish/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "dist" + }, + "include": ["src"], + "exclude": ["src/ui"], + "references": [{ "path": "src/ui" }] +} diff --git a/js/publish/vite.config.ts b/js/publish/vite.config.ts new file mode 100644 index 000000000..eea75c27c --- /dev/null +++ b/js/publish/vite.config.ts @@ -0,0 +1,22 @@ +import { resolve } from "path"; +import { defineConfig } from "vite"; +import solidPlugin from "vite-plugin-solid"; + +export default defineConfig({ + plugins: [solidPlugin()], + build: { + lib: { + entry: { + "ui/index": resolve(__dirname, "src/ui/index.tsx"), + }, + formats: ["es"], + }, + rollupOptions: { + external: ["@moq/hang", "@moq/lite", "@moq/signals", "@moq/ui-core"], + }, + outDir: "dist", + emptyOutDir: false, + sourcemap: true, + target: "esnext", + }, +}); diff --git a/js/ui-core/README.md b/js/ui-core/README.md new file mode 100644 index 000000000..52b0fe021 --- /dev/null +++ b/js/ui-core/README.md @@ -0,0 +1,38 @@ +

+ Media over QUIC +

+ +# @moq/ui-core + +[![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/) + +Shared UI components for [Media over QUIC](https://moq.dev/) (MoQ) packages. + +`@moq/ui-core` provides reusable, accessible UI primitives used by `@moq/watch/ui` and `@moq/publish/ui`, built with [SolidJS](https://www.solidjs.com/). + +## Components + +### Button +A styled, accessible button component with hover/active states and disabled support. + +### Icon +SVG icon library including media controls (play, pause, volume, fullscreen, etc.), device indicators (camera, microphone, screen), and stats icons (network, video, audio, buffer). + +### Stats +Real-time statistics panel for monitoring media streaming performance. Displays network, video, audio, and buffer metrics via a provider pattern. + +## CSS + +Shared stylesheets are available as CSS imports: + +- `@moq/ui-core/variables.css` — Theme variables (colors, spacing, border-radius) +- `@moq/ui-core/flex.css` — Flexbox utility classes +- `@moq/ui-core/button/button.css` — Button component styles +- `@moq/ui-core/stats/styles/index.css` — Stats panel styles + +## License + +Licensed under either: + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) diff --git a/js/hang-ui/package.json b/js/ui-core/package.json similarity index 60% rename from js/hang-ui/package.json rename to js/ui-core/package.json index 46fd7cdbf..405c4514f 100644 --- a/js/hang-ui/package.json +++ b/js/ui-core/package.json @@ -1,18 +1,17 @@ { - "name": "@moq/hang-ui", + "name": "@moq/ui-core", "type": "module", - "version": "0.1.2", - "description": "Media over QUIC library UI components", + "version": "0.1.0", + "description": "Shared UI components for Media over QUIC", "license": "(MIT OR Apache-2.0)", "repository": "github:moq-dev/moq", "exports": { - "./publish": "./src/publish/index.tsx", - "./watch": "./src/watch/index.tsx" + ".": "./src/index.ts", + "./variables.css": "./src/variables.css", + "./flex.css": "./src/flex.css", + "./button/button.css": "./src/button/button.css", + "./stats/styles/index.css": "./src/stats/styles/index.css" }, - "sideEffects": [ - "./src/publish/index.tsx", - "./src/watch/index.tsx" - ], "scripts": { "build": "bun run clean && vite build && bun ../scripts/package.ts", "check": "tsc --noEmit", @@ -21,17 +20,13 @@ "release": "bun ../scripts/release.ts" }, "peerDependencies": { - "@moq/hang": "workspace:^0.1.0", "@moq/signals": "workspace:^0.1.0" }, "devDependencies": { - "@types/audioworklet": "^0.0.77", "@typescript/lib-dom": "npm:@types/web@^0.0.241", "rimraf": "^6.0.1", - "solid-element": "^1.9.1", "solid-js": "^1.9.10", "typescript": "^5.9.2", - "unplugin-solid": "^1.0.0", "vite": "^7.3.1", "vite-plugin-solid": "^2.11.10" } diff --git a/js/hang-ui/src/shared/components/button/button.css b/js/ui-core/src/button/button.css similarity index 100% rename from js/hang-ui/src/shared/components/button/button.css rename to js/ui-core/src/button/button.css diff --git a/js/hang-ui/src/shared/components/button/button.tsx b/js/ui-core/src/button/button.tsx similarity index 100% rename from js/hang-ui/src/shared/components/button/button.tsx rename to js/ui-core/src/button/button.tsx diff --git a/js/hang-ui/src/shared/flex.css b/js/ui-core/src/flex.css similarity index 100% rename from js/hang-ui/src/shared/flex.css rename to js/ui-core/src/flex.css diff --git a/js/hang-ui/src/shared/components/icon/arrow-down.svg b/js/ui-core/src/icon/arrow-down.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/arrow-down.svg rename to js/ui-core/src/icon/arrow-down.svg diff --git a/js/hang-ui/src/shared/components/icon/arrow-up.svg b/js/ui-core/src/icon/arrow-up.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/arrow-up.svg rename to js/ui-core/src/icon/arrow-up.svg diff --git a/js/hang-ui/src/shared/components/icon/audio.svg b/js/ui-core/src/icon/audio.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/audio.svg rename to js/ui-core/src/icon/audio.svg diff --git a/js/hang-ui/src/shared/components/icon/ban.svg b/js/ui-core/src/icon/ban.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/ban.svg rename to js/ui-core/src/icon/ban.svg diff --git a/js/hang-ui/src/shared/components/icon/buffer.svg b/js/ui-core/src/icon/buffer.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/buffer.svg rename to js/ui-core/src/icon/buffer.svg diff --git a/js/hang-ui/src/shared/components/icon/camera.svg b/js/ui-core/src/icon/camera.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/camera.svg rename to js/ui-core/src/icon/camera.svg diff --git a/js/hang-ui/src/shared/components/icon/file.svg b/js/ui-core/src/icon/file.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/file.svg rename to js/ui-core/src/icon/file.svg diff --git a/js/hang-ui/src/shared/components/icon/fullscreen-enter.svg b/js/ui-core/src/icon/fullscreen-enter.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/fullscreen-enter.svg rename to js/ui-core/src/icon/fullscreen-enter.svg diff --git a/js/hang-ui/src/shared/components/icon/fullscreen-exit.svg b/js/ui-core/src/icon/fullscreen-exit.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/fullscreen-exit.svg rename to js/ui-core/src/icon/fullscreen-exit.svg diff --git a/js/hang-ui/src/shared/components/icon/icon.tsx b/js/ui-core/src/icon/icon.tsx similarity index 100% rename from js/hang-ui/src/shared/components/icon/icon.tsx rename to js/ui-core/src/icon/icon.tsx diff --git a/js/hang-ui/src/shared/components/icon/microphone.svg b/js/ui-core/src/icon/microphone.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/microphone.svg rename to js/ui-core/src/icon/microphone.svg diff --git a/js/hang-ui/src/shared/components/icon/mute.svg b/js/ui-core/src/icon/mute.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/mute.svg rename to js/ui-core/src/icon/mute.svg diff --git a/js/hang-ui/src/shared/components/icon/network.svg b/js/ui-core/src/icon/network.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/network.svg rename to js/ui-core/src/icon/network.svg diff --git a/js/hang-ui/src/shared/components/icon/pause.svg b/js/ui-core/src/icon/pause.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/pause.svg rename to js/ui-core/src/icon/pause.svg diff --git a/js/hang-ui/src/shared/components/icon/play.svg b/js/ui-core/src/icon/play.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/play.svg rename to js/ui-core/src/icon/play.svg diff --git a/js/hang-ui/src/shared/components/icon/screen.svg b/js/ui-core/src/icon/screen.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/screen.svg rename to js/ui-core/src/icon/screen.svg diff --git a/js/hang-ui/src/shared/components/icon/stats.svg b/js/ui-core/src/icon/stats.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/stats.svg rename to js/ui-core/src/icon/stats.svg diff --git a/js/hang-ui/src/shared/components/icon/video.svg b/js/ui-core/src/icon/video.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/video.svg rename to js/ui-core/src/icon/video.svg diff --git a/js/hang-ui/src/shared/components/icon/volume-high.svg b/js/ui-core/src/icon/volume-high.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/volume-high.svg rename to js/ui-core/src/icon/volume-high.svg diff --git a/js/hang-ui/src/shared/components/icon/volume-low.svg b/js/ui-core/src/icon/volume-low.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/volume-low.svg rename to js/ui-core/src/icon/volume-low.svg diff --git a/js/hang-ui/src/shared/components/icon/volume-medium.svg b/js/ui-core/src/icon/volume-medium.svg similarity index 100% rename from js/hang-ui/src/shared/components/icon/volume-medium.svg rename to js/ui-core/src/icon/volume-medium.svg diff --git a/js/ui-core/src/index.ts b/js/ui-core/src/index.ts new file mode 100644 index 000000000..1407b03f4 --- /dev/null +++ b/js/ui-core/src/index.ts @@ -0,0 +1,4 @@ +export { type ButtonProps, default as Button } from "./button/button"; +export * as Icon from "./icon/icon"; +export { Stats } from "./stats"; +export type { KnownStatsProviders, ProviderContext, ProviderProps } from "./stats/types"; diff --git a/js/hang-ui/src/shared/components/stats/README.md b/js/ui-core/src/stats/README.md similarity index 100% rename from js/hang-ui/src/shared/components/stats/README.md rename to js/ui-core/src/stats/README.md diff --git a/js/hang-ui/src/shared/components/stats/components/StatsItem.tsx b/js/ui-core/src/stats/components/StatsItem.tsx similarity index 100% rename from js/hang-ui/src/shared/components/stats/components/StatsItem.tsx rename to js/ui-core/src/stats/components/StatsItem.tsx diff --git a/js/hang-ui/src/shared/components/stats/components/StatsPanel.tsx b/js/ui-core/src/stats/components/StatsPanel.tsx similarity index 100% rename from js/hang-ui/src/shared/components/stats/components/StatsPanel.tsx rename to js/ui-core/src/stats/components/StatsPanel.tsx diff --git a/js/hang-ui/src/shared/components/stats/index.tsx b/js/ui-core/src/stats/index.tsx similarity index 100% rename from js/hang-ui/src/shared/components/stats/index.tsx rename to js/ui-core/src/stats/index.tsx diff --git a/js/hang-ui/src/shared/components/stats/providers/audio.ts b/js/ui-core/src/stats/providers/audio.ts similarity index 100% rename from js/hang-ui/src/shared/components/stats/providers/audio.ts rename to js/ui-core/src/stats/providers/audio.ts diff --git a/js/hang-ui/src/shared/components/stats/providers/base.ts b/js/ui-core/src/stats/providers/base.ts similarity index 100% rename from js/hang-ui/src/shared/components/stats/providers/base.ts rename to js/ui-core/src/stats/providers/base.ts diff --git a/js/hang-ui/src/shared/components/stats/providers/buffer.ts b/js/ui-core/src/stats/providers/buffer.ts similarity index 100% rename from js/hang-ui/src/shared/components/stats/providers/buffer.ts rename to js/ui-core/src/stats/providers/buffer.ts diff --git a/js/hang-ui/src/shared/components/stats/providers/index.ts b/js/ui-core/src/stats/providers/index.ts similarity index 100% rename from js/hang-ui/src/shared/components/stats/providers/index.ts rename to js/ui-core/src/stats/providers/index.ts diff --git a/js/hang-ui/src/shared/components/stats/providers/network.ts b/js/ui-core/src/stats/providers/network.ts similarity index 100% rename from js/hang-ui/src/shared/components/stats/providers/network.ts rename to js/ui-core/src/stats/providers/network.ts diff --git a/js/hang-ui/src/shared/components/stats/providers/registry.ts b/js/ui-core/src/stats/providers/registry.ts similarity index 100% rename from js/hang-ui/src/shared/components/stats/providers/registry.ts rename to js/ui-core/src/stats/providers/registry.ts diff --git a/js/hang-ui/src/shared/components/stats/providers/video.ts b/js/ui-core/src/stats/providers/video.ts similarity index 100% rename from js/hang-ui/src/shared/components/stats/providers/video.ts rename to js/ui-core/src/stats/providers/video.ts diff --git a/js/hang-ui/src/shared/components/stats/styles/index.css b/js/ui-core/src/stats/styles/index.css similarity index 100% rename from js/hang-ui/src/shared/components/stats/styles/index.css rename to js/ui-core/src/stats/styles/index.css diff --git a/js/ui-core/src/stats/types.ts b/js/ui-core/src/stats/types.ts new file mode 100644 index 000000000..3041475c1 --- /dev/null +++ b/js/ui-core/src/stats/types.ts @@ -0,0 +1,42 @@ +export type KnownStatsProviders = "network" | "video" | "audio" | "buffer"; + +/** + * A value that can be synchronously read via peek(). + * Matches @moq/signals Getter interface structurally. + */ +interface Peekable { + peek(): T; +} + +/** + * Context passed to providers for updating display data + */ +export interface ProviderContext { + setDisplayData: (data: string) => void; +} + +/** + * Structural interface for an audio backend, matching what stats providers need. + */ +export interface AudioBackend { + source: { + track: Peekable; + config: Peekable<{ sampleRate?: number; numberOfChannels?: number; codec?: string } | undefined>; + }; + stats: Peekable<{ bytesReceived: number } | undefined>; +} + +/** + * Structural interface for a video backend, matching what stats providers need. + */ +export interface VideoBackend { + source: { + catalog: Peekable<{ display?: { width: number; height: number } } | undefined>; + }; + stats: Peekable<{ frameCount: number; bytesReceived: number } | undefined>; +} + +export type ProviderProps = { + audio: AudioBackend; + video: VideoBackend; +}; diff --git a/js/hang-ui/src/shared/variables.css b/js/ui-core/src/variables.css similarity index 100% rename from js/hang-ui/src/shared/variables.css rename to js/ui-core/src/variables.css diff --git a/js/ui-core/src/vite-env.d.ts b/js/ui-core/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/js/ui-core/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/js/hang-ui/tsconfig.json b/js/ui-core/tsconfig.json similarity index 100% rename from js/hang-ui/tsconfig.json rename to js/ui-core/tsconfig.json diff --git a/js/hang-ui/vite.config.ts b/js/ui-core/vite.config.ts similarity index 73% rename from js/hang-ui/vite.config.ts rename to js/ui-core/vite.config.ts index 33ab016c9..01a6c46ed 100644 --- a/js/hang-ui/vite.config.ts +++ b/js/ui-core/vite.config.ts @@ -7,8 +7,7 @@ export default defineConfig({ build: { lib: { entry: { - "publish/index": resolve(__dirname, "src/publish/index.tsx"), - "watch/index": resolve(__dirname, "src/watch/index.tsx"), + index: resolve(__dirname, "src/index.ts"), }, formats: ["es"], }, diff --git a/js/watch/README.md b/js/watch/README.md new file mode 100644 index 000000000..dd91c562f --- /dev/null +++ b/js/watch/README.md @@ -0,0 +1,102 @@ +

+ Media over QUIC +

+ +# @moq/watch + +[![npm](https://img.shields.io/npm/v/@moq/watch)](https://www.npmjs.com/package/@moq/watch) +[![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/) + +Subscribe to and render [Media over QUIC](https://moq.dev/) (MoQ) broadcasts, built on top of [@moq/hang](../hang) and [@moq/lite](../lite). + +## Installation + +```bash +bun add @moq/watch +# or +npm add @moq/watch +``` + +## Web Component + +The simplest way to watch a stream: + +```html + + + + + +``` + +### Attributes + +| Attribute | Type | Default | Description | +|-----------|---------|----------|-----------------------| +| `url` | string | required | Relay server URL | +| `path` | string | required | Broadcast path | +| `paused` | boolean | false | Pause playback | +| `muted` | boolean | false | Mute audio | +| `volume` | number | 1 | Audio volume (0-1) | + +## JavaScript API + +For more control: + +```typescript +import * as Watch from "@moq/watch"; + +const watch = new Watch.Broadcast(connection, { + enabled: true, + name: "alice", + video: { enabled: true }, + audio: { enabled: true }, +}); + +// Access the video stream +watch.video.media.subscribe((stream) => { + if (stream) { + videoElement.srcObject = stream; + } +}); +``` + +## UI Web Component + +`@moq/watch` includes a SolidJS-powered UI overlay (``) with playback controls, volume, buffering indicator, quality selector, and stats panel. It depends on [`@moq/ui-core`](../ui-core) for shared UI primitives. + +```html + + + + + + + +``` + +The `` element automatically discovers the nested `` element and wires up reactive controls. + +## Features + +- **WebCodecs decoding** — Hardware-accelerated video and audio decoding +- **MSE fallback** — Media Source Extensions for broader codec support +- **Reactive state** — All properties are signals from `@moq/signals` +- **Chat** — Subscribe to text chat channels +- **Location** — Peer location and window tracking +- **Quality selection** — Switch between available renditions + +## License + +Licensed under either: + +- Apache License, Version 2.0 ([LICENSE-APACHE](../../LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](../../LICENSE-MIT) or http://opensource.org/licenses/MIT) diff --git a/js/watch/package.json b/js/watch/package.json new file mode 100644 index 000000000..f5f174bcb --- /dev/null +++ b/js/watch/package.json @@ -0,0 +1,39 @@ +{ + "name": "@moq/watch", + "type": "module", + "version": "0.1.0", + "description": "Watch/subscribe to Media over QUIC streams", + "license": "(MIT OR Apache-2.0)", + "repository": "github:moq-dev/moq", + "exports": { + ".": "./src/index.ts", + "./element": "./src/element.ts", + "./ui": "./src/ui/index.tsx" + }, + "sideEffects": [ + "./src/element.ts", + "./src/ui/index.tsx" + ], + "scripts": { + "build": "rimraf dist && tsc -b tsconfig.build.json && vite build && bun ../scripts/package.ts", + "check": "tsc --noEmit", + "test": "bun test --only-failures", + "release": "bun ../scripts/release.ts" + }, + "dependencies": { + "@moq/hang": "workspace:^", + "@moq/lite": "workspace:^", + "@moq/signals": "workspace:^", + "@moq/ui-core": "workspace:^" + }, + "devDependencies": { + "@types/audioworklet": "^0.0.77", + "@typescript/lib-dom": "npm:@types/web@^0.0.241", + "rimraf": "^6.0.1", + "solid-element": "^1.9.1", + "solid-js": "^1.9.10", + "typescript": "^5.9.2", + "vite": "^7.3.1", + "vite-plugin-solid": "^2.11.10" + } +} diff --git a/js/hang/src/watch/audio/backend.ts b/js/watch/src/audio/backend.ts similarity index 100% rename from js/hang/src/watch/audio/backend.ts rename to js/watch/src/audio/backend.ts diff --git a/js/hang/src/watch/audio/decoder.ts b/js/watch/src/audio/decoder.ts similarity index 95% rename from js/hang/src/watch/audio/decoder.ts rename to js/watch/src/audio/decoder.ts index 18c9aaab8..d05265115 100644 --- a/js/hang/src/watch/audio/decoder.ts +++ b/js/watch/src/audio/decoder.ts @@ -1,10 +1,9 @@ +import * as Catalog from "@moq/hang/catalog"; +import * as Container from "@moq/hang/container"; +import * as Util from "@moq/hang/util"; import type * as Moq from "@moq/lite"; import { Time } from "@moq/lite"; import { Effect, type Getter, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; -import * as Container from "../../container"; -import * as Hex from "../../util/hex"; -import * as libav from "../../util/libav"; import type { BufferedRanges } from "../backend"; import type * as Render from "./render"; import type { ToMain } from "./render"; @@ -195,7 +194,7 @@ export class Decoder { }); effect.spawn(async () => { - const loaded = await libav.polyfill(); + const loaded = await Util.Libav.polyfill(); if (!loaded) return; // cancelled const decoder = new AudioDecoder({ @@ -204,7 +203,7 @@ export class Decoder { }); effect.cleanup(() => decoder.close()); - const description = config.description ? Hex.toBytes(config.description) : undefined; + const description = config.description ? Util.Hex.toBytes(config.description) : undefined; decoder.configure({ ...config, description, @@ -236,7 +235,7 @@ export class Decoder { if (config.container.kind !== "cmaf") return; // just to help typescript const { timescale } = config.container; - const description = config.description ? Hex.toBytes(config.description) : undefined; + const description = config.description ? Util.Hex.toBytes(config.description) : undefined; // For CMAF, just use decode buffer (no network jitter buffer yet) // TODO: Add CMAF consumer wrapper for latency control @@ -246,7 +245,7 @@ export class Decoder { }); effect.spawn(async () => { - const loaded = await libav.polyfill(); + const loaded = await Util.Libav.polyfill(); if (!loaded) return; // cancelled const decoder = new AudioDecoder({ @@ -377,7 +376,7 @@ export class Decoder { } async function supported(config: Catalog.AudioConfig): Promise { - const description = config.description ? Hex.toBytes(config.description) : undefined; + const description = config.description ? Util.Hex.toBytes(config.description) : undefined; const res = await AudioDecoder.isConfigSupported({ ...config, description, diff --git a/js/hang/src/watch/audio/emitter.ts b/js/watch/src/audio/emitter.ts similarity index 100% rename from js/hang/src/watch/audio/emitter.ts rename to js/watch/src/audio/emitter.ts diff --git a/js/hang/src/watch/audio/index.ts b/js/watch/src/audio/index.ts similarity index 100% rename from js/hang/src/watch/audio/index.ts rename to js/watch/src/audio/index.ts diff --git a/js/hang/src/watch/audio/mse.ts b/js/watch/src/audio/mse.ts similarity index 98% rename from js/hang/src/watch/audio/mse.ts rename to js/watch/src/audio/mse.ts index 96ecc304d..bda62067e 100644 --- a/js/hang/src/watch/audio/mse.ts +++ b/js/watch/src/audio/mse.ts @@ -1,7 +1,7 @@ +import * as Catalog from "@moq/hang/catalog"; +import * as Container from "@moq/hang/container"; import * as Moq from "@moq/lite"; import { Effect, type Getter, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; -import * as Container from "../../container"; import { type BufferedRanges, timeRangesToArray } from "../backend"; import type { Muxer } from "../mse"; import type { Backend, Stats } from "./backend"; diff --git a/js/hang/src/watch/audio/render-worklet.ts b/js/watch/src/audio/render-worklet.ts similarity index 100% rename from js/hang/src/watch/audio/render-worklet.ts rename to js/watch/src/audio/render-worklet.ts diff --git a/js/hang/src/watch/audio/render.ts b/js/watch/src/audio/render.ts similarity index 100% rename from js/hang/src/watch/audio/render.ts rename to js/watch/src/audio/render.ts diff --git a/js/hang/src/watch/audio/ring-buffer.test.ts b/js/watch/src/audio/ring-buffer.test.ts similarity index 100% rename from js/hang/src/watch/audio/ring-buffer.test.ts rename to js/watch/src/audio/ring-buffer.test.ts diff --git a/js/hang/src/watch/audio/ring-buffer.ts b/js/watch/src/audio/ring-buffer.ts similarity index 100% rename from js/hang/src/watch/audio/ring-buffer.ts rename to js/watch/src/audio/ring-buffer.ts diff --git a/js/hang/src/watch/audio/source.ts b/js/watch/src/audio/source.ts similarity index 98% rename from js/hang/src/watch/audio/source.ts rename to js/watch/src/audio/source.ts index 4bebdae42..b52c0776e 100644 --- a/js/hang/src/watch/audio/source.ts +++ b/js/watch/src/audio/source.ts @@ -1,6 +1,6 @@ +import type * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import { Effect, type Getter, Signal } from "@moq/signals"; -import type * as Catalog from "../../catalog"; import type { Broadcast } from "../broadcast"; import type { Sync } from "../sync"; diff --git a/js/hang/src/watch/backend.ts b/js/watch/src/backend.ts similarity index 100% rename from js/hang/src/watch/backend.ts rename to js/watch/src/backend.ts diff --git a/js/hang/src/watch/broadcast.ts b/js/watch/src/broadcast.ts similarity index 98% rename from js/hang/src/watch/broadcast.ts rename to js/watch/src/broadcast.ts index a6b6252c2..e67a4e2ec 100644 --- a/js/hang/src/watch/broadcast.ts +++ b/js/watch/src/broadcast.ts @@ -1,6 +1,6 @@ +import * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import { Effect, type Getter, Signal } from "@moq/signals"; -import * as Catalog from "../catalog"; export interface BroadcastProps { connection?: Moq.Connection.Established | Signal; diff --git a/js/hang/src/watch/chat/index.ts b/js/watch/src/chat/index.ts similarity index 95% rename from js/hang/src/watch/chat/index.ts rename to js/watch/src/chat/index.ts index c27e0dd7b..89686714c 100644 --- a/js/hang/src/watch/chat/index.ts +++ b/js/watch/src/chat/index.ts @@ -1,6 +1,6 @@ +import type * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import { Effect, Signal } from "@moq/signals"; -import type * as Catalog from "../../catalog"; import { Message, type MessageProps } from "./message"; import { Typing, type TypingProps } from "./typing"; diff --git a/js/hang/src/watch/chat/message.ts b/js/watch/src/chat/message.ts similarity index 97% rename from js/hang/src/watch/chat/message.ts rename to js/watch/src/chat/message.ts index 41959c1c3..1916de7f9 100644 --- a/js/hang/src/watch/chat/message.ts +++ b/js/watch/src/chat/message.ts @@ -1,6 +1,6 @@ +import * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import { Effect, type Getter, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; export interface MessageProps { // Whether to start downloading the chat. diff --git a/js/hang/src/watch/chat/typing.ts b/js/watch/src/chat/typing.ts similarity index 97% rename from js/hang/src/watch/chat/typing.ts rename to js/watch/src/chat/typing.ts index f4da07394..53c072baa 100644 --- a/js/hang/src/watch/chat/typing.ts +++ b/js/watch/src/chat/typing.ts @@ -1,6 +1,6 @@ +import * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import { Effect, type Getter, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; export interface TypingProps { // Whether to start downloading the chat. diff --git a/js/hang/src/watch/element.ts b/js/watch/src/element.ts similarity index 100% rename from js/hang/src/watch/element.ts rename to js/watch/src/element.ts diff --git a/js/hang/src/watch/index.ts b/js/watch/src/index.ts similarity index 83% rename from js/hang/src/watch/index.ts rename to js/watch/src/index.ts index 75af00750..ef0b6c63d 100644 --- a/js/hang/src/watch/index.ts +++ b/js/watch/src/index.ts @@ -9,4 +9,4 @@ export * from "./sync"; export * as Video from "./video"; // NOTE: element is not exported from this module -// You have to import it from @moq/hang/watch/element instead. +// You have to import it from @moq/watch/element instead. diff --git a/js/hang/src/watch/location/index.ts b/js/watch/src/location/index.ts similarity index 93% rename from js/hang/src/watch/location/index.ts rename to js/watch/src/location/index.ts index 5fed5b1dc..58988ef47 100644 --- a/js/hang/src/watch/location/index.ts +++ b/js/watch/src/location/index.ts @@ -1,6 +1,6 @@ +import type * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import { Effect, type Signal } from "@moq/signals"; -import type * as Catalog from "../../catalog"; import { Peers, type PeersProps } from "./peers"; import { Window, type WindowProps } from "./window"; diff --git a/js/hang/src/watch/location/peers.ts b/js/watch/src/location/peers.ts similarity index 97% rename from js/hang/src/watch/location/peers.ts rename to js/watch/src/location/peers.ts index f83a44253..33a6a33e6 100644 --- a/js/hang/src/watch/location/peers.ts +++ b/js/watch/src/location/peers.ts @@ -1,7 +1,7 @@ +import * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import * as Zod from "@moq/lite/zod"; import { Effect, type Getter, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; export interface PeersProps { enabled?: boolean | Signal; diff --git a/js/hang/src/watch/location/window.ts b/js/watch/src/location/window.ts similarity index 97% rename from js/hang/src/watch/location/window.ts rename to js/watch/src/location/window.ts index 4b743be64..c23c4b7be 100644 --- a/js/hang/src/watch/location/window.ts +++ b/js/watch/src/location/window.ts @@ -1,7 +1,7 @@ +import * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import * as Zod from "@moq/lite/zod"; import { Effect, type Getter, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; export interface WindowProps { enabled?: boolean | Signal; diff --git a/js/hang/src/watch/mse.ts b/js/watch/src/mse.ts similarity index 100% rename from js/hang/src/watch/mse.ts rename to js/watch/src/mse.ts diff --git a/js/hang/src/watch/preview.ts b/js/watch/src/preview.ts similarity index 96% rename from js/hang/src/watch/preview.ts rename to js/watch/src/preview.ts index 8b451112f..24520d582 100644 --- a/js/hang/src/watch/preview.ts +++ b/js/watch/src/preview.ts @@ -1,7 +1,7 @@ +import * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import * as Zod from "@moq/lite/zod"; import { Effect, Signal } from "@moq/signals"; -import * as Catalog from "../catalog"; export interface PreviewProps { enabled?: boolean | Signal; diff --git a/js/hang/src/watch/sync.ts b/js/watch/src/sync.ts similarity index 100% rename from js/hang/src/watch/sync.ts rename to js/watch/src/sync.ts diff --git a/js/hang-ui/src/watch/components/BufferControl.tsx b/js/watch/src/ui/components/BufferControl.tsx similarity index 99% rename from js/hang-ui/src/watch/components/BufferControl.tsx rename to js/watch/src/ui/components/BufferControl.tsx index 273aa941f..e2fb49773 100644 --- a/js/hang-ui/src/watch/components/BufferControl.tsx +++ b/js/watch/src/ui/components/BufferControl.tsx @@ -1,6 +1,6 @@ import { Moq } from "@moq/hang"; -import type { BufferedRange } from "@moq/hang/watch"; import { createMemo, createSignal, For, onCleanup, Show } from "solid-js"; +import type { BufferedRange } from "../.."; import useWatchUIContext from "../hooks/use-watch-ui"; const MIN_RANGE = 0 as Moq.Time.Milli; diff --git a/js/hang-ui/src/watch/components/BufferingIndicator.tsx b/js/watch/src/ui/components/BufferingIndicator.tsx similarity index 100% rename from js/hang-ui/src/watch/components/BufferingIndicator.tsx rename to js/watch/src/ui/components/BufferingIndicator.tsx diff --git a/js/hang-ui/src/watch/components/FullscreenButton.tsx b/js/watch/src/ui/components/FullscreenButton.tsx similarity index 78% rename from js/hang-ui/src/watch/components/FullscreenButton.tsx rename to js/watch/src/ui/components/FullscreenButton.tsx index c1cab0d63..3d52c2808 100644 --- a/js/hang-ui/src/watch/components/FullscreenButton.tsx +++ b/js/watch/src/ui/components/FullscreenButton.tsx @@ -1,6 +1,5 @@ +import { Button, Icon } from "@moq/ui-core"; import { Show } from "solid-js"; -import Button from "../../shared/components/button/button"; -import * as Icon from "../../shared/components/icon/icon"; import useWatchUIContext from "../hooks/use-watch-ui"; export default function FullscreenButton() { diff --git a/js/hang-ui/src/watch/components/LatencySlider.tsx b/js/watch/src/ui/components/LatencySlider.tsx similarity index 100% rename from js/hang-ui/src/watch/components/LatencySlider.tsx rename to js/watch/src/ui/components/LatencySlider.tsx diff --git a/js/hang-ui/src/watch/components/PlayPauseButton.tsx b/js/watch/src/ui/components/PlayPauseButton.tsx similarity index 79% rename from js/hang-ui/src/watch/components/PlayPauseButton.tsx rename to js/watch/src/ui/components/PlayPauseButton.tsx index 6790eaef1..ff6346547 100644 --- a/js/hang-ui/src/watch/components/PlayPauseButton.tsx +++ b/js/watch/src/ui/components/PlayPauseButton.tsx @@ -1,6 +1,5 @@ +import { Button, Icon } from "@moq/ui-core"; import { Show } from "solid-js"; -import Button from "../../shared/components/button/button"; -import * as Icon from "../../shared/components/icon/icon"; import useWatchUIContext from "../hooks/use-watch-ui"; export default function PlayPauseButton() { diff --git a/js/hang-ui/src/watch/components/QualitySelector.tsx b/js/watch/src/ui/components/QualitySelector.tsx similarity index 100% rename from js/hang-ui/src/watch/components/QualitySelector.tsx rename to js/watch/src/ui/components/QualitySelector.tsx diff --git a/js/hang-ui/src/watch/components/StatsButton.tsx b/js/watch/src/ui/components/StatsButton.tsx similarity index 78% rename from js/hang-ui/src/watch/components/StatsButton.tsx rename to js/watch/src/ui/components/StatsButton.tsx index ba6a94015..137fbbfda 100644 --- a/js/hang-ui/src/watch/components/StatsButton.tsx +++ b/js/watch/src/ui/components/StatsButton.tsx @@ -1,5 +1,4 @@ -import Button from "../../shared/components/button/button"; -import * as Icon from "../../shared/components/icon/icon"; +import { Button, Icon } from "@moq/ui-core"; import useWatchUIContext from "../hooks/use-watch-ui"; /** diff --git a/js/hang-ui/src/watch/components/VolumeSlider.tsx b/js/watch/src/ui/components/VolumeSlider.tsx similarity index 91% rename from js/hang-ui/src/watch/components/VolumeSlider.tsx rename to js/watch/src/ui/components/VolumeSlider.tsx index 957acac4b..6bc151d82 100644 --- a/js/hang-ui/src/watch/components/VolumeSlider.tsx +++ b/js/watch/src/ui/components/VolumeSlider.tsx @@ -1,6 +1,5 @@ +import { Button, Icon } from "@moq/ui-core"; import { createEffect, createSignal } from "solid-js"; -import Button from "../../shared/components/button/button"; -import * as Icon from "../../shared/components/icon/icon"; import useWatchUIContext from "../hooks/use-watch-ui"; const getVolumeIcon = (volume: number, isMuted: boolean) => { diff --git a/js/hang-ui/src/watch/components/WatchControls.tsx b/js/watch/src/ui/components/WatchControls.tsx similarity index 100% rename from js/hang-ui/src/watch/components/WatchControls.tsx rename to js/watch/src/ui/components/WatchControls.tsx diff --git a/js/hang-ui/src/watch/components/WatchStatusIndicator.tsx b/js/watch/src/ui/components/WatchStatusIndicator.tsx similarity index 100% rename from js/hang-ui/src/watch/components/WatchStatusIndicator.tsx rename to js/watch/src/ui/components/WatchStatusIndicator.tsx diff --git a/js/hang-ui/src/watch/context.tsx b/js/watch/src/ui/context.tsx similarity index 98% rename from js/hang-ui/src/watch/context.tsx rename to js/watch/src/ui/context.tsx index 76fe7a43f..2c96101b9 100644 --- a/js/hang-ui/src/watch/context.tsx +++ b/js/watch/src/ui/context.tsx @@ -1,9 +1,9 @@ import { type Moq, Signals } from "@moq/hang"; -import type { BufferedRanges } from "@moq/hang/watch"; -import type HangWatch from "@moq/hang/watch/element"; import solid from "@moq/signals/solid"; import type { JSX } from "solid-js"; import { createContext, createSignal, onCleanup } from "solid-js"; +import type { BufferedRanges } from ".."; +import type HangWatch from "../element"; type WatchUIContextProviderProps = { hangWatch: HangWatch; diff --git a/js/hang-ui/src/watch/element.tsx b/js/watch/src/ui/element.tsx similarity index 89% rename from js/hang-ui/src/watch/element.tsx rename to js/watch/src/ui/element.tsx index 30f76a94a..3a2ecd408 100644 --- a/js/hang-ui/src/watch/element.tsx +++ b/js/watch/src/ui/element.tsx @@ -1,7 +1,7 @@ -import type HangWatch from "@moq/hang/watch/element"; +import { Stats } from "@moq/ui-core"; import { useContext } from "solid-js"; import { Show } from "solid-js/web"; -import { Stats } from "../shared/components/stats"; +import type HangWatch from "../element"; import BufferingIndicator from "./components/BufferingIndicator"; import WatchControls from "./components/WatchControls"; diff --git a/js/hang-ui/src/watch/hooks/use-watch-ui.ts b/js/watch/src/ui/hooks/use-watch-ui.ts similarity index 100% rename from js/hang-ui/src/watch/hooks/use-watch-ui.ts rename to js/watch/src/ui/hooks/use-watch-ui.ts diff --git a/js/hang-ui/src/watch/index.tsx b/js/watch/src/ui/index.tsx similarity index 92% rename from js/hang-ui/src/watch/index.tsx rename to js/watch/src/ui/index.tsx index c33937e00..23a5f1a89 100644 --- a/js/hang-ui/src/watch/index.tsx +++ b/js/watch/src/ui/index.tsx @@ -1,6 +1,6 @@ -import type HangWatch from "@moq/hang/watch/element"; import { customElement } from "solid-element"; import { createSignal, onMount, Show } from "solid-js"; +import type HangWatch from "../element"; import { WatchUI } from "./element.tsx"; customElement("hang-watch-ui", (_, { element }) => { diff --git a/js/hang-ui/src/watch/styles/index.css b/js/watch/src/ui/styles/index.css similarity index 96% rename from js/hang-ui/src/watch/styles/index.css rename to js/watch/src/ui/styles/index.css index 995c63553..5112d3e88 100644 --- a/js/hang-ui/src/watch/styles/index.css +++ b/js/watch/src/ui/styles/index.css @@ -1,7 +1,7 @@ -@import "../../shared/variables.css"; -@import "../../shared/flex.css"; -@import "../../shared/components/button/button.css"; -@import "../../shared/components/stats/styles/index.css"; +@import "@moq/ui-core/variables.css"; +@import "@moq/ui-core/flex.css"; +@import "@moq/ui-core/button/button.css"; +@import "@moq/ui-core/stats/styles/index.css"; /* Color variables for buffer states */ :root { diff --git a/js/watch/src/ui/tsconfig.json b/js/watch/src/ui/tsconfig.json new file mode 100644 index 000000000..cd51e8ac3 --- /dev/null +++ b/js/watch/src/ui/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "jsx": "preserve", + "jsxImportSource": "solid-js" + }, + "include": ["."], + "exclude": [] +} diff --git a/js/hang/src/watch/user.ts b/js/watch/src/user.ts similarity index 95% rename from js/hang/src/watch/user.ts rename to js/watch/src/user.ts index f3f638bd5..189115b33 100644 --- a/js/hang/src/watch/user.ts +++ b/js/watch/src/user.ts @@ -1,5 +1,5 @@ +import type * as Catalog from "@moq/hang/catalog"; import { Effect, type Getter, Signal } from "@moq/signals"; -import type * as Catalog from "../catalog"; export interface Props { enabled?: boolean | Signal; diff --git a/js/hang/src/watch/video/backend.ts b/js/watch/src/video/backend.ts similarity index 100% rename from js/hang/src/watch/video/backend.ts rename to js/watch/src/video/backend.ts diff --git a/js/hang/src/watch/video/decoder.ts b/js/watch/src/video/decoder.ts similarity index 96% rename from js/hang/src/watch/video/decoder.ts rename to js/watch/src/video/decoder.ts index e9cdc5400..d6d28dbe7 100644 --- a/js/hang/src/watch/video/decoder.ts +++ b/js/watch/src/video/decoder.ts @@ -1,9 +1,9 @@ +import * as Catalog from "@moq/hang/catalog"; +import * as Container from "@moq/hang/container"; +import * as Util from "@moq/hang/util"; import type * as Moq from "@moq/lite"; import { Time } from "@moq/lite"; import { Effect, type Getter, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; -import * as Container from "../../container"; -import * as Hex from "../../util/hex"; import type { BufferedRanges } from "../backend"; import type { Backend, Stats } from "./backend"; import type { Source } from "./source"; @@ -276,7 +276,7 @@ class DecoderTrack { decoder.configure({ ...this.config, - description: this.config.description ? Hex.toBytes(this.config.description) : undefined, + description: this.config.description ? Util.Hex.toBytes(this.config.description) : undefined, optimizeForLatency: this.config.optimizeForLatency ?? true, // @ts-expect-error Only supported by Chrome, so the renderer has to flip manually. flip: false, @@ -336,7 +336,7 @@ class DecoderTrack { if (this.config.container.kind !== "cmaf") return; const { timescale } = this.config.container; - const description = this.config.description ? Hex.toBytes(this.config.description) : undefined; + const description = this.config.description ? Util.Hex.toBytes(this.config.description) : undefined; // Configure decoder with description from catalog decoder.configure({ @@ -469,7 +469,7 @@ function mergeBufferedRanges(a: BufferedRanges, b: BufferedRanges): BufferedRang } async function supported(config: Catalog.VideoConfig): Promise { - const description = config.description ? Hex.toBytes(config.description) : undefined; + const description = config.description ? Util.Hex.toBytes(config.description) : undefined; const { supported } = await VideoDecoder.isConfigSupported({ codec: config.codec, description, diff --git a/js/hang/src/watch/video/index.ts b/js/watch/src/video/index.ts similarity index 100% rename from js/hang/src/watch/video/index.ts rename to js/watch/src/video/index.ts diff --git a/js/hang/src/watch/video/mse.ts b/js/watch/src/video/mse.ts similarity index 98% rename from js/hang/src/watch/video/mse.ts rename to js/watch/src/video/mse.ts index 1e8c8fd74..af04be702 100644 --- a/js/hang/src/watch/video/mse.ts +++ b/js/watch/src/video/mse.ts @@ -1,7 +1,7 @@ +import * as Catalog from "@moq/hang/catalog"; +import * as Container from "@moq/hang/container"; import * as Moq from "@moq/lite"; import { Effect, type Getter, Signal } from "@moq/signals"; -import * as Catalog from "../../catalog"; -import * as Container from "../../container"; import { type BufferedRanges, timeRangesToArray } from "../backend"; import type { Muxer } from "../mse"; import type { Backend, Stats } from "./backend"; diff --git a/js/hang/src/watch/video/renderer.ts b/js/watch/src/video/renderer.ts similarity index 100% rename from js/hang/src/watch/video/renderer.ts rename to js/watch/src/video/renderer.ts diff --git a/js/hang/src/watch/video/source.ts b/js/watch/src/video/source.ts similarity index 98% rename from js/hang/src/watch/video/source.ts rename to js/watch/src/video/source.ts index 68bd6b858..f193245d3 100644 --- a/js/hang/src/watch/video/source.ts +++ b/js/watch/src/video/source.ts @@ -1,6 +1,6 @@ +import type * as Catalog from "@moq/hang/catalog"; import type * as Moq from "@moq/lite"; import { Effect, type Getter, Signal } from "@moq/signals"; -import type * as Catalog from "../../catalog"; import type { Broadcast } from "../broadcast"; import type { Sync } from "../sync"; diff --git a/js/watch/src/vite-env.d.ts b/js/watch/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/js/watch/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/js/watch/src/worklet.d.ts b/js/watch/src/worklet.d.ts new file mode 100644 index 000000000..7bb79c31d --- /dev/null +++ b/js/watch/src/worklet.d.ts @@ -0,0 +1,4 @@ +declare module "*-worklet.ts?worker&url" { + const url: string; + export default url; +} diff --git a/js/watch/tsconfig.build.json b/js/watch/tsconfig.build.json new file mode 100644 index 000000000..ca0a3803e --- /dev/null +++ b/js/watch/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": true + }, + "exclude": ["src/ui"] +} diff --git a/js/watch/tsconfig.json b/js/watch/tsconfig.json new file mode 100644 index 000000000..66214c1f4 --- /dev/null +++ b/js/watch/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "dist" + }, + "include": ["src"], + "exclude": ["src/ui"], + "references": [{ "path": "src/ui" }] +} diff --git a/js/watch/vite.config.ts b/js/watch/vite.config.ts new file mode 100644 index 000000000..eea75c27c --- /dev/null +++ b/js/watch/vite.config.ts @@ -0,0 +1,22 @@ +import { resolve } from "path"; +import { defineConfig } from "vite"; +import solidPlugin from "vite-plugin-solid"; + +export default defineConfig({ + plugins: [solidPlugin()], + build: { + lib: { + entry: { + "ui/index": resolve(__dirname, "src/ui/index.tsx"), + }, + formats: ["es"], + }, + rollupOptions: { + external: ["@moq/hang", "@moq/lite", "@moq/signals", "@moq/ui-core"], + }, + outDir: "dist", + emptyOutDir: false, + sourcemap: true, + target: "esnext", + }, +}); diff --git a/package.json b/package.json index 6f11f4e0f..ea1696504 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "js/clock", "js/token", "js/hang", - "js/hang-ui", + "js/ui-core", + "js/watch", + "js/publish", "js/hang-demo", "js/signals" ],