@lakshyashukla/nexora-sdk
Version:
SDK for 1:1 chat and video calling
819 lines (810 loc) • 38 kB
JavaScript
// src/index.tsx
import React4 from "react";
// src/context/AppContext.tsx
import React, {
createContext,
useContext,
useState,
useCallback,
useEffect
} from "react";
var ApiKeyError = class extends Error {
constructor(message) {
super(message);
this.name = "ApiKeyError";
}
};
var ApiKeyContext = createContext({
apiKey: "",
isValid: false,
feature: null,
user: null,
verifyApiKey: async (_key) => false,
setApiKey: (_key) => {
}
});
var ApiKeyProvider = ({
children,
apiKey: initialApiKey
}) => {
const [apiKey, setApiKey] = useState(initialApiKey);
const [isValid, setIsValid] = useState(false);
const [feature, setFeature] = useState(null);
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
const verifyApiKey = 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;
}
}, []);
useEffect(() => {
if (initialApiKey) {
verifyApiKey(initialApiKey);
}
}, [initialApiKey, verifyApiKey]);
if (!isValid && error) {
throw error;
}
return /* @__PURE__ */ React.createElement(
ApiKeyContext.Provider,
{
value: { apiKey, isValid, feature, user, verifyApiKey, setApiKey }
},
children
);
};
// src/components/Chat.tsx
import React2, { useEffect as useEffect2, useState as useState2, useRef } from "react";
// src/services/ChatService.ts
import { io } from "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 = 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] = useState2([]);
const [inputMessage, setInputMessage] = useState2("");
const [chatService, setChatService] = useState2(null);
const [isSending, setIsSending] = useState2(false);
const messagesEndRef = useRef(null);
useEffect2(() => {
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]);
useEffect2(() => {
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__ */ React2.createElement("div", { className: "chat-container" }, /* @__PURE__ */ React2.createElement("div", { className: "messages" }, messages.length === 0 && /* @__PURE__ */ React2.createElement("div", { className: "no-messages" }, "No messages yet"), messages.map((message) => /* @__PURE__ */ React2.createElement(
"div",
{
key: message.id,
className: `message ${message.senderId === userId ? "sent" : "received"}`
},
/* @__PURE__ */ React2.createElement("p", null, message.content),
/* @__PURE__ */ React2.createElement("small", null, new Date(message.timestamp).toLocaleTimeString())
)), /* @__PURE__ */ React2.createElement("div", { ref: messagesEndRef })), /* @__PURE__ */ React2.createElement("div", { className: "input-area" }, /* @__PURE__ */ React2.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__ */ React2.createElement(
"button",
{
onClick: handleSendMessage,
disabled: isSending || !inputMessage.trim()
},
isSending ? "Sending..." : "Send"
)));
};
// src/components/VideoCall.tsx
import React3, { useEffect as useEffect3, useRef as useRef2, useState as useState3, useCallback as useCallback2 } from "react";
import { io as io2 } from "socket.io-client";
var VideoCall = ({
userId,
receiverId,
serverUrl,
userName = "You",
receiverName = "User",
onCallStarted,
onCallEnded,
onError
}) => {
var _a;
const [isInCall, setIsInCall] = useState3(false);
const [isCalling, setIsCalling] = useState3(false);
const [isReceivingCall, setIsReceivingCall] = useState3(false);
const [isMuted, setIsMuted] = useState3(false);
const [isVideoEnabled, setIsVideoEnabled] = useState3(true);
const [callDuration, setCallDuration] = useState3(0);
const [socket, setSocket] = useState3(null);
const [callStatus, setCallStatus] = useState3("idle");
const [videoDevices, setVideoDevices] = useState3([]);
const [currentVideoDeviceIndex, setCurrentVideoDeviceIndex] = useState3(0);
const localVideoRef = useRef2(null);
const remoteVideoRef = useRef2(null);
const peerConnection = useRef2(null);
const localStream = useRef2(null);
const timerRef = useRef2(null);
useEffect3(() => {
console.log("Initializing socket with server URL:", serverUrl);
const newSocket = io2(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]);
useEffect3(() => {
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]);
useEffect3(() => {
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 = useCallback2(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]);
useEffect3(() => {
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__ */ React3.createElement("div", { className: "video-call-container" }, /* @__PURE__ */ React3.createElement("div", { className: `remote-video-container ${!isInCall ? "hidden" : ""}` }, /* @__PURE__ */ React3.createElement(
"video",
{
ref: remoteVideoRef,
autoPlay: true,
playsInline: true,
className: "remote-video"
}
), /* @__PURE__ */ React3.createElement("div", { className: "remote-name" }, receiverName)), /* @__PURE__ */ React3.createElement("div", { className: `local-video-container ${!isInCall && !isCalling && !isReceivingCall ? "centered" : ""}` }, /* @__PURE__ */ React3.createElement(
"video",
{
ref: localVideoRef,
autoPlay: true,
muted: true,
playsInline: true,
className: `local-video ${!isVideoEnabled ? "video-disabled" : ""}`
}
), !isVideoEnabled && /* @__PURE__ */ React3.createElement("div", { className: "video-off-indicator" }, "Camera Off"), /* @__PURE__ */ React3.createElement("div", { className: "local-name" }, userName)), /* @__PURE__ */ React3.createElement("div", { className: "call-status-container" }, callStatus === "connecting" && /* @__PURE__ */ React3.createElement("div", { className: "call-status connecting" }, "Connecting..."), callStatus === "connected" && /* @__PURE__ */ React3.createElement("div", { className: "call-status connected" }, "Connected \u2022 ", formatDuration(callDuration)), callStatus === "ended" && /* @__PURE__ */ React3.createElement("div", { className: "call-status ended" }, "Call Ended")), /* @__PURE__ */ React3.createElement("div", { className: "call-controls" }, !isInCall && !isCalling && !isReceivingCall ? (
// Idle state - Start call button
/* @__PURE__ */ React3.createElement(
"button",
{
className: "start-call-button",
onClick: handleStartCall
},
/* @__PURE__ */ React3.createElement("span", { className: "icon-phone" }),
"Call ",
receiverName
)
) : isCalling ? (
// Calling state
/* @__PURE__ */ React3.createElement("div", { className: "calling-controls" }, /* @__PURE__ */ React3.createElement("div", { className: "calling-text" }, "Calling ", receiverName, "..."), /* @__PURE__ */ React3.createElement(
"button",
{
className: "end-call-button",
onClick: handleEndCall
},
/* @__PURE__ */ React3.createElement("span", { className: "icon-hangup" }),
"Cancel"
))
) : isReceivingCall ? (
// Receiving call state
/* @__PURE__ */ React3.createElement("div", { className: "incoming-call-controls" }, /* @__PURE__ */ React3.createElement("div", { className: "incoming-call-text" }, "Incoming call from ", receiverName), /* @__PURE__ */ React3.createElement("div", { className: "incoming-call-buttons" }, /* @__PURE__ */ React3.createElement(
"button",
{
className: "accept-call-button",
onClick: createAnswer
},
/* @__PURE__ */ React3.createElement("span", { className: "icon-accept" }),
"Accept"
), /* @__PURE__ */ React3.createElement(
"button",
{
className: "reject-call-button",
onClick: handleEndCall
},
/* @__PURE__ */ React3.createElement("span", { className: "icon-reject" }),
"Decline"
)))
) : (
// In call controls
/* @__PURE__ */ React3.createElement("div", { className: "in-call-controls" }, /* @__PURE__ */ React3.createElement(
"button",
{
className: `mute-button ${isMuted ? "active" : ""}`,
onClick: toggleMute
},
/* @__PURE__ */ React3.createElement("span", { className: `icon-${isMuted ? "muted" : "mic"}` })
), /* @__PURE__ */ React3.createElement(
"button",
{
className: "end-call-button",
onClick: handleEndCall
},
/* @__PURE__ */ React3.createElement("span", { className: "icon-hangup" })
), /* @__PURE__ */ React3.createElement(
"button",
{
className: `video-button ${!isVideoEnabled ? "active" : ""}`,
onClick: toggleVideo
},
/* @__PURE__ */ React3.createElement("span", { className: `icon-${isVideoEnabled ? "video" : "video-off"}` })
), videoDevices.length > 1 && /* @__PURE__ */ React3.createElement(
"button",
{
className: "switch-camera-button",
onClick: switchCamera,
title: "Switch Camera"
},
/* @__PURE__ */ React3.createElement("span", { className: "icon-switch-camera" })
))
)), isInCall && videoDevices.length > 1 && /* @__PURE__ */ React3.createElement("div", { className: "camera-indicator" }, "Camera: ", ((_a = videoDevices[currentVideoDeviceIndex]) == null ? void 0 : _a.label) || `Camera ${currentVideoDeviceIndex + 1}`));
};
// src/services/VideoService.ts
import { io as io3 } from "socket.io-client";
import "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 = io3(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__ */ React4.createElement(ApiKeyProvider, { apiKey }, /* @__PURE__ */ React4.createElement(Component, { ...componentProps }));
};
}
var Chat2 = withApiKeyProvider(Chat);
var VideoCall2 = withApiKeyProvider(VideoCall);
export {
Chat2 as Chat,
ChatService,
VideoCall2 as VideoCall,
VideoService
};
//# sourceMappingURL=index.mjs.map