UNPKG

@lakshyashukla/nexora-sdk

Version:

SDK for 1:1 chat and video calling

851 lines (841 loc) 41 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.tsx var src_exports = {}; __export(src_exports, { Chat: () => Chat2, ChatService: () => ChatService, VideoCall: () => VideoCall2, VideoService: () => VideoService }); module.exports = __toCommonJS(src_exports); var import_react4 = __toESM(require("react")); // src/context/AppContext.tsx var import_react = __toESM(require("react")); var ApiKeyError = class extends Error { constructor(message) { super(message); this.name = "ApiKeyError"; } }; var ApiKeyContext = (0, import_react.createContext)({ apiKey: "", isValid: false, feature: null, user: null, verifyApiKey: async (_key) => false, setApiKey: (_key) => { } }); var ApiKeyProvider = ({ children, apiKey: initialApiKey }) => { const [apiKey, setApiKey] = (0, import_react.useState)(initialApiKey); const [isValid, setIsValid] = (0, import_react.useState)(false); const [feature, setFeature] = (0, import_react.useState)(null); const [user, setUser] = (0, import_react.useState)(null); const [error, setError] = (0, import_react.useState)(null); const verifyApiKey = (0, import_react.useCallback)(async (key) => { var _a, _b; try { const res = await fetch("http://localhost:5000/apikey/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key }) }); const data = await res.json(); if (!data.valid) { throw new ApiKeyError("Invalid API key"); } setIsValid(true); setApiKey(key); setFeature((_a = data.feature) != null ? _a : null); setUser((_b = data.user) != null ? _b : null); setError(null); return true; } catch (err) { setIsValid(false); setFeature(null); setUser(null); setError(err instanceof Error ? err : new Error("Failed to verify API key")); return false; } }, []); (0, import_react.useEffect)(() => { if (initialApiKey) { verifyApiKey(initialApiKey); } }, [initialApiKey, verifyApiKey]); if (!isValid && error) { throw error; } return /* @__PURE__ */ import_react.default.createElement( ApiKeyContext.Provider, { value: { apiKey, isValid, feature, user, verifyApiKey, setApiKey } }, children ); }; // src/components/Chat.tsx var import_react2 = __toESM(require("react")); // src/services/ChatService.ts var import_socket = require("socket.io-client"); var ChatService = class { constructor(config) { this.socket = null; this.config = config; this.initialize(); } initialize() { console.log("Initializing chat service with config:", this.config); this.socket = (0, import_socket.io)(this.config.serverUrl); this.socket.on("connect", () => { var _a; console.log("Connected to chat server"); (_a = this.socket) == null ? void 0 : _a.emit("register", this.config.userId); }); this.socket.on("connect_error", (error) => { console.error("Connection error:", error); }); this.socket.on("receive_message", (message) => { var _a, _b; console.log("Message received:", message); (_b = (_a = this.config).onMessageReceived) == null ? void 0 : _b.call(_a, message); }); } async sendMessage(receiverId, content) { var _a; if (!((_a = this.socket) == null ? void 0 : _a.connected)) { console.error("Not connected to chat server"); return false; } try { const message = { id: Math.random().toString(36).substr(2, 9), content, senderId: this.config.userId, receiverId, timestamp: Date.now() }; console.log("Sending message:", message); return new Promise((resolve) => { var _a2; (_a2 = this.socket) == null ? void 0 : _a2.emit("send_message", message, (acknowledgement) => { var _a3, _b; console.log("Message acknowledgement:", acknowledgement); if (acknowledgement == null ? void 0 : acknowledgement.success) { (_b = (_a3 = this.config).onMessageReceived) == null ? void 0 : _b.call(_a3, message); resolve(true); } else { console.error("Failed to send message:", acknowledgement == null ? void 0 : acknowledgement.error); resolve(false); } }); }); } catch (error) { console.error("Error in sendMessage:", error); return false; } } disconnect() { var _a; if ((_a = this.socket) == null ? void 0 : _a.connected) { this.socket.disconnect(); } this.socket = null; } }; // #style-inject:#style-inject function styleInject(css, { insertAt } = {}) { if (!css || typeof document === "undefined") return; const head = document.head || document.getElementsByTagName("head")[0]; const style = document.createElement("style"); style.type = "text/css"; if (insertAt === "top") { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } // src/components/styles.css styleInject('.chat-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 400px;\n border: 1px solid #ddd;\n border-radius: 8px;\n}\n.messages {\n flex: 1;\n padding: 16px;\n overflow-y: auto;\n}\n.message {\n margin: 8px 0;\n padding: 8px 12px;\n border-radius: 12px;\n max-width: 70%;\n}\n.message.sent {\n background-color: #0084ff;\n color: white;\n margin-left: auto;\n}\n.message.received {\n background-color: #f0f0f0;\n margin-right: auto;\n}\n.input-area {\n display: flex;\n padding: 16px;\n border-top: 1px solid #ddd;\n}\n.input-area input {\n flex: 1;\n padding: 8px 12px;\n border: 1px solid #ddd;\n border-radius: 20px;\n margin-right: 8px;\n}\n.input-area button {\n padding: 8px 16px;\n background-color: #0084ff;\n color: white;\n border: none;\n border-radius: 20px;\n cursor: pointer;\n}\n.input-area button:hover {\n background-color: #0073e6;\n}\n.video-call-container {\n width: 100%;\n max-width: 1200px;\n margin: 0 auto;\n}\n.video-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n gap: 16px;\n margin-bottom: 16px;\n}\n.video-wrapper {\n position: relative;\n padding-top: 56.25%;\n border-radius: 8px;\n overflow: hidden;\n}\n.video-wrapper video {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n.video-wrapper span {\n position: absolute;\n bottom: 8px;\n left: 8px;\n background-color: rgba(0, 0, 0, 0.6);\n color: white;\n padding: 4px 8px;\n border-radius: 4px;\n}\n.controls {\n display: flex;\n justify-content: center;\n gap: 16px;\n}\n.controls button {\n padding: 12px 24px;\n border-radius: 24px;\n border: none;\n cursor: pointer;\n font-weight: 500;\n}\n.controls button:first-child {\n background-color: #4caf50;\n color: white;\n}\n.controls button:last-child {\n background-color: #f44336;\n color: white;\n}\n.connection-status {\n display: flex;\n align-items: center;\n padding: 8px;\n background-color: #f5f5f5;\n border-bottom: 1px solid #ddd;\n font-size: 12px;\n}\n.status-indicator {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n margin-right: 8px;\n}\n.status-indicator.connecting {\n background-color: #ffcc00;\n animation: blink 1s infinite;\n}\n.status-indicator.connected {\n background-color: #4caf50;\n}\n.status-indicator.disconnected {\n background-color: #f44336;\n}\n.retry-button {\n margin-left: auto;\n padding: 4px 8px;\n background-color: #2196F3;\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 12px;\n}\n.retry-button:hover {\n background-color: #0b7dda;\n}\n.error-message {\n padding: 8px;\n background-color: #ffebee;\n color: #d32f2f;\n font-size: 12px;\n text-align: center;\n}\n@keyframes blink {\n 0% {\n opacity: 0.5;\n }\n 50% {\n opacity: 1;\n }\n 100% {\n opacity: 0.5;\n }\n}\n.video-call-container {\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 300px;\n background-color: #121212;\n border-radius: 12px;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\n}\n.remote-video-container {\n position: absolute;\n width: 100%;\n height: 100%;\n z-index: 1;\n}\n.remote-video-container.hidden {\n display: none;\n}\n.remote-video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n background-color: #2c2c2c;\n}\n.remote-name {\n position: absolute;\n bottom: 100px;\n left: 20px;\n background-color: rgba(0, 0, 0, 0.5);\n color: white;\n padding: 5px 10px;\n border-radius: 4px;\n font-size: 14px;\n}\n.local-video-container {\n position: absolute;\n right: 20px;\n top: 20px;\n width: 150px;\n height: 200px;\n border-radius: 8px;\n overflow: hidden;\n z-index: 2;\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);\n border: 2px solid rgba(255, 255, 255, 0.1);\n transition: all 0.3s ease;\n}\n.local-video-container.centered {\n position: relative;\n width: 100%;\n height: 100%;\n right: 0;\n top: 0;\n border: none;\n}\n.local-video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n background-color: #2c2c2c;\n transition: all 0.3s ease;\n}\n.local-video.video-disabled {\n background-color: #3a3a3a;\n}\n.video-off-indicator {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n color: white;\n font-size: 14px;\n background-color: rgba(0, 0, 0, 0.6);\n padding: 5px 10px;\n border-radius: 4px;\n}\n.local-name {\n position: absolute;\n bottom: 10px;\n left: 10px;\n background-color: rgba(0, 0, 0, 0.5);\n color: white;\n padding: 3px 6px;\n border-radius: 4px;\n font-size: 12px;\n}\n.call-status-container {\n position: absolute;\n top: 20px;\n left: 20px;\n z-index: 3;\n}\n.call-status {\n padding: 6px 12px;\n border-radius: 20px;\n font-size: 13px;\n font-weight: 500;\n display: flex;\n align-items: center;\n}\n.call-status.connecting {\n background-color: rgba(255, 196, 0, 0.2);\n color: #ffc400;\n}\n.call-status.connected {\n background-color: rgba(76, 175, 80, 0.2);\n color: #4caf50;\n}\n.call-status.ended {\n background-color: rgba(244, 67, 54, 0.2);\n color: #f44336;\n}\n.call-status.connecting::before {\n content: "";\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background-color: #ffc400;\n margin-right: 6px;\n animation: blink 1s infinite;\n}\n.call-status.connected::before {\n content: "";\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background-color: #4caf50;\n margin-right: 6px;\n}\n.call-controls {\n position: absolute;\n bottom: 20px;\n left: 0;\n right: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 3;\n}\n.start-call-button {\n background-color: #4caf50;\n color: white;\n border: none;\n border-radius: 50px;\n padding: 12px 24px;\n font-size: 16px;\n font-weight: 500;\n cursor: pointer;\n display: flex;\n align-items: center;\n transition: all 0.2s ease;\n}\n.start-call-button:hover {\n background-color: #43a047;\n transform: translateY(-2px);\n}\n.start-call-button:active {\n transform: translateY(0);\n}\n.icon-phone::before {\n content: "\\1f4de";\n margin-right: 8px;\n}\n.in-call-controls {\n display: flex;\n gap: 16px;\n background-color: rgba(0, 0, 0, 0.5);\n padding: 12px 24px;\n border-radius: 50px;\n}\n.mute-button,\n.video-button,\n.end-call-button {\n width: 50px;\n height: 50px;\n border-radius: 50%;\n border: none;\n display: flex;\n justify-content: center;\n align-items: center;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.mute-button,\n.video-button {\n background-color: rgba(255, 255, 255, 0.2);\n color: white;\n}\n.mute-button:hover,\n.video-button:hover {\n background-color: rgba(255, 255, 255, 0.3);\n}\n.mute-button.active,\n.video-button.active {\n background-color: #f44336;\n}\n.end-call-button {\n background-color: #f44336;\n color: white;\n}\n.end-call-button:hover {\n background-color: #e53935;\n transform: scale(1.1);\n}\n.calling-controls,\n.incoming-call-controls {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n background-color: rgba(0, 0, 0, 0.5);\n padding: 16px 32px;\n border-radius: 12px;\n}\n.calling-text,\n.incoming-call-text {\n color: white;\n font-size: 16px;\n}\n.incoming-call-buttons {\n display: flex;\n gap: 16px;\n}\n.accept-call-button,\n.reject-call-button {\n border: none;\n border-radius: 50px;\n padding: 10px 20px;\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n display: flex;\n align-items: center;\n transition: all 0.2s ease;\n}\n.accept-call-button {\n background-color: #4caf50;\n color: white;\n}\n.reject-call-button {\n background-color: #f44336;\n color: white;\n}\n.icon-mic::before {\n content: "\\1f3a4";\n}\n.icon-muted::before {\n content: "\\1f507";\n}\n.icon-video::before {\n content: "\\1f4f9";\n}\n.icon-video-off::before {\n content: "\\1f6ab";\n}\n.icon-hangup::before {\n content: "\\1f4f5";\n}\n.icon-accept::before {\n content: "\\2705";\n}\n.icon-reject::before {\n content: "\\274c";\n}\n@keyframes blink {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.5;\n }\n}\n@media (max-width: 768px) {\n .local-video-container {\n width: 100px;\n height: 130px;\n }\n .in-call-controls {\n padding: 10px 16px;\n }\n .mute-button,\n .video-button,\n .end-call-button {\n width: 40px;\n height: 40px;\n }\n}\n.switch-camera-button {\n width: 50px;\n height: 50px;\n border-radius: 50%;\n border: none;\n background-color: rgba(255, 255, 255, 0.2);\n color: white;\n display: flex;\n justify-content: center;\n align-items: center;\n cursor: pointer;\n transition: all 0.2s ease;\n}\n.switch-camera-button:hover {\n background-color: rgba(255, 255, 255, 0.3);\n}\n.icon-switch-camera::before {\n content: "\\1f504";\n}\n.camera-indicator {\n position: absolute;\n bottom: 90px;\n left: 20px;\n background-color: rgba(0, 0, 0, 0.5);\n color: white;\n padding: 5px 10px;\n border-radius: 4px;\n font-size: 12px;\n z-index: 3;\n max-width: 200px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n@media (max-width: 768px) {\n .camera-indicator {\n bottom: 80px;\n font-size: 10px;\n max-width: 150px;\n }\n}\n'); // src/components/Chat.tsx var Chat = ({ userId, threadId, receiverId, serverUrl }) => { const [messages, setMessages] = (0, import_react2.useState)([]); const [inputMessage, setInputMessage] = (0, import_react2.useState)(""); const [chatService, setChatService] = (0, import_react2.useState)(null); const [isSending, setIsSending] = (0, import_react2.useState)(false); const messagesEndRef = (0, import_react2.useRef)(null); (0, import_react2.useEffect)(() => { if (!userId) { console.error("Chat component requires a userId"); return; } const service = new ChatService({ userId, serverUrl, onMessageReceived: (message) => { console.log("Message received in component:", message); setMessages((prev) => { if (prev.some((m) => m.id === message.id)) { return prev; } return [...prev, message]; }); } }); setChatService(service); return () => { service.disconnect(); }; }, [userId, serverUrl]); (0, import_react2.useEffect)(() => { var _a; (_a = messagesEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" }); }, [messages]); const handleSendMessage = async () => { if (!inputMessage.trim() || !chatService || isSending) return; try { setIsSending(true); const sent = await chatService.sendMessage(receiverId, inputMessage.trim()); if (sent) { setInputMessage(""); } else { console.error("Failed to send message"); } } catch (error) { console.error("Error sending message:", error); } finally { setIsSending(false); } }; return /* @__PURE__ */ import_react2.default.createElement("div", { className: "chat-container" }, /* @__PURE__ */ import_react2.default.createElement("div", { className: "messages" }, messages.length === 0 && /* @__PURE__ */ import_react2.default.createElement("div", { className: "no-messages" }, "No messages yet"), messages.map((message) => /* @__PURE__ */ import_react2.default.createElement( "div", { key: message.id, className: `message ${message.senderId === userId ? "sent" : "received"}` }, /* @__PURE__ */ import_react2.default.createElement("p", null, message.content), /* @__PURE__ */ import_react2.default.createElement("small", null, new Date(message.timestamp).toLocaleTimeString()) )), /* @__PURE__ */ import_react2.default.createElement("div", { ref: messagesEndRef })), /* @__PURE__ */ import_react2.default.createElement("div", { className: "input-area" }, /* @__PURE__ */ import_react2.default.createElement( "input", { type: "text", value: inputMessage, onChange: (e) => setInputMessage(e.target.value), onKeyPress: (e) => e.key === "Enter" && handleSendMessage(), placeholder: "Type a message...", disabled: isSending } ), /* @__PURE__ */ import_react2.default.createElement( "button", { onClick: handleSendMessage, disabled: isSending || !inputMessage.trim() }, isSending ? "Sending..." : "Send" ))); }; // src/components/VideoCall.tsx var import_react3 = __toESM(require("react")); var import_socket2 = require("socket.io-client"); var VideoCall = ({ userId, receiverId, serverUrl, userName = "You", receiverName = "User", onCallStarted, onCallEnded, onError }) => { var _a; const [isInCall, setIsInCall] = (0, import_react3.useState)(false); const [isCalling, setIsCalling] = (0, import_react3.useState)(false); const [isReceivingCall, setIsReceivingCall] = (0, import_react3.useState)(false); const [isMuted, setIsMuted] = (0, import_react3.useState)(false); const [isVideoEnabled, setIsVideoEnabled] = (0, import_react3.useState)(true); const [callDuration, setCallDuration] = (0, import_react3.useState)(0); const [socket, setSocket] = (0, import_react3.useState)(null); const [callStatus, setCallStatus] = (0, import_react3.useState)("idle"); const [videoDevices, setVideoDevices] = (0, import_react3.useState)([]); const [currentVideoDeviceIndex, setCurrentVideoDeviceIndex] = (0, import_react3.useState)(0); const localVideoRef = (0, import_react3.useRef)(null); const remoteVideoRef = (0, import_react3.useRef)(null); const peerConnection = (0, import_react3.useRef)(null); const localStream = (0, import_react3.useRef)(null); const timerRef = (0, import_react3.useRef)(null); (0, import_react3.useEffect)(() => { console.log("Initializing socket with server URL:", serverUrl); const newSocket = (0, import_socket2.io)(serverUrl, { query: { userId }, // Make sure userId is passed in query transports: ["websocket", "polling"], reconnection: true, reconnectionAttempts: 5 }); newSocket.on("connect", () => { console.log("Socket connected with ID:", newSocket.id); newSocket.emit("register", userId); }); newSocket.on("connect_error", (error) => { console.error("Socket connection error:", error); onError == null ? void 0 : onError(new Error(`Failed to connect to video call server: ${error.message}`)); }); setSocket(newSocket); return () => { newSocket.disconnect(); }; }, [serverUrl, userId, onError]); (0, import_react3.useEffect)(() => { if (isInCall && callStatus === "connected") { timerRef.current = setInterval(() => { setCallDuration((prev) => prev + 1); }, 1e3); } else { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } if (callStatus === "idle") { setCallDuration(0); } } return () => { if (timerRef.current) { clearInterval(timerRef.current); } }; }, [isInCall, callStatus]); (0, import_react3.useEffect)(() => { if (!socket) return; console.log("Setting up video call signal handlers"); socket.on("incoming-call", ({ from }) => { console.log("Incoming call from:", from); if (from === receiverId) { setIsReceivingCall(true); } }); socket.on("offer", async ({ from, offer }) => { console.log("Received offer from:", from); if (from === receiverId) { setCallStatus("connecting"); await handleOffer(offer); } }); socket.on("answer", async ({ from, answer }) => { console.log("Received answer from:", from); if (from !== receiverId) return; if (!peerConnection.current) { console.error("No peer connection available"); return; } try { await peerConnection.current.setRemoteDescription(new RTCSessionDescription(answer)); setCallStatus("connected"); setIsInCall(true); setIsCalling(false); onCallStarted == null ? void 0 : onCallStarted(); } catch (err) { console.error("Error setting remote description:", err); onError == null ? void 0 : onError(err instanceof Error ? err : new Error(String(err))); } }); socket.on("ice-candidate", async ({ candidate }) => { if (!peerConnection.current) return; try { if (candidate) { await peerConnection.current.addIceCandidate(new RTCIceCandidate(candidate)); } } catch (err) { onError == null ? void 0 : onError(err instanceof Error ? err : new Error(String(err))); } }); socket.on("end-call", () => { handleEndCall(); setCallStatus("ended"); setTimeout(() => setCallStatus("idle"), 3e3); }); return () => { socket.off("offer"); socket.off("answer"); socket.off("ice-candidate"); socket.off("end-call"); socket.off("incoming-call"); }; }, [socket, onError, receiverId, onCallStarted]); const initializePeerConnection = () => { peerConnection.current = new RTCPeerConnection({ iceServers: [ { urls: "stun:stun.l.google.com:19302" }, { urls: "stun:stun1.l.google.com:19302" }, // Add more STUN servers { urls: "stun:stun.stunprotocol.org:3478" }, { urls: "stun:stun.fwdnet.net" } ] }); peerConnection.current.onicecandidate = (event) => { if (event.candidate && socket) { socket.emit("ice-candidate", { to: receiverId, candidate: event.candidate }); } }; peerConnection.current.ontrack = (event) => { if (remoteVideoRef.current) { remoteVideoRef.current.srcObject = event.streams[0]; } }; if (localStream.current) { localStream.current.getTracks().forEach((track) => { if (peerConnection.current && localStream.current) { peerConnection.current.addTrack(track, localStream.current); } }); } }; const getVideoDevices = (0, import_react3.useCallback)(async () => { try { const devices = await navigator.mediaDevices.enumerateDevices(); const videoInputs = devices.filter((device) => device.kind === "videoinput"); setVideoDevices(videoInputs); console.log("Available video devices:", videoInputs); } catch (err) { console.error("Error getting video devices:", err); onError == null ? void 0 : onError(err instanceof Error ? err : new Error(String(err))); } }, [onError]); (0, import_react3.useEffect)(() => { getVideoDevices(); }, [getVideoDevices]); const switchCamera = async () => { if (!localStream.current || videoDevices.length <= 1) return; try { localStream.current.getVideoTracks().forEach((track) => track.stop()); const nextDeviceIndex = (currentVideoDeviceIndex + 1) % videoDevices.length; setCurrentVideoDeviceIndex(nextDeviceIndex); const newDeviceId = videoDevices[nextDeviceIndex].deviceId; console.log("Switching to camera:", videoDevices[nextDeviceIndex].label || `Device ${nextDeviceIndex + 1}`); const newVideoStream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: newDeviceId } }, audio: false }); const newVideoTrack = newVideoStream.getVideoTracks()[0]; const audioTracks = localStream.current.getAudioTracks(); const newStream = new MediaStream([...audioTracks, newVideoTrack]); localStream.current = newStream; if (localVideoRef.current) { localVideoRef.current.srcObject = newStream; } if (peerConnection.current && isInCall) { const senders = peerConnection.current.getSenders(); const videoSender = senders.find( (sender) => { var _a2; return ((_a2 = sender.track) == null ? void 0 : _a2.kind) === "video"; } ); if (videoSender) { await videoSender.replaceTrack(newVideoTrack); } } } catch (err) { console.error("Error switching camera:", err); onError == null ? void 0 : onError(err instanceof Error ? err : new Error(String(err))); } }; const handleStartCall = async () => { try { console.log("Starting call to:", receiverId); setIsCalling(true); setCallStatus("connecting"); socket == null ? void 0 : socket.emit("initiate-call", { to: receiverId }); console.log("Call initiation event sent"); localStream.current = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); await getVideoDevices(); const activeTrack = localStream.current.getVideoTracks()[0]; if (activeTrack) { const activeDeviceId = activeTrack.getSettings().deviceId; const activeIndex = videoDevices.findIndex((device) => device.deviceId === activeDeviceId); if (activeIndex >= 0) { setCurrentVideoDeviceIndex(activeIndex); } } if (localVideoRef.current) { localVideoRef.current.srcObject = localStream.current; } initializePeerConnection(); if (peerConnection.current) { const offer = await peerConnection.current.createOffer(); await peerConnection.current.setLocalDescription(offer); socket == null ? void 0 : socket.emit("offer", { to: receiverId, offer }); } } catch (err) { setIsCalling(false); setCallStatus("idle"); onError == null ? void 0 : onError(err instanceof Error ? err : new Error(String(err))); } }; const handleOffer = async (offer) => { try { localStream.current = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); if (localVideoRef.current) { localVideoRef.current.srcObject = localStream.current; } initializePeerConnection(); if (peerConnection.current) { await peerConnection.current.setRemoteDescription(new RTCSessionDescription(offer)); await createAnswer(); } } catch (err) { onError == null ? void 0 : onError(err instanceof Error ? err : new Error(String(err))); } }; const createAnswer = async () => { try { if (!peerConnection.current) return; const answer = await peerConnection.current.createAnswer(); await peerConnection.current.setLocalDescription(answer); socket == null ? void 0 : socket.emit("answer", { to: receiverId, answer }); setIsInCall(true); setIsReceivingCall(false); setCallStatus("connected"); onCallStarted == null ? void 0 : onCallStarted(); } catch (err) { onError == null ? void 0 : onError(err instanceof Error ? err : new Error(String(err))); } }; const handleEndCall = () => { socket == null ? void 0 : socket.emit("end-call", { to: receiverId }); if (localStream.current) { localStream.current.getTracks().forEach((track) => track.stop()); localStream.current = null; } if (peerConnection.current) { peerConnection.current.close(); peerConnection.current = null; } setIsInCall(false); setIsCalling(false); setIsReceivingCall(false); onCallEnded == null ? void 0 : onCallEnded(); }; const toggleMute = () => { if (localStream.current) { const audioTracks = localStream.current.getAudioTracks(); audioTracks.forEach((track) => { track.enabled = !track.enabled; }); setIsMuted(!isMuted); } }; const toggleVideo = () => { if (localStream.current) { const videoTracks = localStream.current.getVideoTracks(); videoTracks.forEach((track) => { track.enabled = !track.enabled; }); setIsVideoEnabled(!isVideoEnabled); } }; const formatDuration = (seconds) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; }; return /* @__PURE__ */ import_react3.default.createElement("div", { className: "video-call-container" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: `remote-video-container ${!isInCall ? "hidden" : ""}` }, /* @__PURE__ */ import_react3.default.createElement( "video", { ref: remoteVideoRef, autoPlay: true, playsInline: true, className: "remote-video" } ), /* @__PURE__ */ import_react3.default.createElement("div", { className: "remote-name" }, receiverName)), /* @__PURE__ */ import_react3.default.createElement("div", { className: `local-video-container ${!isInCall && !isCalling && !isReceivingCall ? "centered" : ""}` }, /* @__PURE__ */ import_react3.default.createElement( "video", { ref: localVideoRef, autoPlay: true, muted: true, playsInline: true, className: `local-video ${!isVideoEnabled ? "video-disabled" : ""}` } ), !isVideoEnabled && /* @__PURE__ */ import_react3.default.createElement("div", { className: "video-off-indicator" }, "Camera Off"), /* @__PURE__ */ import_react3.default.createElement("div", { className: "local-name" }, userName)), /* @__PURE__ */ import_react3.default.createElement("div", { className: "call-status-container" }, callStatus === "connecting" && /* @__PURE__ */ import_react3.default.createElement("div", { className: "call-status connecting" }, "Connecting..."), callStatus === "connected" && /* @__PURE__ */ import_react3.default.createElement("div", { className: "call-status connected" }, "Connected \u2022 ", formatDuration(callDuration)), callStatus === "ended" && /* @__PURE__ */ import_react3.default.createElement("div", { className: "call-status ended" }, "Call Ended")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "call-controls" }, !isInCall && !isCalling && !isReceivingCall ? ( // Idle state - Start call button /* @__PURE__ */ import_react3.default.createElement( "button", { className: "start-call-button", onClick: handleStartCall }, /* @__PURE__ */ import_react3.default.createElement("span", { className: "icon-phone" }), "Call ", receiverName ) ) : isCalling ? ( // Calling state /* @__PURE__ */ import_react3.default.createElement("div", { className: "calling-controls" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "calling-text" }, "Calling ", receiverName, "..."), /* @__PURE__ */ import_react3.default.createElement( "button", { className: "end-call-button", onClick: handleEndCall }, /* @__PURE__ */ import_react3.default.createElement("span", { className: "icon-hangup" }), "Cancel" )) ) : isReceivingCall ? ( // Receiving call state /* @__PURE__ */ import_react3.default.createElement("div", { className: "incoming-call-controls" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "incoming-call-text" }, "Incoming call from ", receiverName), /* @__PURE__ */ import_react3.default.createElement("div", { className: "incoming-call-buttons" }, /* @__PURE__ */ import_react3.default.createElement( "button", { className: "accept-call-button", onClick: createAnswer }, /* @__PURE__ */ import_react3.default.createElement("span", { className: "icon-accept" }), "Accept" ), /* @__PURE__ */ import_react3.default.createElement( "button", { className: "reject-call-button", onClick: handleEndCall }, /* @__PURE__ */ import_react3.default.createElement("span", { className: "icon-reject" }), "Decline" ))) ) : ( // In call controls /* @__PURE__ */ import_react3.default.createElement("div", { className: "in-call-controls" }, /* @__PURE__ */ import_react3.default.createElement( "button", { className: `mute-button ${isMuted ? "active" : ""}`, onClick: toggleMute }, /* @__PURE__ */ import_react3.default.createElement("span", { className: `icon-${isMuted ? "muted" : "mic"}` }) ), /* @__PURE__ */ import_react3.default.createElement( "button", { className: "end-call-button", onClick: handleEndCall }, /* @__PURE__ */ import_react3.default.createElement("span", { className: "icon-hangup" }) ), /* @__PURE__ */ import_react3.default.createElement( "button", { className: `video-button ${!isVideoEnabled ? "active" : ""}`, onClick: toggleVideo }, /* @__PURE__ */ import_react3.default.createElement("span", { className: `icon-${isVideoEnabled ? "video" : "video-off"}` }) ), videoDevices.length > 1 && /* @__PURE__ */ import_react3.default.createElement( "button", { className: "switch-camera-button", onClick: switchCamera, title: "Switch Camera" }, /* @__PURE__ */ import_react3.default.createElement("span", { className: "icon-switch-camera" }) )) )), isInCall && videoDevices.length > 1 && /* @__PURE__ */ import_react3.default.createElement("div", { className: "camera-indicator" }, "Camera: ", ((_a = videoDevices[currentVideoDeviceIndex]) == null ? void 0 : _a.label) || `Camera ${currentVideoDeviceIndex + 1}`)); }; // src/services/VideoService.ts var import_socket3 = require("socket.io-client"); var import_webrtc_adapter = require("webrtc-adapter"); var VideoService = class { constructor(config) { this.socket = null; this.peerConnection = null; this.localStream = null; this.remoteStream = null; this.config = config; this.initialize(); } initialize() { this.socket = (0, import_socket3.io)(this.config.serverUrl); this.socket.on("connect", () => { var _a; (_a = this.socket) == null ? void 0 : _a.emit("register", this.config.userId); }); this.socket.on("call-received", (callerId) => { var _a, _b; (_b = (_a = this.config).onCallReceived) == null ? void 0 : _b.call(_a, callerId); }); this.socket.on("call-ended", () => { var _a, _b; this.endCall(); (_b = (_a = this.config).onCallEnded) == null ? void 0 : _b.call(_a); }); this.socket.on("offer", async ({ offer, callerId }) => { await this.handleOffer(offer, callerId); }); this.socket.on("answer", async ({ answer }) => { await this.handleAnswer(answer); }); this.socket.on("ice-candidate", async ({ candidate }) => { await this.handleIceCandidate(candidate); }); } async setupPeerConnection() { this.peerConnection = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }); this.peerConnection.onicecandidate = (event) => { var _a; if (event.candidate) { (_a = this.socket) == null ? void 0 : _a.emit("ice-candidate", { candidate: event.candidate }); } }; this.peerConnection.ontrack = (event) => { this.remoteStream = event.streams[0]; }; if (this.localStream) { this.localStream.getTracks().forEach((track) => { var _a; (_a = this.peerConnection) == null ? void 0 : _a.addTrack(track, this.localStream); }); } } async startCall(receiverId) { var _a; try { this.localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); await this.setupPeerConnection(); const offer = await this.peerConnection.createOffer(); await this.peerConnection.setLocalDescription(offer); (_a = this.socket) == null ? void 0 : _a.emit("call-user", { receiverId, offer }); } catch (error) { console.error("Error starting call:", error); throw error; } } async handleOffer(offer, callerId) { var _a; try { await this.setupPeerConnection(); await this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); const answer = await this.peerConnection.createAnswer(); await this.peerConnection.setLocalDescription(answer); (_a = this.socket) == null ? void 0 : _a.emit("answer", { callerId, answer }); } catch (error) { console.error("Error handling offer:", error); } } async handleAnswer(answer) { var _a; try { await ((_a = this.peerConnection) == null ? void 0 : _a.setRemoteDescription(new RTCSessionDescription(answer))); } catch (error) { console.error("Error handling answer:", error); } } async handleIceCandidate(candidate) { var _a; try { await ((_a = this.peerConnection) == null ? void 0 : _a.addIceCandidate(new RTCIceCandidate(candidate))); } catch (error) { console.error("Error handling ICE candidate:", error); } } endCall() { var _a, _b, _c; (_a = this.localStream) == null ? void 0 : _a.getTracks().forEach((track) => track.stop()); (_b = this.peerConnection) == null ? void 0 : _b.close(); (_c = this.socket) == null ? void 0 : _c.emit("end-call"); this.localStream = null; this.remoteStream = null; this.peerConnection = null; } getLocalStream() { return this.localStream; } getRemoteStream() { return this.remoteStream; } disconnect() { var _a; this.endCall(); (_a = this.socket) == null ? void 0 : _a.disconnect(); this.socket = null; } }; // src/index.tsx function withApiKeyProvider(Component) { return function Wrapper(props) { const { apiKey, ...componentProps } = props; return /* @__PURE__ */ import_react4.default.createElement(ApiKeyProvider, { apiKey }, /* @__PURE__ */ import_react4.default.createElement(Component, { ...componentProps })); }; } var Chat2 = withApiKeyProvider(Chat); var VideoCall2 = withApiKeyProvider(VideoCall); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Chat, ChatService, VideoCall, VideoService }); //# sourceMappingURL=index.js.map