sw-fe/src/components/Live/LiveKitViewerModal.jsx

167 lines
5.0 KiB
JavaScript

import { useCallback, useEffect, useRef, useState } from "react";
import { Room, RoomEvent } from "livekit-client";
import { useAuth } from "../../auth/AuthContext";
import "./Live.css";
const LIVEKIT_URL = "wss://live.us1.sociowire.com";
const LIVEKIT_API = "https://live.us1.sociowire.com";
function roomNameFromPost(post) {
const raw = (post?.embed_url || post?.video_url || "").toString().trim();
if (raw.startsWith("livekit://")) {
return raw.slice("livekit://".length);
}
return raw;
}
export default function LiveKitViewerModal({ post, onClose }) {
const { username, token: authToken } = useAuth();
const userID = username || `viewer_${Date.now()}`;
const roomName = roomNameFromPost(post);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState(null);
const [viewerCount, setViewerCount] = useState(0);
const [videoTrack, setVideoTrack] = useState(null);
const roomRef = useRef(null);
const videoRef = useRef(null);
const getToken = useCallback(async () => {
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 || userID,
canPublish: false,
canSubscribe: true,
}),
});
if (!res.ok) {
throw new Error("Failed to get LiveKit token");
}
const data = await res.json();
return data.token;
}, [authToken, roomName, userID, username]);
useEffect(() => {
if (!roomName) {
setError("Missing room name");
return;
}
let mounted = true;
const connect = async () => {
setIsConnecting(true);
setError(null);
try {
const token = await getToken();
if (!mounted) return;
const room = new Room({ adaptiveStream: true, dynacast: true });
roomRef.current = room;
room.on(RoomEvent.TrackSubscribed, (track) => {
if (track.kind === "video") {
setVideoTrack(track);
}
});
room.on(RoomEvent.ParticipantConnected, () => {
setViewerCount((prev) => prev + 1);
});
room.on(RoomEvent.ParticipantDisconnected, () => {
setViewerCount((prev) => Math.max(0, prev - 1));
});
room.on(RoomEvent.Disconnected, () => {
setViewerCount(0);
});
await room.connect(LIVEKIT_URL, token);
setViewerCount(room.participants?.size || 0);
} catch (err) {
if (mounted) {
setError(err.message || "Failed to connect");
}
} finally {
if (mounted) setIsConnecting(false);
}
};
connect();
return () => {
mounted = false;
if (roomRef.current) {
roomRef.current.disconnect();
roomRef.current = null;
}
};
}, [roomName, getToken]);
useEffect(() => {
if (!videoRef.current || !videoTrack) return;
videoTrack.attach(videoRef.current);
return () => {
try { videoTrack.detach(videoRef.current); } catch {}
};
}, [videoTrack]);
const handleClose = useCallback(() => {
if (roomRef.current) {
roomRef.current.disconnect();
roomRef.current = null;
}
onClose?.();
}, [onClose]);
if (!post) return null;
return (
<div className="sw-viewer-modal" onClick={(e) => {
if (e.target === e.currentTarget) handleClose();
}}>
<div className="sw-viewer-container">
<div className="sw-viewer-header">
<div className="sw-viewer-header-left">
<div className="sw-viewer-live-badge">
<span className="sw-viewer-dot" />
<span>LIVE</span>
</div>
<span className="sw-viewer-title">{post?.title || "Live Stream"}</span>
</div>
<div className="sw-viewer-header-right">
<span className="sw-viewer-count">
<i className="fa-solid fa-eye" /> {viewerCount}
</span>
<button type="button" className="sw-viewer-close" onClick={handleClose}>
<i className="fa-solid fa-times" />
</button>
</div>
</div>
<div className="sw-viewer-content">
<div className="sw-viewer-video-area">
{isConnecting ? (
<div className="sw-viewer-video-placeholder">
<i className="fa-solid fa-spinner fa-spin" />
<span>Connecting...</span>
</div>
) : error ? (
<div className="sw-viewer-video-placeholder error">
<i className="fa-solid fa-exclamation-triangle" />
<span>{error}</span>
</div>
) : (
<div className="sw-viewer-main-video">
<video ref={videoRef} autoPlay playsInline controls />
</div>
)}
</div>
</div>
</div>
</div>
);
}