-
Notifications
You must be signed in to change notification settings - Fork 127
Hang UI as a separate package - Issue 695 #717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9fe06cb
c3e9187
33dd744
c0a1b1d
6f35008
58b3bab
9ff000a
f001671
a62ec0e
9934618
d82773d
1501401
8b23c5b
c187923
d2384ee
3ec936a
6cf9471
8fb9b9d
3650f45
4978644
eeecb01
f954a89
e3b7aea
12906f8
0d2e6b8
a5721cf
93f474f
e77c316
839764d
e54b14e
a2a843f
f7a0c4c
cbb5dfb
2f7de8b
a480d5e
3f41d25
814326b
f31063e
e90166b
79e796a
61e2660
3796fe2
1e7b7e0
3a9d9e4
4c14759
e8d97e4
092a163
48b18a9
b3fe482
cc111d5
e16c241
a5b8178
0a2f34e
d1af166
fa6f555
49ce3cf
15c3705
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| <p align="center"> | ||
| <img height="128px" src="https://github.com/kixelated/moq/blob/main/.github/logo.svg" alt="Media over QUIC"> | ||
| </p> | ||
|
|
||
| # @kixelated/hang-ui | ||
|
|
||
| [](https://www.npmjs.com/package/@kixelated/hang-ui) | ||
| [](https://www.typescriptlang.org/) | ||
|
|
||
| A TypeScript library for interacting with @kixelated/hang Web Components. Provides methods to control playback and publish sources, as well as status of the connection. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| npm add @kixelated/hang-ui | ||
| # or | ||
| pnpm add @kixelated/hang-ui | ||
| yarn add @kixelated/hang-ui | ||
| bun add @kixelated/hang-ui | ||
| ``` | ||
|
|
||
| ## Web Components | ||
|
|
||
| Currently, there are two Web Components provided by @kixelated/hang-ui: | ||
|
|
||
| - `<hang-watch-ui>` | ||
| - `<hang-publish-ui>` | ||
|
|
||
| Here's how you can use them (see also @kixelated/hang-demo for a complete example): | ||
|
|
||
| ```html | ||
| <hang-watch-ui> | ||
| <hang-watch url="<MOQ relay URL>" path="<relay path>" muted> | ||
| <canvas style="width: 100%; height: auto; border-radius: 4px; margin: 0 auto;"></canvas> | ||
| </hang-watch> | ||
| </hang-watch-ui> | ||
| ``` | ||
|
|
||
| ```html | ||
| <hang-publish-ui> | ||
| <hang-publish url="<MOQ relay URL>" path="<relay path>"> | ||
| <video | ||
| style="width: 100%; height: auto; border-radius: 4px; margin: 0 auto;" | ||
| muted | ||
| autoplay | ||
| ></video> | ||
| </hang-publish> | ||
| </hang-publish-ui> | ||
| ``` | ||
|
Comment on lines
+1
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Fix indentation to use spaces instead of tabs. The file contains hard tabs on multiple lines (2, 40-48), which conflicts with the project's Biome configuration that specifies Run the following command to automatically fix the formatting: #!/bin/bash
# Fix formatting for the hang-ui README
cd js/hang-ui && bun run fix🧰 Tools🪛 markdownlint-cli2 (0.18.1)2-2: Hard tabs (MD010, no-hard-tabs) 40-40: Hard tabs (MD010, no-hard-tabs) 41-41: Hard tabs (MD010, no-hard-tabs) 42-42: Hard tabs (MD010, no-hard-tabs) 43-43: Hard tabs (MD010, no-hard-tabs) 44-44: Hard tabs (MD010, no-hard-tabs) 45-45: Hard tabs (MD010, no-hard-tabs) 46-46: Hard tabs (MD010, no-hard-tabs) 47-47: Hard tabs (MD010, no-hard-tabs) 48-48: Hard tabs (MD010, no-hard-tabs) 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| { | ||
| "name": "@kixelated/hang-ui", | ||
| "type": "module", | ||
| "version": "0.1.0", | ||
| "description": "Media over QUIC library UI components", | ||
| "license": "(MIT OR Apache-2.0)", | ||
| "repository": "github:kixelated/moq", | ||
| "main": "dist/publish-controls.esm.js", | ||
| "module": "dist/publish-controls.esm.js", | ||
| "exports": { | ||
| "./publish/element": "./dist/publish-controls.esm.js", | ||
| "./watch/element": "./dist/watch-controls.esm.js" | ||
| }, | ||
| "sideEffects": true, | ||
| "scripts": { | ||
| "build": "npm run clean && rollup -c", | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And yeah, the other packages use a script to rewrite package.json to change |
||
| "check": "biome check src", | ||
| "clean": "rimraf dist", | ||
| "fix": "biome check src --fix" | ||
| }, | ||
| "devDependencies": { | ||
| "@biomejs/biome": "^2.2.2", | ||
| "@kixelated/hang": "workspace:^0.7.0", | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be a peerDependency? |
||
| "@rollup/plugin-node-resolve": "^16.0.3", | ||
| "rimraf": "^6.0.1", | ||
| "rollup": "^4.53.3", | ||
| "rollup-plugin-esbuild": "^6.2.1", | ||
| "solid-element": "^1.9.1", | ||
| "solid-js": "^1.9.10", | ||
| "unplugin-solid": "^1.0.0" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| // rollup.config.mjs / rollup.config.js | ||
| import { readFileSync } from 'node:fs'; | ||
| import nodeResolve from '@rollup/plugin-node-resolve'; | ||
| import esbuild from 'rollup-plugin-esbuild'; | ||
| import solid from 'unplugin-solid/rollup'; | ||
|
|
||
| function inlineCss() { | ||
| return { | ||
| name: 'inline-css', | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like the most simple solution for now to include the CSS without having the user include a separate file, but as the UI grows, this made lead to issues with larger bundle sizes.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might want to do this anyway for |
||
| load(id) { | ||
| if (id.endsWith('.css?inline')) { | ||
| const realId = id.replace(/\?inline$/, ''); | ||
| const css = readFileSync(realId, 'utf8'); | ||
| return `export default ${JSON.stringify(css)};`; | ||
| } | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| export default [ | ||
| { | ||
| input: 'src/Components/publish/element.tsx', | ||
| output: { | ||
| file: 'dist/publish-controls.esm.js', | ||
| format: 'es', | ||
| sourcemap: true, | ||
| }, | ||
| plugins: [ | ||
| inlineCss(), | ||
| solid({ dev: false, hydratable: false }), | ||
| esbuild({ | ||
| include: /\.[jt]sx?$/, | ||
| jsx: 'preserve', | ||
| tsconfig: 'tsconfig.json', | ||
| }), | ||
| nodeResolve({ extensions: ['.js', '.ts', '.tsx'] }), | ||
| ], | ||
| }, | ||
| { | ||
| input: 'src/Components/watch/element.tsx', | ||
| output: { | ||
| file: 'dist/watch-controls.esm.js', | ||
| format: 'es', | ||
| sourcemap: true, | ||
| }, | ||
| plugins: [ | ||
| inlineCss(), | ||
| solid({ dev: false, hydratable: false }), | ||
| esbuild({ | ||
| include: /\.[jt]sx?$/, | ||
| jsx: 'preserve', | ||
| tsconfig: 'tsconfig.json', | ||
| }), | ||
| nodeResolve({ extensions: ['.js', '.ts', '.tsx'] }), | ||
| ], | ||
| }, | ||
| ]; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { Show, useContext } from "solid-js"; | ||
| import MediaSourceSourceSelector from "./MediaSourceSelector"; | ||
| import { PublishUIContext } from "./PublishUIContextProvider"; | ||
|
|
||
| export default function CameraSourceButton() { | ||
| const context = useContext(PublishUIContext); | ||
| const onClick = () => { | ||
| const hangPublishEl = context?.hangPublish(); | ||
| if (!hangPublishEl) return; | ||
|
|
||
| if (hangPublishEl.source === "camera") { | ||
| // Camera already selected, toggle video. | ||
| hangPublishEl.video = !hangPublishEl.video; | ||
| } else { | ||
| hangPublishEl.source = "camera"; | ||
| hangPublishEl.video = true; | ||
| } | ||
| }; | ||
|
|
||
| const onSourceSelected = (sourceId: MediaDeviceInfo["deviceId"]) => { | ||
| const hangPublishEl = context?.hangPublish(); | ||
| if (!hangPublishEl) return; | ||
|
|
||
| hangPublishEl.videoDevice = sourceId; | ||
| }; | ||
|
|
||
| return ( | ||
| <div class="publishSourceButtonContainer"> | ||
| <button | ||
| type="button" | ||
| title="Camera" | ||
| class={`publishButton publishSourceButton ${context?.cameraActive?.() ? "active" : ""}`} | ||
| onClick={onClick} | ||
| > | ||
| 📷 | ||
| </button> | ||
| <Show when={context?.cameraActive?.() && context?.cameraDevices().length}> | ||
| <MediaSourceSourceSelector | ||
| sources={context?.cameraDevices()} | ||
| selectedSource={context?.selectedCameraSource?.()} | ||
| onSelected={onSourceSelected} | ||
| /> | ||
| </Show> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { createSignal, useContext } from "solid-js"; | ||
| import { PublishUIContext } from "./PublishUIContextProvider"; | ||
|
|
||
| export default function FileSourceButton() { | ||
| const [fileInputRef, setFileInputRef] = createSignal<HTMLInputElement | undefined>(); | ||
| const context = useContext(PublishUIContext); | ||
| const onClick = () => fileInputRef()?.click(); | ||
| const onChange = (event: Event) => { | ||
| const castedInputEl = event.target as HTMLInputElement; | ||
| const file = castedInputEl.files?.[0]; | ||
|
|
||
| if (file) { | ||
| context?.setFile(file); | ||
| castedInputEl.value = ""; | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <input | ||
| ref={setFileInputRef} | ||
| onChange={onChange} | ||
| type="file" | ||
| class="hidden" | ||
| accept="video/*,audio/*,image/*" | ||
| /> | ||
| <button | ||
| type="button" | ||
| title="Upload File" | ||
| onClick={onClick} | ||
| class={`publishButton publishSourceButton ${context?.fileActive?.() ? "active" : ""}`} | ||
| > | ||
| 📁 | ||
| </button> | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { createSignal, For, Show } from "solid-js"; | ||
|
|
||
| type MediaSourceSelectorProps = { | ||
| sources?: MediaDeviceInfo[]; | ||
| selectedSource?: MediaDeviceInfo["deviceId"]; | ||
| onSelected?: (sourceId: MediaDeviceInfo["deviceId"]) => void; | ||
| }; | ||
|
|
||
| export default function MediaSourceSelector(props: MediaSourceSelectorProps) { | ||
| const [sourcesVisible, setSourcesVisible] = createSignal(false); | ||
|
|
||
| const toggleSourcesVisible = () => setSourcesVisible((visible) => !visible); | ||
|
|
||
| return ( | ||
| <> | ||
| <button | ||
| type="button" | ||
| onClick={toggleSourcesVisible} | ||
| class="publishButton mediaSourceVisibilityToggle" | ||
| title={sourcesVisible() ? "Hide Sources" : "Show Sources"} | ||
| > | ||
| {sourcesVisible() ? "▲" : "▼"} | ||
| </button> | ||
| <Show when={sourcesVisible()}> | ||
| <select | ||
| value={props.selectedSource} | ||
| class="mediaSourceSelector" | ||
| onChange={(e) => props.onSelected?.(e.currentTarget.value as MediaDeviceInfo["deviceId"])} | ||
| > | ||
| <For each={props.sources}> | ||
| {(source) => <option value={source.deviceId}>{source.label}</option>} | ||
| </For> | ||
| </select> | ||
| </Show> | ||
| </> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kinda gross, but harmless I guess. You should leave a comment that this isn't needed when using the package from NPM.
This wasn't needed when
package.jsonpointed tosrcinstead ofdist.