sw-fe/src/components/Live/useLiveKit.js

235 lines
6.7 KiB
JavaScript

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;