235 lines
6.7 KiB
JavaScript
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;
|