167 lines
5.0 KiB
JavaScript
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>
|
|
);
|
|
}
|