diff --git a/server/package.json b/server/package.json index 4d17751..781962e 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,8 @@ "test": "echo \"Error: no test specified\" && exit 1", "start:http": "NODE_ENV=development nodemon src/http/server.ts", "start:socket": "NODE_ENV=development nodemon src/socket/server.ts", - "start:all": "concurrently \"npm run start:http\" \"npm run start:socket\"", + "start:webrtc": "NODE_ENV=development nodemon src/webrtc/signalingServer.ts", + "start:all": "concurrently \"npm run start:http\" \"npm run start:socket\" \"npm run start:webrtc\"", "list:env": "node -e 'console.log(process.env)'", "build": "tsc", "start": "node dist/http/server.js" diff --git a/server/src/utils/env.ts b/server/src/utils/env.ts index 81575ba..1631cf1 100644 --- a/server/src/utils/env.ts +++ b/server/src/utils/env.ts @@ -1,5 +1,5 @@ import dotenv from "dotenv"; -import path from 'path'; +import path from "path"; const envFile = process.env.NODE_ENV === "production" ? ".env.production" : ".env.development"; dotenv.config({ path: path.resolve(__dirname, "../../", envFile) }); @@ -24,6 +24,7 @@ export const GOOGLE_CLIENT_SECRET = getEnvVar("GOOGLE_CLIENT_SECRET"); export const UI_REDIRECT_URL = getEnvVar("UI_REDIRECT_URL"); export const SECRET_KEY = getEnvVar("SECRET_KEY"); export const GOOGLE_CALLBACK_URL = getEnvVar("GOOGLE_CALLBACK_URL"); +export const WEBRTC_PORT = parseInt(getEnvVar("WEBRTC_PORT")); // Log environment variables in development if (process.env.NODE_ENV === "development") { @@ -38,5 +39,6 @@ if (process.env.NODE_ENV === "development") { UI_REDIRECT_URL, SECRET_KEY, GOOGLE_CALLBACK_URL, + WEBRTC_PORT, }); } diff --git a/server/src/webrtc/signalingServer.ts b/server/src/webrtc/signalingServer.ts new file mode 100644 index 0000000..0aac216 --- /dev/null +++ b/server/src/webrtc/signalingServer.ts @@ -0,0 +1,72 @@ +import express from "express"; +import http from "http"; +import { Server, Socket } from "socket.io"; +import { WEBRTC_PORT } from "../utils/env"; + +const app = express(); +const server = http.createServer(app); +const io = new Server(server, { + cors: { + origin: "*", + methods: ["GET", "POST"], + }, +}); + +type RoomMap = Record>; +const rooms: RoomMap = {}; +const TAG = "SignalingServer"; + +io.on("connection", (socket: Socket) => { + console.log(TAG, "User connected:", socket.id); + + socket.on("join", (roomId: string, email: string) => { + if (!rooms[roomId]) rooms[roomId] = {}; + // if (rooms[roomId].length >= 2) { + // socket.emit("room_full"); + // return; + // } + + rooms[roomId][email] = socket.id; + socket.join(roomId); + console.log(TAG, `${socket.id} joined room ${roomId}`); + console.log(TAG, rooms[roomId]); + + const otherUser = Object.keys(rooms[roomId]).find((id) => id !== email); + if (otherUser) { + console.log(TAG, `${socket.id} found other user ${otherUser}`); + socket.emit("other-user", rooms[roomId][otherUser], otherUser); + io.to(rooms[roomId][otherUser]).emit("user-joined", socket.id, email); + } + + socket.on("offer", ({ to, offer }: { to: string; offer: RTCSessionDescriptionInit }, email: string) => { + console.log(TAG, `${socket.id} sent offer to ${to}`); + io.to(to).emit("offer", { from: socket.id, offer }, email); + }); + + socket.on("answer", ({ to, answer }: { to: string; answer: RTCSessionDescriptionInit }, email: string) => { + console.log(TAG, `${socket.id} sent answer to ${to}`); + io.to(to).emit("answer", { from: socket.id, answer }, email); + }); + + socket.on( + "ice-candidate", + ({ to, candidate }: { to: string; candidate: RTCIceCandidateInit }, email: string) => { + console.log(TAG, `${socket.id} sent ice candidate to ${to}`); + io.to(to).emit("ice-candidate", { from: socket.id, candidate }, email); + } + ); + + socket.on("disconnect", () => { + console.log(TAG, "User disconnected:", socket.id); + const email = Object.keys(rooms[roomId]).find((id) => rooms[roomId][id] === socket.id); + if (email) { + socket.to(rooms[roomId][email]).emit("user-disconnected", email); + delete rooms[roomId][email]; + } + }); + }); +}); + +server.listen(WEBRTC_PORT, () => { + console.log(`Signaling server listening on http://localhost:${WEBRTC_PORT}`); +}); diff --git a/ui/package-lock.json b/ui/package-lock.json index 00f1a9b..e15bed0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -13,10 +13,12 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "@types/js-cookie": "^3.0.6", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "dotenv": "^16.4.7", "emoji-mart": "^5.6.0", + "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -3762,6 +3764,12 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -10304,6 +10312,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/ui/package.json b/ui/package.json index 78023d0..0e868ef 100644 --- a/ui/package.json +++ b/ui/package.json @@ -8,10 +8,12 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "@types/js-cookie": "^3.0.6", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "dotenv": "^16.4.7", "emoji-mart": "^5.6.0", + "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/ui/src/PageRoute.tsx b/ui/src/PageRoute.tsx index 848c42c..87a7e93 100644 --- a/ui/src/PageRoute.tsx +++ b/ui/src/PageRoute.tsx @@ -13,12 +13,13 @@ import ProfileV2 from "./pages/ProfileV2/Page"; import { ProtectedRoute } from "./components/ProtectedRoute"; import { AuthProvider } from "./contexts/Auth"; import StreamingRoomV2 from "./pages/StreamingRoomV2/Page"; +import Cookies from "js-cookie"; const PageRoute: FC = () => { const [initialCompenent, setInitialComponent] = useState(); useEffect(() => { - const currentToken = localStorage.getItem("authToken"); + const currentToken = Cookies.get("authToken"); if (currentToken) { setInitialComponent(); } else { diff --git a/ui/src/api/common.ts b/ui/src/api/common.ts index 6442e2e..c1f91c7 100644 --- a/ui/src/api/common.ts +++ b/ui/src/api/common.ts @@ -1,10 +1,12 @@ import { HTTP_SERVER_URL } from "../utils/env"; +import Cookies from "js-cookie"; + export const baseUrl = HTTP_SERVER_URL + "/"; export const getHeaders = () => { - const token = localStorage.getItem("authToken"); + const cookie = Cookies.get("authToken"); return { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${cookie}`, "Content-Type": "application/json", }; }; diff --git a/ui/src/components/ProtectedRoute.tsx b/ui/src/components/ProtectedRoute.tsx index 6ccb8f0..2cf3d40 100644 --- a/ui/src/components/ProtectedRoute.tsx +++ b/ui/src/components/ProtectedRoute.tsx @@ -1,6 +1,7 @@ import React, { FC, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useAuth } from "../contexts/Auth"; +import Cookies from "js-cookie"; interface ProtectedRouteProps { children: React.ReactNode; @@ -11,7 +12,7 @@ export const ProtectedRoute: FC = ({ children }) => { const { user } = useAuth(); useEffect(() => { - const token = localStorage.getItem("authToken"); + const token = Cookies.get("authToken"); if (!token || !user) { navigate("/login", { replace: true }); } diff --git a/ui/src/contexts/Auth.tsx b/ui/src/contexts/Auth.tsx index 9c5ba96..539af55 100644 --- a/ui/src/contexts/Auth.tsx +++ b/ui/src/contexts/Auth.tsx @@ -1,6 +1,7 @@ import React, { createContext, useContext, useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { getProfile } from "../api/profile"; +import Cookies from "js-cookie"; interface User { email: string; @@ -24,7 +25,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { useEffect(() => { const validateToken = async () => { - const token = localStorage.getItem("authToken"); + const token = Cookies.get("authToken"); if (token) { try { const profile = await getProfile(); @@ -35,7 +36,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { }); } catch (error) { // Token is invalid or expired - localStorage.removeItem("authToken"); + Cookies.remove("authToken"); setUser(null); navigate("/login", { replace: true }); } diff --git a/ui/src/pages/Home.tsx b/ui/src/pages/Home.tsx index 7c35fb9..d93303b 100644 --- a/ui/src/pages/Home.tsx +++ b/ui/src/pages/Home.tsx @@ -14,6 +14,7 @@ import { import { getStreamingRoomsList, StreamingRoom } from "../api/streamingRoom"; import { StreamingRoomListItem } from "../components/StreamingRoomListItem"; import AuthContext from "../contexts/Auth"; +import Cookies from "js-cookie"; enum Section { StreamingRooms, @@ -29,7 +30,7 @@ const Home: FC = () => { const { setUser, user } = useContext(AuthContext); const checkOldTokenValidity = (newToken: string | null): boolean => { - const currentToken = localStorage.getItem("authToken"); + const currentToken = Cookies.get("authToken"); console.log("currentToken", currentToken); console.log("token", newToken); if (newToken == null && currentToken) { @@ -51,14 +52,14 @@ const Home: FC = () => { const checkNewTokenValidity = (name: string | null, email: string | null, token: string | null) => { if (!name || !email || !token) { - const token = localStorage.getItem("authToken"); + const token = Cookies.get("authToken"); if (!user?.email || !user?.name || !token) { navigation("/login"); return; } return; } - localStorage.setItem("authToken", token ?? ""); + Cookies.set("authToken", token ?? "", { expires: 1, path: "/", secure: true }); changeSection(Section.StreamingRooms); setUser({ email: email ?? "", name: name ?? "", picture: "" }); }; diff --git a/ui/src/pages/HomeV2/Page.tsx b/ui/src/pages/HomeV2/Page.tsx index 0669881..d429848 100644 --- a/ui/src/pages/HomeV2/Page.tsx +++ b/ui/src/pages/HomeV2/Page.tsx @@ -19,6 +19,7 @@ import { import AuthContext from "../../contexts/Auth"; import HeartIcon from "./svg/HeartIcon"; import FriendsIcon from "./svg/FriendsIcon"; +import Cookies from "js-cookie"; function getInitials(name: string): string { return name @@ -80,7 +81,7 @@ export const HomeV2: React.FC = () => { const token = params.get("token"); const checkOldTokenValidity = (newToken: string | null): boolean => { - const currentToken = localStorage.getItem("authToken"); + const currentToken = Cookies.get("authToken"); if (newToken == null && currentToken) { getProfile() .then((profileResp) => { @@ -96,14 +97,14 @@ export const HomeV2: React.FC = () => { const checkNewTokenValidity = (name: string | null, email: string | null, token: string | null) => { if (!name || !email || !token) { - const token = localStorage.getItem("authToken"); + const token = Cookies.get("authToken"); if (!user?.email || !user?.name || !token) { navigation("/login"); return; } return; } - localStorage.setItem("authToken", token ?? ""); + Cookies.set("authToken", token ?? "", { expires: 1, path: "/", secure: true }); setUser({ email: email ?? "", name: name ?? "", picture: "" }); }; diff --git a/ui/src/pages/Profile.tsx b/ui/src/pages/Profile.tsx index eed5700..c210b8a 100644 --- a/ui/src/pages/Profile.tsx +++ b/ui/src/pages/Profile.tsx @@ -5,6 +5,7 @@ import { useLocation, useNavigate } from "react-router-dom"; import AuthContext from "../contexts/Auth"; import { useContext } from "react"; import { FriendshipStatus } from "../api/profile"; +import Cookies from "js-cookie"; export const Profile: FC = () => { const [user, setUser] = useState(null); @@ -45,7 +46,7 @@ export const Profile: FC = () => { }; const handleLogout = () => { - localStorage.removeItem("authToken"); + Cookies.remove("authToken"); setAuthUser(null); navigate("/login"); }; diff --git a/ui/src/pages/ProfileV2/components/LogOut.tsx b/ui/src/pages/ProfileV2/components/LogOut.tsx index 564fed8..408e582 100644 --- a/ui/src/pages/ProfileV2/components/LogOut.tsx +++ b/ui/src/pages/ProfileV2/components/LogOut.tsx @@ -1,6 +1,7 @@ import React, { FC } from "react"; import { useNavigate } from "react-router-dom"; import { useAuth } from "../../../contexts/Auth"; +import Cookies from "js-cookie"; export const LogOut: FC = () => { const navigate = useNavigate(); @@ -8,7 +9,7 @@ export const LogOut: FC = () => { const { setUser } = useAuth(); const onLogout = () => { - localStorage.removeItem("authToken"); + Cookies.remove("authToken"); setUser(null); document.cookie.split(";").forEach(function (c) { document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); diff --git a/ui/src/pages/StreamingRoomV2/Page.tsx b/ui/src/pages/StreamingRoomV2/Page.tsx index 2f2ee21..e4a477b 100644 --- a/ui/src/pages/StreamingRoomV2/Page.tsx +++ b/ui/src/pages/StreamingRoomV2/Page.tsx @@ -16,6 +16,7 @@ import { } from "../../api/streamingRoom"; import AuthContext from "../../contexts/Auth"; import { VideoUrlModal } from "./components/VideoUrlModal"; +import VideoCall from "./components/VideoCall"; interface Room { id: string; @@ -59,6 +60,8 @@ const StreamingRoomV2: React.FC = () => { const [newVideoUrl, setNewVideoUrl] = useState(""); const [updateUrlError, setUpdateUrlError] = useState(null); const [videoTitle, setVideoTitle] = useState(""); + const [showVideoCall, setShowVideoCall] = useState(false); + const [leftTab, setLeftTab] = useState<"friends" | "chat">("friends"); const viewOnly = useMemo(() => { return room?.createdBy !== user?.email; @@ -359,34 +362,76 @@ const StreamingRoomV2: React.FC = () => {
-
- + {/* Left: Tabbed Friends/Chat panel */} +
+
+ + +
+
+ {leftTab === "friends" ? ( + + ) : ( + + )} +
-
- - setIsUrlModalOpen(true)} - onLeave={handleLeaveRoom} - onDelete={!viewOnly ? handleDeleteRoom : undefined} - videoTitle={room.videoTitle} - /> + {/* Center: Main video area and info bar (unchanged) */} +
+
+
+ +
+
+
+ setIsUrlModalOpen(true)} + onLeave={handleLeaveRoom} + onDelete={!viewOnly ? handleDeleteRoom : undefined} + videoTitle={room.videoTitle} + roomId={room.id} + onToggleVideoCall={() => setShowVideoCall(!showVideoCall)} + isVideoCallActive={showVideoCall} + /> +
{ @@ -399,7 +444,16 @@ const StreamingRoomV2: React.FC = () => { error={updateUrlError} />
- + {/* Right: VideoCall panel */} +
+ {showVideoCall ? ( + setShowVideoCall(false)} /> + ) : ( +
+ Click "Start Video Call" to begin +
+ )} +
); diff --git a/ui/src/pages/StreamingRoomV2/components/RoomInfoBar.tsx b/ui/src/pages/StreamingRoomV2/components/RoomInfoBar.tsx index e752dfe..2f49da0 100644 --- a/ui/src/pages/StreamingRoomV2/components/RoomInfoBar.tsx +++ b/ui/src/pages/StreamingRoomV2/components/RoomInfoBar.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { FiUsers, FiClock, FiVideo, FiChevronDown, FiChevronUp } from "react-icons/fi"; +import VideoCall from "./VideoCall"; interface RoomInfoBarProps { roomName: string; @@ -10,6 +11,9 @@ interface RoomInfoBarProps { onLeave: () => void; onDelete?: () => void; videoTitle?: string; + roomId: string; + onToggleVideoCall: () => void; + isVideoCallActive: boolean; } const RoomInfoBar: React.FC = ({ @@ -21,8 +25,17 @@ const RoomInfoBar: React.FC = ({ onLeave, onDelete, videoTitle, + roomId, + onToggleVideoCall, + isVideoCallActive, }) => { const [expanded, setExpanded] = useState(false); + const [videoCall, setVideoCall] = useState(false); + + const handleVideoCall = () => { + setVideoCall(true); + }; + return (
{/* Mobile: Toggle bar */} @@ -121,8 +134,20 @@ const RoomInfoBar: React.FC = ({ Update Video )} + +
+ {videoCall && setVideoCall(false)} />}
); }; diff --git a/ui/src/pages/StreamingRoomV2/components/VideoCall.tsx b/ui/src/pages/StreamingRoomV2/components/VideoCall.tsx new file mode 100644 index 0000000..2903c28 --- /dev/null +++ b/ui/src/pages/StreamingRoomV2/components/VideoCall.tsx @@ -0,0 +1,174 @@ +import React, { useEffect, useRef, useState } from "react"; +import io, { Socket } from "socket.io-client"; +import { useAuth } from "../../../contexts/Auth"; +import { WEBRTC_SERVER_URL } from "../../../utils/env"; + +interface VideoCallProps { + roomId: string; + onClose: () => void; +} + +const TAG = "VideoCall"; + +const VideoCall: React.FC = ({ roomId, onClose }) => { + const localVideoRef = useRef(null); + const remoteVideoRef = useRef(null); + const localStreamRef = useRef(null); + const peerRef = useRef(null); + const socketRef = useRef(null); + const { user } = useAuth(); + const [permission, setPermission] = useState<"pending" | "granted" | "denied">("pending"); + const [error, setError] = useState(null); + const [status, setStatus] = useState<"idle" | "connecting" | "in-call" | "ended">("idle"); + + useEffect(() => { + navigator.mediaDevices + .getUserMedia({ video: true, audio: true }) + .then((stream) => { + setPermission("granted"); + localStreamRef.current = stream; + if (localVideoRef.current) { + localVideoRef.current.srcObject = stream; + } + setStatus("connecting"); + startSignaling(stream); + }) + .catch((err) => { + setPermission("denied"); + setError("Camera/mic permission denied or not available."); + }); + return () => { + cleanup(); + }; + }, []); + + const startSignaling = (stream: MediaStream) => { + console.log(TAG, "startSignaling"); + const socket = io(WEBRTC_SERVER_URL); + socketRef.current = socket; + socket.emit("join", roomId, user?.email ?? ""); + + socket.on("other-user", async (userId: string, email: string) => { + console.log(TAG, "other-user", userId, email); + await createOffer(userId); + }); + + socket.on("user-joined", async (userId: string, email: string) => { + console.log(TAG, "user-joined", userId, email); + // Another user joined after you + }); + + socket.on("offer", async ({ from, offer }, email: string) => { + console.log(TAG, "offer", from, offer, email); + await createAnswer(from, offer); + }); + + socket.on("answer", ({ from, answer }, email: string) => { + console.log(TAG, "answer", from, answer, email); + if (peerRef.current && peerRef.current.signalingState !== "stable") { + peerRef.current.setRemoteDescription(new RTCSessionDescription(answer)); + setStatus("in-call"); + } + }); + + socket.on("ice-candidate", ({ from, candidate }, email: string) => { + console.log(TAG, "ice-candidate", from, candidate, email); + peerRef.current?.addIceCandidate(new RTCIceCandidate(candidate)); + }); + + socket.on("user-disconnected", (email: string) => { + console.log(TAG, "user-disconnected", email); + setStatus("ended"); + if (remoteVideoRef.current) remoteVideoRef.current.srcObject = null; + peerRef.current?.close(); + peerRef.current = null; + }); + }; + + const createPeerConnection = (targetSocketId: string) => { + console.log(TAG, "createPeerConnection", targetSocketId); + const peer = new RTCPeerConnection({ + iceServers: [{ urls: "stun:stun.l.google.com:19302" }], + }); + peer.onicecandidate = (e) => { + console.log(TAG, "onicecandidate", e); + if (e.candidate) { + socketRef.current?.emit( + "ice-candidate", + { + to: targetSocketId, + candidate: e.candidate, + }, + user?.email ?? "" + ); + } + }; + peer.ontrack = (e) => { + console.log(TAG, "ontrack", e); + if (remoteVideoRef.current) { + remoteVideoRef.current.srcObject = e.streams[0]; + } + }; + localStreamRef.current?.getTracks().forEach((track) => { + console.log(TAG, "addTrack", track); + peer.addTrack(track, localStreamRef.current!); + }); + peerRef.current = peer; + return peer; + }; + + const createOffer = async (targetId: string) => { + console.log(TAG, "createOffer", targetId); + const peer = createPeerConnection(targetId); + const offer = await peer.createOffer(); + await peer.setLocalDescription(offer); + socketRef.current?.emit("offer", { to: targetId, offer }, user?.email ?? ""); + }; + + const createAnswer = async (from: string, offer: RTCSessionDescriptionInit) => { + console.log(TAG, "createAnswer", from, offer); + const peer = createPeerConnection(from); + await peer.setRemoteDescription(new RTCSessionDescription(offer)); + const answer = await peer.createAnswer(); + await peer.setLocalDescription(answer); + socketRef.current?.emit("answer", { to: from, answer }, user?.email ?? ""); + setStatus("in-call"); + }; + + const cleanup = () => { + console.log(TAG, "cleanup"); + peerRef.current?.close(); + peerRef.current = null; + socketRef.current?.disconnect(); + socketRef.current = null; + localStreamRef.current?.getTracks().forEach((track) => track.stop()); + if (remoteVideoRef.current) remoteVideoRef.current.srcObject = null; + if (localVideoRef.current) localVideoRef.current.srcObject = null; + }; + + return ( +
+
+
+
+
+
+ ); +}; + +export default VideoCall; diff --git a/ui/src/pages/StreamingRoomV2/components/VideoPlayerSection.tsx b/ui/src/pages/StreamingRoomV2/components/VideoPlayerSection.tsx index 36d9f26..b22cccb 100644 --- a/ui/src/pages/StreamingRoomV2/components/VideoPlayerSection.tsx +++ b/ui/src/pages/StreamingRoomV2/components/VideoPlayerSection.tsx @@ -17,7 +17,7 @@ interface VideoPlayerSectionProps { const VideoPlayerSection = forwardRef( ({ room, isPlaying, onPlay, onPause, onSeek, onProgress, viewOnly }, ref) => { return ( -
+
{room.videoUrl ? ( diff --git a/ui/src/utils/env.ts b/ui/src/utils/env.ts index 8cec029..3627c04 100644 --- a/ui/src/utils/env.ts +++ b/ui/src/utils/env.ts @@ -9,12 +9,14 @@ export const getEnvVar = (key: string): string => { export const SOCKET_SERVER_URL = getEnvVar("REACT_APP_SOCKET_SERVER_URL"); export const HTTP_SERVER_URL = getEnvVar("REACT_APP_HTTP_SERVER_URL"); +export const WEBRTC_SERVER_URL = getEnvVar("REACT_APP_WEBRTC_SERVER_URL"); // Log environment variables in development if (process.env.NODE_ENV === "development") { console.log("Environment variables loaded:", { SOCKET_SERVER_URL, HTTP_SERVER_URL, + WEBRTC_SERVER_URL, allEnv: process.env, }); }