fix: request media directly and expose host moderation
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { createLocalTracks, Track, type LocalTrack } from "livekit-client";
|
||||
|
||||
function describeDeviceError(error: unknown): string {
|
||||
const name = error instanceof Error ? error.name : "";
|
||||
@@ -19,43 +18,66 @@ function describeDeviceError(error: unknown): string {
|
||||
return "Kamera oder Mikrofon konnten nicht gestartet werden. Du kannst dem Meeting trotzdem ohne sie beitreten.";
|
||||
}
|
||||
|
||||
export function PreJoinScreen({ onContinue }: { onContinue: (tracks: LocalTrack[]) => void }) {
|
||||
const [tracks, setTracks] = useState<LocalTrack[]>([]);
|
||||
export function PreJoinScreen({ onContinue }: { onContinue: (tracks: MediaStreamTrack[]) => void }) {
|
||||
const [tracks, setTracks] = useState<MediaStreamTrack[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [micOn, setMicOn] = useState(true);
|
||||
const [cameraOn, setCameraOn] = useState(true);
|
||||
const [speakerTested, setSpeakerTested] = useState(false);
|
||||
const [requestingDevices, setRequestingDevices] = useState(false);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const tracksRef = useRef<MediaStreamTrack[]>([]);
|
||||
const handedOffRef = useRef(false);
|
||||
|
||||
const videoTrack = tracks.find((track) => track.kind === Track.Kind.Video);
|
||||
const audioTrack = tracks.find((track) => track.kind === Track.Kind.Audio);
|
||||
const videoTrack = tracks.find((track) => track.kind === "video");
|
||||
const audioTrack = tracks.find((track) => track.kind === "audio");
|
||||
|
||||
useEffect(() => {
|
||||
const videoElement = videoRef.current;
|
||||
if (!videoElement || !videoTrack) return;
|
||||
videoTrack.attach(videoElement);
|
||||
if (!videoElement || !videoTrack || !cameraOn) return;
|
||||
videoElement.srcObject = new MediaStream([videoTrack]);
|
||||
void videoElement.play().catch(() => {
|
||||
// Autoplay may be restricted until the user interacts with the page.
|
||||
});
|
||||
return () => {
|
||||
videoTrack.detach(videoElement);
|
||||
videoElement.srcObject = null;
|
||||
};
|
||||
}, [videoTrack]);
|
||||
}, [cameraOn, videoTrack]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (!handedOffRef.current) tracks.forEach((track) => track.stop());
|
||||
if (!handedOffRef.current) tracksRef.current.forEach((track) => track.stop());
|
||||
};
|
||||
}, [tracks]);
|
||||
}, []);
|
||||
|
||||
async function requestDevices() {
|
||||
if (requestingDevices) return;
|
||||
setError(null);
|
||||
|
||||
if (!navigator.mediaDevices?.getUserMedia) {
|
||||
setError("Dein Browser unterstützt keine Kamera- und Mikrofonfreigabe für Video-Calls.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the native browser request immediately while this click still has
|
||||
// user activation. LiveKit receives these tracks only after the preview.
|
||||
let request: Promise<MediaStream>;
|
||||
try {
|
||||
request = navigator.mediaDevices.getUserMedia({
|
||||
audio: { autoGainControl: true, echoCancellation: true, noiseSuppression: true },
|
||||
video: { facingMode: "user", height: { ideal: 720 }, width: { ideal: 1280 } },
|
||||
});
|
||||
} catch (requestError) {
|
||||
setError(describeDeviceError(requestError));
|
||||
return;
|
||||
}
|
||||
|
||||
setRequestingDevices(true);
|
||||
try {
|
||||
// This must originate from the button click. Some privacy-focused
|
||||
// browsers deliberately ignore a getUserMedia request from useEffect.
|
||||
const nextTracks = await createLocalTracks({ audio: true, video: true });
|
||||
tracks.forEach((track) => track.stop());
|
||||
const stream = await request;
|
||||
const nextTracks = stream.getTracks();
|
||||
tracksRef.current.forEach((track) => track.stop());
|
||||
tracksRef.current = nextTracks;
|
||||
setTracks(nextTracks);
|
||||
setMicOn(true);
|
||||
setCameraOn(true);
|
||||
@@ -66,18 +88,20 @@ export function PreJoinScreen({ onContinue }: { onContinue: (tracks: LocalTrack[
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleMic() {
|
||||
if (!audioTrack) return;
|
||||
if (micOn) await audioTrack.mute();
|
||||
else await audioTrack.unmute();
|
||||
setMicOn(!micOn);
|
||||
function toggleMic() {
|
||||
const track = tracksRef.current.find((candidate) => candidate.kind === "audio");
|
||||
if (!track) return;
|
||||
const next = !micOn;
|
||||
track.enabled = next;
|
||||
setMicOn(next);
|
||||
}
|
||||
|
||||
async function toggleCamera() {
|
||||
if (!videoTrack) return;
|
||||
if (cameraOn) await videoTrack.mute();
|
||||
else await videoTrack.unmute();
|
||||
setCameraOn(!cameraOn);
|
||||
function toggleCamera() {
|
||||
const track = tracksRef.current.find((candidate) => candidate.kind === "video");
|
||||
if (!track) return;
|
||||
const next = !cameraOn;
|
||||
track.enabled = next;
|
||||
setCameraOn(next);
|
||||
}
|
||||
|
||||
function testSpeaker() {
|
||||
@@ -97,7 +121,7 @@ export function PreJoinScreen({ onContinue }: { onContinue: (tracks: LocalTrack[
|
||||
|
||||
function continueToCall() {
|
||||
handedOffRef.current = true;
|
||||
onContinue(tracks);
|
||||
onContinue(tracksRef.current);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -141,10 +165,10 @@ export function PreJoinScreen({ onContinue }: { onContinue: (tracks: LocalTrack[
|
||||
{requestingDevices ? "Browser-Anfrage ist offen …" : tracks.length > 0 ? "Kamera & Mikrofon erneut prüfen" : "Kamera & Mikrofon aktivieren"}
|
||||
</button>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button type="button" onClick={() => void toggleMic()} disabled={!audioTrack} className={`rounded-2xl px-4 py-3 text-xs transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] disabled:opacity-40 ${micOn ? "bg-white/10 hover:bg-white/15" : "bg-red-500/85 hover:bg-red-500"}`}>
|
||||
<button type="button" onClick={toggleMic} disabled={!audioTrack} className={`rounded-2xl px-4 py-3 text-xs transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] disabled:opacity-40 ${micOn ? "bg-white/10 hover:bg-white/15" : "bg-red-500/85 hover:bg-red-500"}`}>
|
||||
{micOn ? "Mikrofon aus" : "Mikrofon an"}
|
||||
</button>
|
||||
<button type="button" onClick={() => void toggleCamera()} disabled={!videoTrack} className={`rounded-2xl px-4 py-3 text-xs transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] disabled:opacity-40 ${cameraOn ? "bg-white/10 hover:bg-white/15" : "bg-red-500/85 hover:bg-red-500"}`}>
|
||||
<button type="button" onClick={toggleCamera} disabled={!videoTrack} className={`rounded-2xl px-4 py-3 text-xs transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] disabled:opacity-40 ${cameraOn ? "bg-white/10 hover:bg-white/15" : "bg-red-500/85 hover:bg-red-500"}`}>
|
||||
{cameraOn ? "Kamera aus" : "Kamera an"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user