Fix camera switch using useDualCamera hook
- Use useDualCamera for device enumeration and reliable switching - Auto-update LiveKit video track when camera swaps during live - Only show switch button if device has 2 cameras - Better camera unavailable state handling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
870921a482
commit
694a0aaaa2
|
|
@ -1,5 +1,6 @@
|
|||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { useLiveKit } from "./useLiveKit";
|
||||
import { useDualCamera } from "./useDualCamera";
|
||||
import { useAuth } from "../../auth/AuthContext";
|
||||
import "./Live.css";
|
||||
|
||||
|
|
@ -16,11 +17,8 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
|||
|
||||
const [roomName] = useState(() => `live_${userID}_${Date.now()}`);
|
||||
const [title, setTitle] = useState("");
|
||||
const [localStream, setLocalStream] = useState(null);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [livePostId, setLivePostId] = useState(null);
|
||||
const [facingMode, setFacingMode] = useState("user");
|
||||
const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
|
||||
|
||||
// Timer state
|
||||
const [liveStartTime, setLiveStartTime] = useState(null);
|
||||
|
|
@ -47,10 +45,23 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
|||
|
||||
const videoRef = useRef(null);
|
||||
|
||||
// Camera hook - handles device enumeration and swapping
|
||||
const {
|
||||
mainStream,
|
||||
isSwapped,
|
||||
error: cameraError,
|
||||
isStarting: cameraStarting,
|
||||
isSwapping,
|
||||
hasDualCameras,
|
||||
startCameras,
|
||||
swapCameras,
|
||||
stopCameras,
|
||||
} = useDualCamera();
|
||||
|
||||
const {
|
||||
isLive,
|
||||
isConnecting,
|
||||
error,
|
||||
error: liveKitError,
|
||||
viewerCount,
|
||||
startBroadcast,
|
||||
stopBroadcast,
|
||||
|
|
@ -62,37 +73,22 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
|||
token: authToken,
|
||||
});
|
||||
|
||||
const error = cameraError || liveKitError;
|
||||
|
||||
// Start camera on mount
|
||||
useEffect(() => {
|
||||
const startCamera = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: "user", width: 1280, height: 720 },
|
||||
audio: true,
|
||||
});
|
||||
setLocalStream(stream);
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = stream;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[LiveKit] Camera error:", err);
|
||||
}
|
||||
};
|
||||
startCamera();
|
||||
|
||||
startCameras();
|
||||
return () => {
|
||||
if (localStream) {
|
||||
localStream.getTracks().forEach((t) => t.stop());
|
||||
}
|
||||
stopCameras();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update video element when stream changes
|
||||
useEffect(() => {
|
||||
if (videoRef.current && localStream) {
|
||||
videoRef.current.srcObject = localStream;
|
||||
if (videoRef.current && mainStream) {
|
||||
videoRef.current.srcObject = mainStream;
|
||||
}
|
||||
}, [localStream]);
|
||||
}, [mainStream]);
|
||||
|
||||
// Timer effect - update every second when live
|
||||
useEffect(() => {
|
||||
|
|
@ -119,45 +115,23 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
|||
|
||||
// Switch between front and back camera
|
||||
const switchCamera = useCallback(async () => {
|
||||
if (isSwitchingCamera) return;
|
||||
setIsSwitchingCamera(true);
|
||||
if (isSwapping || !hasDualCameras) return;
|
||||
|
||||
try {
|
||||
const newFacingMode = facingMode === "user" ? "environment" : "user";
|
||||
await swapCameras();
|
||||
|
||||
// Get new stream with switched camera
|
||||
const newStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: newFacingMode, width: 1280, height: 720 },
|
||||
audio: true,
|
||||
});
|
||||
// If live, replace the video track in LiveKit after swap
|
||||
// Note: mainStream will be updated by useDualCamera, we need to wait for it
|
||||
}, [isSwapping, hasDualCameras, swapCameras]);
|
||||
|
||||
// Stop old video tracks only (keep audio reference for LiveKit)
|
||||
if (localStream) {
|
||||
localStream.getVideoTracks().forEach((t) => t.stop());
|
||||
// Update LiveKit track when mainStream changes during live
|
||||
useEffect(() => {
|
||||
if (isLive && mainStream) {
|
||||
const newVideoTrack = mainStream.getVideoTracks()[0];
|
||||
if (newVideoTrack) {
|
||||
replaceVideoTrack(newVideoTrack);
|
||||
}
|
||||
|
||||
// If live, replace the video track in LiveKit
|
||||
if (isLive) {
|
||||
const newVideoTrack = newStream.getVideoTracks()[0];
|
||||
await replaceVideoTrack(newVideoTrack);
|
||||
}
|
||||
|
||||
// Update state
|
||||
setLocalStream(newStream);
|
||||
setFacingMode(newFacingMode);
|
||||
|
||||
// Update video element
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = newStream;
|
||||
}
|
||||
|
||||
console.log("[LiveKit] Switched to", newFacingMode, "camera");
|
||||
} catch (err) {
|
||||
console.error("[LiveKit] Failed to switch camera:", err);
|
||||
} finally {
|
||||
setIsSwitchingCamera(false);
|
||||
}
|
||||
}, [facingMode, localStream, isSwitchingCamera, isLive, replaceVideoTrack]);
|
||||
}, [mainStream, isLive, replaceVideoTrack]);
|
||||
|
||||
// Add a comment
|
||||
const handleAddComment = useCallback(() => {
|
||||
|
|
@ -183,13 +157,13 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
|||
alert("Please enter a title");
|
||||
return;
|
||||
}
|
||||
if (!localStream) {
|
||||
if (!mainStream) {
|
||||
alert("Camera not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get video track from stream
|
||||
const videoTrack = localStream.getVideoTracks()[0];
|
||||
const videoTrack = mainStream.getVideoTracks()[0];
|
||||
await startBroadcast(videoTrack, true);
|
||||
|
||||
// Start timer
|
||||
|
|
@ -238,13 +212,11 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
|||
console.error("[LiveKit] Failed to create live post:", err);
|
||||
alert("Live started, but we couldn't create the live post.");
|
||||
}
|
||||
}, [title, localStream, startBroadcast, roomName, liveCoords, onStreamCreated, authToken, username]);
|
||||
}, [title, mainStream, startBroadcast, roomName, liveCoords, onStreamCreated, authToken, username]);
|
||||
|
||||
const handleEndStream = useCallback(async (save) => {
|
||||
await stopBroadcast();
|
||||
if (localStream) {
|
||||
localStream.getTracks().forEach((t) => t.stop());
|
||||
}
|
||||
stopCameras();
|
||||
|
||||
// Stop egress recording and get video key
|
||||
let videoKey = "";
|
||||
|
|
@ -301,13 +273,11 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
|||
}
|
||||
}
|
||||
onClose?.();
|
||||
}, [stopBroadcast, localStream, onClose, livePostId, authToken, roomName, comments]);
|
||||
}, [stopBroadcast, stopCameras, onClose, livePostId, authToken, roomName, comments]);
|
||||
|
||||
const handleBackdropClick = (e) => {
|
||||
if (e.target === e.currentTarget && !isLive) {
|
||||
if (localStream) {
|
||||
localStream.getTracks().forEach((t) => t.stop());
|
||||
}
|
||||
stopCameras();
|
||||
onClose?.();
|
||||
}
|
||||
};
|
||||
|
|
@ -340,9 +310,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
|||
if (isLive) {
|
||||
setShowConfirm(true);
|
||||
} else {
|
||||
if (localStream) {
|
||||
localStream.getTracks().forEach((t) => t.stop());
|
||||
}
|
||||
stopCameras();
|
||||
onClose?.();
|
||||
}
|
||||
}}
|
||||
|
|
@ -356,7 +324,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
|||
{/* Video preview */}
|
||||
<div className="sw-broadcast-video-area">
|
||||
<div className="sw-broadcast-main-video">
|
||||
{localStream ? (
|
||||
{mainStream ? (
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
|
|
@ -366,8 +334,17 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
|||
/>
|
||||
) : (
|
||||
<div className="sw-broadcast-video-placeholder">
|
||||
{cameraStarting ? (
|
||||
<>
|
||||
<i className="fa-solid fa-spinner fa-spin" />
|
||||
<span>Starting camera...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="fa-solid fa-video-slash" />
|
||||
<span>Camera unavailable</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -380,19 +357,21 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
|||
|
||||
{/* Camera controls overlay */}
|
||||
<div className="sw-broadcast-controls-overlay">
|
||||
{hasDualCameras && (
|
||||
<button
|
||||
type="button"
|
||||
className="sw-broadcast-control-btn"
|
||||
onClick={switchCamera}
|
||||
disabled={isSwitchingCamera}
|
||||
title={facingMode === "user" ? "Switch to back camera" : "Switch to front camera"}
|
||||
disabled={isSwapping}
|
||||
title={isSwapped ? "Switch to front camera" : "Switch to back camera"}
|
||||
>
|
||||
{isSwitchingCamera ? (
|
||||
{isSwapping ? (
|
||||
<i className="fa-solid fa-spinner fa-spin" />
|
||||
) : (
|
||||
<i className="fa-solid fa-camera-rotate" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -496,7 +475,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
|||
type="button"
|
||||
className="sw-broadcast-btn primary"
|
||||
onClick={handleGoLive}
|
||||
disabled={isConnecting || !localStream}
|
||||
disabled={isConnecting || !mainStream}
|
||||
>
|
||||
{isConnecting ? (
|
||||
<>
|
||||
|
|
|
|||
Loading…
Reference in New Issue