feat(frontend): dev mock-join fallback when LiveKit unconfigured
If the backend returns the placeholder LiveKit URL, skip the real connect and screen capture and drop into the post-join state (with a DEV MODE banner) so the connected UI is developable without LiveKit creds or HTTPS. Real joins also guard getDisplayMedia behind isSecureContext. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+16
-3
@@ -8,6 +8,8 @@ export default function App() {
|
|||||||
const team = useMemo(() => TEAMS.find((t) => t.id === teamId) ?? TEAMS[0]!, [teamId]);
|
const team = useMemo(() => TEAMS.find((t) => t.id === teamId) ?? TEAMS[0]!, [teamId]);
|
||||||
const [member, setMember] = useState(team.members[0]!);
|
const [member, setMember] = useState(team.members[0]!);
|
||||||
const [room, setRoom] = useState<Room | null>(null);
|
const [room, setRoom] = useState<Room | null>(null);
|
||||||
|
const [joined, setJoined] = useState(false);
|
||||||
|
const [devMode, setDevMode] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [connecting, setConnecting] = useState(false);
|
const [connecting, setConnecting] = useState(false);
|
||||||
|
|
||||||
@@ -22,7 +24,10 @@ export default function App() {
|
|||||||
setConnecting(true);
|
setConnecting(true);
|
||||||
try {
|
try {
|
||||||
const identity = `${member}-${Math.random().toString(36).slice(2, 7)}`;
|
const identity = `${member}-${Math.random().toString(36).slice(2, 7)}`;
|
||||||
setRoom(await joinPod(team.id, identity, member));
|
const result = await joinPod(team.id, identity, member);
|
||||||
|
setRoom(result.room);
|
||||||
|
setDevMode(result.mode === 'dev');
|
||||||
|
setJoined(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError((err as Error).message);
|
setError((err as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -39,12 +44,20 @@ export default function App() {
|
|||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{room ? (
|
{joined ? (
|
||||||
<section className="rounded-lg border border-slate-700 bg-slate-900/50 p-4">
|
<section className="rounded-lg border border-slate-700 bg-slate-900/50 p-4">
|
||||||
<p className="font-medium text-emerald-400">
|
<p className="font-medium text-emerald-400">
|
||||||
{member} connected to “{team.name}”.
|
{member} connected to “{team.name}”.
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-sm text-slate-400">Sharing screen + mic. PodMan is watching.</p>
|
<p className="mt-1 text-sm text-slate-400">
|
||||||
|
{room ? 'Sharing screen + mic. PodMan is watching.' : 'Pod joined.'}
|
||||||
|
</p>
|
||||||
|
{devMode && (
|
||||||
|
<p className="mt-3 rounded-md border border-amber-700/50 bg-amber-950/40 px-3 py-2 text-xs text-amber-300">
|
||||||
|
DEV MODE — LiveKit not configured, so screen capture is off. Set LIVEKIT_* in the
|
||||||
|
backend .env (and serve over HTTPS) for a real join.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
|
|||||||
+25
-6
@@ -2,7 +2,7 @@ import { Room, RoomEvent } from 'livekit-client';
|
|||||||
|
|
||||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8787';
|
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8787';
|
||||||
|
|
||||||
/** Ask the backend for a LiveKit token to join a pod. */
|
/** Token + LiveKit URL minted by the backend. */
|
||||||
export async function fetchPodToken(
|
export async function fetchPodToken(
|
||||||
podId: string,
|
podId: string,
|
||||||
identity: string,
|
identity: string,
|
||||||
@@ -17,22 +17,41 @@ export async function fetchPodToken(
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** True once a real LiveKit server is configured (not the placeholder). */
|
||||||
|
export function isLiveKitConfigured(url: string | undefined): boolean {
|
||||||
|
return !!url && !url.includes('REPLACE_ME');
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JoinResult = { mode: 'live'; room: Room } | { mode: 'dev'; room: null };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Join a pod: connect to the LiveKit room and publish screen + mic so PodMan
|
* Join a pod. When LiveKit is configured we connect for real and publish
|
||||||
* can watch. Returns the connected Room.
|
* screen + mic. Otherwise we fall back to a dev mock join so the post-join UI
|
||||||
|
* is developable without LiveKit creds / HTTPS.
|
||||||
*/
|
*/
|
||||||
export async function joinPod(podId: string, identity: string, name: string): Promise<Room> {
|
export async function joinPod(podId: string, identity: string, name: string): Promise<JoinResult> {
|
||||||
const { token, url } = await fetchPodToken(podId, identity, name);
|
const { token, url } = await fetchPodToken(podId, identity, name);
|
||||||
|
|
||||||
|
if (!isLiveKitConfigured(url)) {
|
||||||
|
console.warn('[podman] LiveKit not configured — dev mock join (no screen capture)');
|
||||||
|
return { mode: 'dev', room: null };
|
||||||
|
}
|
||||||
|
|
||||||
const room = new Room({ adaptiveStream: true, dynacast: true });
|
const room = new Room({ adaptiveStream: true, dynacast: true });
|
||||||
room.on(RoomEvent.Disconnected, () => console.log('[podman] disconnected'));
|
room.on(RoomEvent.Disconnected, () => console.log('[podman] disconnected'));
|
||||||
|
|
||||||
await room.connect(url, token);
|
await room.connect(url, token);
|
||||||
|
|
||||||
|
// Screen capture needs a secure context (HTTPS or localhost). Guard so a
|
||||||
|
// non-secure origin doesn't hard-crash the join.
|
||||||
|
if (window.isSecureContext && navigator.mediaDevices?.getDisplayMedia) {
|
||||||
const screen = await navigator.mediaDevices.getDisplayMedia({ video: true });
|
const screen = await navigator.mediaDevices.getDisplayMedia({ video: true });
|
||||||
for (const track of screen.getTracks()) {
|
for (const track of screen.getTracks()) {
|
||||||
await room.localParticipant.publishTrack(track);
|
await room.localParticipant.publishTrack(track);
|
||||||
}
|
}
|
||||||
await room.localParticipant.setMicrophoneEnabled(true);
|
await room.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
} else {
|
||||||
|
console.warn('[podman] insecure context — screen capture skipped (needs HTTPS)');
|
||||||
|
}
|
||||||
|
|
||||||
return room;
|
return { mode: 'live', room };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user