fix: request media directly and expose host moderation
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { DisconnectReason, Room, RoomEvent, Track, type LocalTrack, type Participant } from "livekit-client";
|
||||
import { DisconnectReason, Room, RoomEvent, Track, type Participant } from "livekit-client";
|
||||
import { PreJoinScreen } from "./PreJoinScreen";
|
||||
import { SfuVideoTile } from "./SfuVideoTile";
|
||||
import { MEETING_SESSION_KEY, type StoredMeetingSession } from "./JoinForm";
|
||||
@@ -73,7 +73,7 @@ export function CallRoom({ slug }: { slug: string }) {
|
||||
const [callDurationMs, setCallDurationMs] = useState<number | null>(null);
|
||||
|
||||
const roomRef = useRef<Room | null>(null);
|
||||
const localTracksRef = useRef<LocalTrack[]>([]);
|
||||
const localTracksRef = useRef<MediaStreamTrack[]>([]);
|
||||
const activeSpeakersRef = useRef<Set<string>>(new Set());
|
||||
const intentionalCloseRef = useRef(false);
|
||||
const connectedAtRef = useRef<number | null>(null);
|
||||
@@ -163,7 +163,7 @@ export function CallRoom({ slug }: { slug: string }) {
|
||||
setPhase("ended");
|
||||
}
|
||||
|
||||
async function connectRoom(previewTracks: LocalTrack[]) {
|
||||
async function connectRoom(previewTracks: MediaStreamTrack[]) {
|
||||
if (!session) return;
|
||||
setPhase("connecting");
|
||||
setErrorMessage(null);
|
||||
@@ -225,12 +225,12 @@ export function CallRoom({ slug }: { slug: string }) {
|
||||
await room.connect(serverUrl, participantToken);
|
||||
for (const track of previewTracks) {
|
||||
await room.localParticipant.publishTrack(track, {
|
||||
source: track.kind === Track.Kind.Audio ? Track.Source.Microphone : Track.Source.Camera,
|
||||
source: track.kind === "audio" ? Track.Source.Microphone : Track.Source.Camera,
|
||||
});
|
||||
}
|
||||
refresh();
|
||||
setMicOn(previewTracks.some((track) => track.kind === Track.Kind.Audio && track.mediaStreamTrack.enabled));
|
||||
setCamOn(previewTracks.some((track) => track.kind === Track.Kind.Video && track.mediaStreamTrack.enabled));
|
||||
setMicOn(previewTracks.some((track) => track.kind === "audio" && track.enabled));
|
||||
setCamOn(previewTracks.some((track) => track.kind === "video" && track.enabled));
|
||||
connectedAtRef.current = Date.now();
|
||||
setCallDurationMs(null);
|
||||
setPhase("connected");
|
||||
@@ -366,7 +366,9 @@ export function CallRoom({ slug }: { slug: string }) {
|
||||
<div><p className="text-sm font-medium">{session.eventTitle}</p><p className="text-xs text-white/45">LiveKit-SFU · {participants.length} Teilnehmer</p></div>
|
||||
<div className="flex items-center gap-2">
|
||||
{(["grid", "speaker", "screen"] as LayoutMode[]).map((mode) => <button key={mode} type="button" onClick={() => setLayout(mode)} aria-pressed={layout === mode} className={`rounded-full px-3 py-2 text-xs ${layout === mode ? "bg-white text-neutral-900" : "bg-white/10 text-white/75 hover:bg-white/15"}`}>{mode === "grid" ? "Galerie" : mode === "speaker" ? "Sprecher" : "Bildschirm"}</button>)}
|
||||
<button type="button" onClick={() => setShowParticipants((value) => !value)} className="rounded-full bg-white/10 px-3 py-2 text-xs hover:bg-white/15">Teilnehmer ({participants.length})</button>
|
||||
<button type="button" onClick={() => setShowParticipants((value) => !value)} aria-expanded={showParticipants} className={`rounded-full px-3 py-2 text-xs transition-colors ${showParticipants ? "bg-white text-neutral-900" : "bg-white/10 text-white hover:bg-white/15"}`}>
|
||||
{session.role === "host" ? `Moderieren (${participants.length})` : `Teilnehmer (${participants.length})`}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
{errorMessage && <p className="bg-amber-900/50 px-5 py-2 text-center text-xs text-amber-100">{errorMessage}</p>}
|
||||
@@ -376,7 +378,36 @@ export function CallRoom({ slug }: { slug: string }) {
|
||||
{layout === "speaker" && activeSpeaker && <div className="space-y-3"><div className="mx-auto max-w-5xl">{tile(activeSpeaker, { videoTrack: activeSpeaker.screenTrack ?? activeSpeaker.cameraTrack })}</div><div className="grid grid-cols-2 gap-3 sm:grid-cols-4">{participants.filter((participant) => participant.identity !== activeSpeaker.identity).map((participant) => tile(participant, { compact: true }))}</div></div>}
|
||||
{layout === "screen" && <div className="space-y-3">{screenOwner ? <div className="mx-auto max-w-6xl">{tile(screenOwner, { videoTrack: screenOwner.screenTrack, name: `${screenOwner.name} · Bildschirm` })}</div> : <div className="flex aspect-video items-center justify-center rounded-3xl border border-dashed border-white/15 text-sm text-white/45">Noch niemand teilt den Bildschirm.</div>}<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">{participants.map((participant) => tile(participant, { compact: true }))}</div></div>}
|
||||
</section>
|
||||
{showParticipants && <aside className="w-full border-t border-white/10 p-4 lg:w-72 lg:border-l lg:border-t-0"><h2 className="mb-3 text-xs font-medium uppercase tracking-wide text-white/55">Teilnehmer</h2><ul className="space-y-3">{participants.map((participant) => <li key={participant.identity} className="flex items-center justify-between gap-2 text-sm"><span className="truncate">● {participant.name}{participant.identity === local?.identity && " (du)"}</span>{session.role === "host" && participant.identity !== local?.identity && <button type="button" onClick={() => void kickParticipant(participant.identity)} className="shrink-0 text-xs text-white/50 hover:text-red-300">entfernen</button>}</li>)}</ul></aside>}
|
||||
{showParticipants && (
|
||||
<aside className="w-full border-t border-white/10 p-4 lg:w-80 lg:border-l lg:border-t-0">
|
||||
<h2 className="text-xs font-medium uppercase tracking-wide text-white/55">{session.role === "host" ? "Moderation" : "Teilnehmer"}</h2>
|
||||
{session.role === "host" && (
|
||||
<div className="mt-3 rounded-2xl border border-anouma-rose/20 bg-anouma-mauve-dark/20 p-3 text-xs leading-relaxed text-white/75">
|
||||
Du bist Host. Hier kannst du sehen, wer beigetreten ist, und Personen aus dem Meeting entfernen.
|
||||
</div>
|
||||
)}
|
||||
<ul className="mt-4 space-y-2">
|
||||
{participants.map((participant) => {
|
||||
const isYou = participant.identity === local?.identity;
|
||||
return (
|
||||
<li key={participant.identity} className="flex items-center justify-between gap-3 rounded-2xl bg-white/5 px-3 py-2.5 text-sm">
|
||||
<span className="min-w-0 truncate">{participant.name}{isYou && " (du)"}</span>
|
||||
{session.role === "host" && !isYou ? (
|
||||
<button type="button" onClick={() => void kickParticipant(participant.identity)} className="shrink-0 rounded-full bg-red-500/15 px-3 py-1.5 text-xs font-medium text-red-200 transition-colors hover:bg-red-500 hover:text-white">
|
||||
Entfernen
|
||||
</button>
|
||||
) : (
|
||||
<span className="shrink-0 text-[10px] uppercase tracking-wide text-white/45">{participant.role === "host" ? "Host" : "Im Call"}</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{session.role === "host" && remoteParticipants.length === 0 && (
|
||||
<p className="mt-4 text-xs leading-relaxed text-white/45">Sobald weitere Personen beitreten, kannst du sie hier direkt verwalten.</p>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
<footer className="flex flex-wrap items-center justify-center gap-3 border-t border-white/10 px-5 py-4">
|
||||
<ControlButton enabled={micOn} busy={pendingControl === "mic"} onClick={() => void toggleMic()} label="Mikrofon" icon="mic" />
|
||||
|
||||
Reference in New Issue
Block a user