Skip to content

Commit

Permalink
Merge pull request #263 from binbat/feat/web-node-sub-dashboard
Browse files Browse the repository at this point in the history
feat(web): liveman sub-dashboard for specific nodes
  • Loading branch information
a-wing authored Dec 22, 2024
2 parents 3527ae8 + ec12c83 commit aac6fb6
Show file tree
Hide file tree
Showing 17 changed files with 2,629 additions and 1,055 deletions.
8 changes: 0 additions & 8 deletions docs/.vitepress/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,4 @@ export default defineConfig({
root: en,
zh,
},

vite: {
css: {
postcss: {
// don't need postcss in docs
}
}
}
});
3,511 changes: 2,536 additions & 975 deletions package-lock.json

Large diffs are not rendered by default.

37 changes: 15 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,25 @@
},
"dependencies": {
"@binbat/whip-whep": "^1.1.1-sdp-trickle-throw",
"@nuintun/qrcode": "^4.1.6",
"@heroicons/react": "^2.1.5",
"preact": "^10.24.3",
"@nuintun/qrcode": "^4.1.12",
"@heroicons/react": "^2.2.0",
"preact": "^10.25.3",
"react-daisyui": "^5.0.5",
"typescript-event-target": "^1.1.1",
"wretch": "^2.11.0"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@preact/preset-vite": "^2.9.1",
"@stylistic/eslint-plugin-js": "^2.10.1",
"@types/node": "^22.8.6",
"daisyui": "^4.12.14",
"eslint": "^9.13.0",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"typescript-eslint": "^8.12.2",
"vite": "^5.4.10",
"vitepress": "^1.4.3",
"vitest": "^2.1.4"
},
"postcss": {
"plugins": {
"tailwindcss": {
"config": "./web/tailwind.config.ts"
}
}
"@eslint/js": "^9.17.0",
"@preact/preset-vite": "^2.9.3",
"@stylistic/eslint-plugin-js": "^2.12.1",
"@types/node": "^22.10.2",
"daisyui": "^4.12.22",
"eslint": "^9.17.0",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.1",
"vite": "^6.0.5",
"vitepress": "^1.5.0",
"vitest": "^2.1.8"
}
}
3 changes: 1 addition & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@
},
"include": ["web"],
"exclude": [
"./web/**/vite.config.ts",
"./web/tailwind.config.ts"
"./web/**/vite.config.ts"
],
"references": [{ "path": "./tsconfig.node.json" }]
}
3 changes: 1 addition & 2 deletions tsconfig.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"strict": true
},
"include": [
"./web/**/vite.config.ts",
"./web/tailwind.config.ts"
"./web/**/vite.config.ts"
]
}
3 changes: 1 addition & 2 deletions web/liveion/components/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ export function Login({ show, onSuccess }: LoginProps) {
<span>Token</span>
<input class="grow" value={token} onInput={e => setToken(e.currentTarget?.value)} />
</label>
<Button type="submit" color="primary" className="w-full text-base" disabled={loading}>
{/* @ts-expect-error -- size */}
<Button color="primary" className="w-full text-base" disabled={loading}>
{loading ? <Loading size="sm" /> : null}
<span>Login</span>
</Button>
Expand Down
11 changes: 9 additions & 2 deletions web/liveman/api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import wretch from 'wretch';
import QueryStringAddon from 'wretch/addons/queryString';

import type { Stream } from '../shared/api';
import { type Stream } from '../shared/api';
import { makeAuthorizationMiddleware } from '../shared/authorization-middleware';

const authMiddleware = makeAuthorizationMiddleware();

const w = wretch().middlewares([authMiddleware]);
const w = wretch().addon(QueryStringAddon).middlewares([authMiddleware]);

export const setAuthToken = authMiddleware.setAuthorization;
export const addUnauthorizedCallback = authMiddleware.addUnauthorizedCallback;
Expand All @@ -32,6 +33,12 @@ export function getNodes() {
return w.url('/api/nodes/').get().json<Node[]>();
}

export { type Stream };

export function getStreams(nodes?: string[]) {
return w.url('/api/streams/').query({ nodes }).get().json<Stream[]>();
}

export interface StreamDetail {
[key: string]: Stream;
}
Expand Down
4 changes: 1 addition & 3 deletions web/liveman/components/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ export function Login({ show, onSuccess }: LoginProps) {
<Modal.Header className="mb-2">
<h3 className="font-bold">Authorization Required</h3>
</Modal.Header>
{/* @ts-expect-error -- size */}
<Tabs variant="bordered" size="lg" className="my-4">
{Object.values(AuthorizeType).map(t =>
<Tabs.Tab className="text-base" active={t === authType} onClick={() => setAuthType(t)}>{t}</Tabs.Tab>
Expand All @@ -107,8 +106,7 @@ export function Login({ show, onSuccess }: LoginProps) {
{authType === AuthorizeType.Password ? [usernameInput, passwordInput]
: authType === AuthorizeType.Token ? tokenInput
: null}
<Button type="submit" color="primary" className="w-full text-base" disabled={loading}>
{/* @ts-expect-error -- size */}
<Button color="primary" className="w-full text-base" disabled={loading}>
{loading ? <Loading size="sm" /> : null}
<span>Login</span>
</Button>
Expand Down
3 changes: 1 addition & 2 deletions web/liveman/components/nodes-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function NodesTable() {
<span>{n.status}</span>
<span>{n.duration}</span>
<NodeStrategyLabel strategy={n.strategy} />
<Link href={n.url} target="_blank">{n.url}</Link>
<Link href={location.href + '?nodes=' + n.alias} target="_blank">{location.href + '?nodes=' + n.alias}</Link>
</Table.Row>
) : <tr><td colspan={5} className="text-center">N/A</td></tr>}
</Table.Body>
Expand All @@ -81,7 +81,6 @@ function NodeStrategyLabel({ strategy }: NodeStrategyLabelProps) {
<EllipsisHorizontalIcon className="size-4" />
</Dropdown.Toggle>
<Dropdown.Menu className="z-10 mx-[-1rem]">
{/* @ts-expect-error -- size */}
<Table size="xs">
<Table.Body>
{entries.map(([k, v]) =>
Expand Down
31 changes: 27 additions & 4 deletions web/liveman/liveman.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from 'preact/hooks';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { Button } from 'react-daisyui';

import { type Stream } from '@/shared/api';
Expand All @@ -14,12 +14,30 @@ import { type IStreamTokenDialog, StreamTokenDialog } from './components/dialog-
export function Liveman() {
const [token, setToken] = useState('');
const [needsAuthorizaiton, setNeedsAuthorization] = useNeedAuthorization(api);

const onLoginSuccess = (t: string) => {
setToken(t);
setNeedsAuthorization(false);
};

const [filterNodes, setFilterNodes] = useState<string[] | undefined>();
useEffect(() => {
const params = new URLSearchParams(location.search);
setFilterNodes(params.getAll('nodes'));
}, [location.search]);
const getStreams = useCallback(async () => {
const streams = await api.getStreams(filterNodes);
return streams.sort((a, b) => a.createdAt - b.createdAt);
}, [filterNodes]);
const getWhxpUrl = (whxp: 'whep' | 'whip', streamId: string) => {
let url = `/${whxp}/${streamId}`;
if (filterNodes && filterNodes.length > 0) {
const params = new URLSearchParams();
filterNodes?.forEach(v => params.append('nodes', v));
url += `?${params.toString()}`;
}
return new URL(url, location.origin).toString();
};

const refStreamTokenDialog = useRef<IStreamTokenDialog>(null);
const renderCreateToken = useCallback((stream: Stream) => {
return (
Expand All @@ -30,8 +48,13 @@ export function Liveman() {
return (
<>
<PageLayout token={token}>
<NodesTable />
<StreamsTable renderExtraActions={renderCreateToken} />
{filterNodes && filterNodes.length > 0 ? null : <NodesTable />}
<StreamsTable
getStreams={getStreams}
getWhepUrl={streamId => getWhxpUrl('whep', streamId)}
getWhipUrl={streamId => getWhxpUrl('whip', streamId)}
renderExtraActions={renderCreateToken}
/>
</PageLayout>
<StreamTokenDialog ref={refStreamTokenDialog} />
<Login
Expand Down
3 changes: 2 additions & 1 deletion web/shared/components/dialog-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useLogger } from '../hooks/use-logger';
import { QRCodeStreamDecoder } from '../qrcode-stream';

interface Props {
getWhepUrl?: (streamId: string) => string;
onStop(): void
}

Expand Down Expand Up @@ -119,7 +120,6 @@ export const PreviewDialog = forwardRef<IPreviewDialog, Props>((props, ref) => {
});
refPeerConnection.current = pc;
const whep = new WHEPClient();
const url = `${location.origin}/whep/${streamId}`;
whep.onOffer = sdp => {
logger.log('http offer sent');
return sdp;
Expand All @@ -130,6 +130,7 @@ export const PreviewDialog = forwardRef<IPreviewDialog, Props>((props, ref) => {
};
refWhepClient.current = whep;
try {
const url = props.getWhepUrl?.(streamId) ?? `${location.origin}/whep/${streamId}`;
await whep.view(pc, url, tokenContext.token);
} catch (e: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
setConnState('Error');
Expand Down
3 changes: 2 additions & 1 deletion web/shared/components/dialog-web-stream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useLogger } from '../hooks/use-logger';
import { QRCodeStream } from '../qrcode-stream';

interface Props {
getWhipUrl?: (streamId: string) => string;
onStop(): void
}

Expand Down Expand Up @@ -68,7 +69,6 @@ export const WebStreamDialog = forwardRef<IWebStreamDialog, Props>((props, ref)
});
const whip = new WHIPClient();
refWhipClient.current = whip;
const url = `${location.origin}/whip/${streamId}`;
whip.onOffer = sdp => {
logger.log('http offer sent');
return sdp;
Expand All @@ -78,6 +78,7 @@ export const WebStreamDialog = forwardRef<IWebStreamDialog, Props>((props, ref)
return sdp;
};
try {
const url = props.getWhipUrl?.(streamId) ?? `${location.origin}/whep/${streamId}`;
await whip.publish(pc, url, tokenContext.token);
} catch (e: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
setConnState('Error');
Expand Down
7 changes: 6 additions & 1 deletion web/shared/components/streams-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ async function getStreamsSorted() {
}

export interface StreamTableProps {
getStreams?: () => Promise<Stream[]>;
getWhepUrl?: (streamId: string) => string;
getWhipUrl?: (streamId: string) => string;
showCascade?: boolean;
renderExtraActions?: (s: Stream) => ReactNode;
}

export function StreamsTable(props: StreamTableProps) {
const streams = useRefreshTimer([], getStreamsSorted);
const streams = useRefreshTimer([], props.getStreams ?? getStreamsSorted);
const [selectedStreamId, setSelectedStreamId] = useState('');
const refCascadePull = useRef<ICascadeDialog>(null);
const refCascadePush = useRef<ICascadeDialog>(null);
Expand Down Expand Up @@ -227,6 +230,7 @@ export function StreamsTable(props: StreamTableProps) {
refPreviewStreams.current.delete(s);
}
}}
getWhepUrl={props.getWhepUrl}
onStop={() => handlePreviewStop(s)}
/>
)}
Expand All @@ -243,6 +247,7 @@ export function StreamsTable(props: StreamTableProps) {
refWebStreams.current.delete(s);
}
}}
getWhipUrl={props.getWhipUrl}
onStop={() => handleWebStreamStop(s)}
/>
)}
Expand Down
18 changes: 7 additions & 11 deletions web/shared/hooks/use-refresh-timer.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
import { useEffect, useState } from 'preact/hooks';
import { useCallback, useEffect, useState } from 'preact/hooks';

export function useRefreshTimer<T>(initial: T, fetchData: () => Promise<T>, timeout = 3000, immediate = true) {
const [data, setData] = useState<T>(initial);
const [isImmediate, _setIsImmediate] = useState(immediate);
const [refreshTimer, setRefreshTimer] = useState(-1);
const isRefreshing = refreshTimer > 0;
const updateData = async () => {
setData(await fetchData());
};
const updateData = useCallback(async () => setData(await fetchData()), [fetchData]);
useEffect(() => {
if (isImmediate) {
updateData();
}
return () => {
if (isRefreshing) {
window.clearInterval(refreshTimer);
}
};
}, []);
useEffect(() => {
if (isRefreshing) {
clearInterval(refreshTimer);
window.clearInterval(refreshTimer);
setRefreshTimer(window.setInterval(updateData, timeout));
}
}, [timeout]);
return () => {
window.clearInterval(refreshTimer);
};
}, [updateData, timeout]);
const toggleTimer = () => {
if (isRefreshing) {
clearInterval(refreshTimer);
Expand Down
4 changes: 2 additions & 2 deletions web/shared/tools/debugger/compat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export default function DebuggerCompat() {

<section>
<h3>WHIP Video:</h3>
<debug-player controls autoplay id="whip-video-player"></debug-player>
<debug-player id="whip-video-player"></debug-player>
</section>
<section>
<data-channel id="whip-datachannel"></data-channel>
Expand Down Expand Up @@ -202,7 +202,7 @@ export default function DebuggerCompat() {

<section>
<h3>WHEP Video:</h3>
<debug-player controls autoplay id="whep-video-player"></debug-player>
<debug-player id="whep-video-player"></debug-player>
</section>
<section>
<data-channel id="whep-datachannel"></data-channel>
Expand Down
17 changes: 0 additions & 17 deletions web/tailwind.config.ts

This file was deleted.

18 changes: 18 additions & 0 deletions web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { resolve } from 'node:path';

import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
import tailwindcss from 'tailwindcss';
import daisyui from 'daisyui';

export const ProjectRoot = resolve(import.meta.dirname, '..');

Expand All @@ -21,5 +23,21 @@ export default defineConfig({
alias: {
'@': resolve(ProjectRoot, 'web')
}
},
css: {
postcss: {
plugins: [
tailwindcss({
content: [
'node_modules/daisyui/dist/**/*.js',
'node_modules/react-daisyui/dist/**/*.js',
'web/**/*.{html,tsx}'
].map(p => resolve(ProjectRoot, p)),
plugins: [
daisyui
],
})
]
}
}
});

0 comments on commit aac6fb6

Please sign in to comment.