import { useState, useCallback, useRef, useEffect } from "react"; import { Room, RoomEvent, VideoPresets, createLocalVideoTrack, createLocalAudioTrack, } from "livekit-client"; const LIVEKIT_URL = "wss://live.us1.sociowire.com"; const LIVEKIT_API = "https://live.us1.sociowire.com"; /** * Hook for LiveKit-based live streaming * Much more reliable than raw WebRTC - handles TURN/ICE automatically */ export function useLiveKit({ roomName, userID, username, token: authToken }) { const [isLive, setIsLive] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [error, setError] = useState(null); const [viewerCount, setViewerCount] = useState(0); const [participants, setParticipants] = useState([]); const roomRef = useRef(null); const localVideoTrackRef = useRef(null); const localAudioTrackRef = useRef(null); // Get LiveKit token from backend const getToken = useCallback(async (isHost = true) => { try { const res = await fetch(`${LIVEKIT_API}/api/livekit/token`, { method: "POST", headers: { "Content-Type": "application/json", ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}), }, body: JSON.stringify({ room: roomName, identity: userID, name: username, canPublish: isHost, canSubscribe: true, }), }); if (!res.ok) { throw new Error("Failed to get LiveKit token"); } const data = await res.json(); return data.token; } catch (err) { console.error("[LiveKit] Token error:", err); throw err; } }, [roomName, userID, username, authToken]); // Start broadcasting const startBroadcast = useCallback(async (videoTrack, audioEnabled = true) => { if (isConnecting || isLive) return; setIsConnecting(true); setError(null); try { // Get token const token = await getToken(true); // Create room const room = new Room({ adaptiveStream: true, dynacast: true, videoCaptureDefaults: { resolution: VideoPresets.h720.resolution, }, }); // Set up event handlers room.on(RoomEvent.ParticipantConnected, (participant) => { console.log("[LiveKit] Participant joined:", participant.identity); setParticipants((prev) => [ ...prev.filter((p) => p.identity !== participant.identity), { identity: participant.identity, name: participant.name || participant.identity, }, ]); setViewerCount((prev) => prev + 1); }); room.on(RoomEvent.ParticipantDisconnected, (participant) => { console.log("[LiveKit] Participant left:", participant.identity); setParticipants((prev) => prev.filter((p) => p.identity !== participant.identity) ); setViewerCount((prev) => Math.max(0, prev - 1)); }); room.on(RoomEvent.Disconnected, () => { console.log("[LiveKit] Disconnected from room"); setIsLive(false); }); room.on(RoomEvent.Reconnecting, () => { console.log("[LiveKit] Reconnecting..."); }); room.on(RoomEvent.Reconnected, () => { console.log("[LiveKit] Reconnected"); }); // Connect to room await room.connect(LIVEKIT_URL, token); console.log("[LiveKit] Connected to room:", room.name); // Create and publish video track from provided stream if (videoTrack) { // If a MediaStreamTrack is provided, use it const track = videoTrack instanceof MediaStreamTrack ? videoTrack : videoTrack.getVideoTracks()[0]; if (track) { await room.localParticipant.publishTrack(track, { name: "camera", simulcast: true, videoCodec: "vp8", }); localVideoTrackRef.current = track; } } else { // Create default camera track const videoTrack = await createLocalVideoTrack({ facingMode: "user", resolution: VideoPresets.h720.resolution, }); await room.localParticipant.publishTrack(videoTrack); localVideoTrackRef.current = videoTrack; } // Create and publish audio track if (audioEnabled) { const audioTrack = await createLocalAudioTrack(); await room.localParticipant.publishTrack(audioTrack); localAudioTrackRef.current = audioTrack; } roomRef.current = room; setIsLive(true); console.log("[LiveKit] Broadcasting started"); // Start egress recording try { await fetch(`${LIVEKIT_API}/api/livekit/egress/start`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ room: roomName }), }); console.log("[LiveKit] Egress recording started"); } catch (err) { console.warn("[LiveKit] Failed to start egress:", err); } } catch (err) { console.error("[LiveKit] Failed to start broadcast:", err); setError(err.message || "Failed to start broadcast"); } finally { setIsConnecting(false); } }, [isConnecting, isLive, getToken]); // Stop broadcasting const stopBroadcast = useCallback(async () => { if (roomRef.current) { // Unpublish tracks if (localVideoTrackRef.current) { roomRef.current.localParticipant.unpublishTrack(localVideoTrackRef.current); localVideoTrackRef.current.stop(); localVideoTrackRef.current = null; } if (localAudioTrackRef.current) { roomRef.current.localParticipant.unpublishTrack(localAudioTrackRef.current); localAudioTrackRef.current.stop(); localAudioTrackRef.current = null; } // Disconnect from room await roomRef.current.disconnect(); roomRef.current = null; } setIsLive(false); setViewerCount(0); setParticipants([]); console.log("[LiveKit] Broadcast stopped"); }, []); // Toggle audio const toggleAudio = useCallback((muted) => { if (localAudioTrackRef.current) { localAudioTrackRef.current.enabled = !muted; } }, []); // Toggle video const toggleVideo = useCallback((disabled) => { if (localVideoTrackRef.current) { localVideoTrackRef.current.enabled = !disabled; } }, []); // Cleanup on unmount useEffect(() => { return () => { if (roomRef.current) { roomRef.current.disconnect(); } }; }, []); return { isLive, isConnecting, error, viewerCount, participants, startBroadcast, stopBroadcast, toggleAudio, toggleVideo, room: roomRef.current, }; } export default useLiveKit;