diff --git a/src/components/Live/LiveKitBroadcast.jsx b/src/components/Live/LiveKitBroadcast.jsx index 888e520..cc69e39 100644 --- a/src/components/Live/LiveKitBroadcast.jsx +++ b/src/components/Live/LiveKitBroadcast.jsx @@ -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 */}
- {localStream ? ( + {mainStream ? (
@@ -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 ? ( <>