UNPKG

@lakshyashukla/nexora-sdk

Version:

SDK for 1:1 chat and video calling

1 lines 60.3 kB
{"version":3,"sources":["../src/index.tsx","../src/context/AppContext.tsx","../src/components/Chat.tsx","../src/services/ChatService.ts","#style-inject:#style-inject","../src/components/styles.css","../src/components/VideoCall.tsx","../src/services/VideoService.ts"],"sourcesContent":["import React from 'react';\nimport { ApiKeyProvider } from './context/AppContext';\nimport { Chat as ChatComponent, ChatProps } from './components/Chat';\nimport { VideoCall as VideoCallComponent, VideoCallProps } from './components/VideoCall';\n\n// Define interface for components that need API key\nexport interface WithApiKeyProps {\n apiKey: string;\n}\n\n// Update wrapper to accept and pass apiKey\nfunction withApiKeyProvider<P extends object>(Component: React.ComponentType<P>): \n React.FC<P & WithApiKeyProps> {\n return function Wrapper(props: P & WithApiKeyProps) {\n const { apiKey, ...componentProps } = props;\n return (\n <ApiKeyProvider apiKey={apiKey}>\n <Component {...componentProps as unknown as P} />\n </ApiKeyProvider>\n );\n };\n}\n\n// Export the wrapped components with their proper types\nexport const Chat: React.FC<ChatProps & WithApiKeyProps> = withApiKeyProvider(ChatComponent);\nexport const VideoCall: React.FC<VideoCallProps & WithApiKeyProps> = withApiKeyProvider(VideoCallComponent);\n\n// Re-export the props types so consumers can use them\nexport type { ChatProps } from './components/Chat';\nexport type { VideoCallProps } from './components/VideoCall';\n\nexport { ChatService } from './services/ChatService';\nexport { VideoService } from './services/VideoService';\nexport * from './types';","import React, {\n createContext,\n useContext,\n useState,\n useCallback,\n ReactNode,\n useEffect\n} from 'react';\n\ninterface ApiKeyContextType {\n apiKey: string;\n isValid: boolean;\n feature: any;\n user: any;\n verifyApiKey: (key: string) => Promise<boolean>;\n setApiKey: (key: string) => void;\n}\n\ninterface ApiKeyProviderProps {\n children: ReactNode;\n apiKey: string;\n}\n\n// Custom error class for API key validation failures\nexport class ApiKeyError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ApiKeyError';\n }\n}\n\nconst ApiKeyContext = createContext<ApiKeyContextType>({\n apiKey: '',\n isValid: false,\n feature: null,\n user: null,\n verifyApiKey: async (_key: string) => false,\n setApiKey: (_key: string) => {}\n});\n\nexport const ApiKeyProvider: React.FC<ApiKeyProviderProps> = ({ \n children,\n apiKey: initialApiKey \n}) => {\n const [apiKey, setApiKey] = useState<string>(initialApiKey);\n const [isValid, setIsValid] = useState<boolean>(false);\n const [feature, setFeature] = useState<any>(null);\n const [user, setUser] = useState<any>(null);\n const [error, setError] = useState<Error | null>(null);\n\n const verifyApiKey = useCallback(async (key: string): Promise<boolean> => {\n try {\n const res = await fetch('http://localhost:5000/apikey/verify', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ key })\n });\n \n const data = await res.json();\n \n if (!data.valid) {\n throw new ApiKeyError('Invalid API key');\n }\n \n setIsValid(true);\n setApiKey(key);\n setFeature(data.feature ?? null);\n setUser(data.user ?? null);\n setError(null);\n return true;\n } catch (err) {\n setIsValid(false);\n setFeature(null);\n setUser(null);\n setError(err instanceof Error ? err : new Error('Failed to verify API key'));\n return false;\n }\n }, []);\n\n // Automatically verify the initial API key when component mounts\n useEffect(() => {\n if (initialApiKey) {\n verifyApiKey(initialApiKey);\n }\n }, [initialApiKey, verifyApiKey]);\n\n // If API key is invalid and error exists, throw it\n if (!isValid && error) {\n throw error;\n }\n\n return (\n <ApiKeyContext.Provider\n value={{ apiKey, isValid, feature, user, verifyApiKey, setApiKey }}\n >\n {children}\n </ApiKeyContext.Provider>\n );\n};\n\nexport const useApiKey = (): ApiKeyContextType => useContext(ApiKeyContext);","import React, { useEffect, useState, useRef } from 'react';\nimport { ChatService } from '../services/ChatService';\nimport { Message } from '../types';\nimport './styles.css';\n\nexport interface ChatProps {\n userId: string;\n threadId?: string;\n receiverId: string;\n serverUrl: string;\n}\n\nexport const Chat: React.FC<ChatProps> = ({ userId, threadId, receiverId, serverUrl }) => {\n const [messages, setMessages] = useState<Message[]>([]);\n const [inputMessage, setInputMessage] = useState('');\n const [chatService, setChatService] = useState<ChatService | null>(null);\n const [isSending, setIsSending] = useState(false);\n const messagesEndRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!userId) {\n console.error(\"Chat component requires a userId\");\n return;\n }\n\n const service = new ChatService({\n userId,\n serverUrl,\n onMessageReceived: (message) => {\n console.log(\"Message received in component:\", message);\n setMessages(prev => {\n // Avoid duplicate messages\n if (prev.some(m => m.id === message.id)) {\n return prev;\n }\n return [...prev, message];\n });\n }\n });\n\n setChatService(service);\n\n return () => {\n service.disconnect();\n };\n }, [userId, serverUrl]);\n\n useEffect(() => {\n // Scroll to bottom whenever messages change\n messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });\n }, [messages]);\n\n const handleSendMessage = async () => {\n if (!inputMessage.trim() || !chatService || isSending) return;\n\n try {\n setIsSending(true);\n const sent = await chatService.sendMessage(receiverId, inputMessage.trim());\n \n if (sent) {\n setInputMessage('');\n } else {\n console.error(\"Failed to send message\");\n // Optionally show an error to the user\n }\n } catch (error) {\n console.error('Error sending message:', error);\n } finally {\n setIsSending(false);\n }\n };\n\n return (\n <div className=\"chat-container\">\n <div className=\"messages\">\n {messages.length === 0 && (\n <div className=\"no-messages\">No messages yet</div>\n )}\n {messages.map((message) => (\n <div\n key={message.id}\n className={`message ${message.senderId === userId ? 'sent' : 'received'}`}\n >\n <p>{message.content}</p>\n <small>{new Date(message.timestamp).toLocaleTimeString()}</small>\n </div>\n ))}\n <div ref={messagesEndRef} />\n </div>\n <div className=\"input-area\">\n <input\n type=\"text\"\n value={inputMessage}\n onChange={(e) => setInputMessage(e.target.value)}\n onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}\n placeholder=\"Type a message...\"\n disabled={isSending}\n />\n <button \n onClick={handleSendMessage} \n disabled={isSending || !inputMessage.trim()}\n >\n {isSending ? 'Sending...' : 'Send'}\n </button>\n </div>\n </div>\n );\n};","import { io, Socket } from 'socket.io-client';\nimport { Message, ChatConfig } from '../types';\n\nexport class ChatService {\n private socket: Socket | null = null;\n private config: ChatConfig;\n\n constructor(config: ChatConfig) {\n this.config = config;\n this.initialize();\n }\n\n private initialize() {\n console.log('Initializing chat service with config:', this.config);\n \n this.socket = io(this.config.serverUrl);\n \n this.socket.on('connect', () => {\n console.log('Connected to chat server');\n this.socket?.emit('register', this.config.userId);\n });\n\n this.socket.on('connect_error', (error) => {\n console.error('Connection error:', error);\n });\n\n // Change from 'message' to 'receive_message' to match server event\n this.socket.on('receive_message', (message: Message) => {\n console.log('Message received:', message);\n this.config.onMessageReceived?.(message);\n });\n }\n\n public async sendMessage(receiverId: string, content: string): Promise<boolean> {\n if (!this.socket?.connected) {\n console.error('Not connected to chat server');\n return false;\n }\n\n try {\n const message: Message = {\n id: Math.random().toString(36).substr(2, 9),\n content,\n senderId: this.config.userId,\n receiverId,\n timestamp: Date.now()\n };\n\n console.log('Sending message:', message);\n \n return new Promise((resolve) => {\n this.socket?.emit('send_message', message, (acknowledgement: any) => {\n console.log('Message acknowledgement:', acknowledgement);\n if (acknowledgement?.success) {\n // Also call the message received handler for the sender's UI\n this.config.onMessageReceived?.(message);\n resolve(true);\n } else {\n console.error('Failed to send message:', acknowledgement?.error);\n resolve(false);\n }\n });\n });\n } catch (error) {\n console.error('Error in sendMessage:', error);\n return false;\n }\n }\n\n public disconnect(): void {\n if (this.socket?.connected) {\n this.socket.disconnect();\n }\n this.socket = null;\n }\n}","\n export default function styleInject(css, { insertAt } = {}) {\n if (!css || typeof document === 'undefined') return\n \n const head = document.head || document.getElementsByTagName('head')[0]\n const style = document.createElement('style')\n style.type = 'text/css'\n \n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild)\n } else {\n head.appendChild(style)\n }\n } else {\n head.appendChild(style)\n }\n \n if (style.styleSheet) {\n style.styleSheet.cssText = css\n } else {\n style.appendChild(document.createTextNode(css))\n }\n }\n ","import styleInject from '#style-inject';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\")","import React, { useEffect, useRef, useState, useCallback } from 'react';\nimport { io, Socket } from 'socket.io-client';\nimport './styles.css';\n\n// Add export keyword to make the interface available outside this file\nexport interface VideoCallProps {\n /**\n * ID of the current user\n */\n userId: string;\n \n /**\n * ID of the user being called\n */\n receiverId: string;\n \n /**\n * URL for the WebSocket server\n */\n serverUrl: string;\n \n /**\n * Display name for the current user\n * @default \"You\"\n */\n userName?: string;\n \n /**\n * Display name for the user being called\n * @default \"User\"\n */\n receiverName?: string;\n \n /**\n * Callback triggered when a call is successfully started\n */\n onCallStarted?: () => void;\n \n /**\n * Callback triggered when a call ends\n */\n onCallEnded?: () => void;\n \n /**\n * Callback for handling errors during call setup or execution\n */\n onError?: (error: Error) => void;\n}\n\nexport const VideoCall: React.FC<VideoCallProps> = ({\n userId,\n receiverId,\n serverUrl,\n userName = 'You',\n receiverName = 'User',\n onCallStarted,\n onCallEnded,\n onError\n}) => {\n const [isInCall, setIsInCall] = useState(false);\n const [isCalling, setIsCalling] = useState(false);\n const [isReceivingCall, setIsReceivingCall] = useState(false);\n const [isMuted, setIsMuted] = useState(false);\n const [isVideoEnabled, setIsVideoEnabled] = useState(true);\n const [callDuration, setCallDuration] = useState(0);\n const [socket, setSocket] = useState<Socket | null>(null);\n const [callStatus, setCallStatus] = useState<'idle' | 'connecting' | 'connected' | 'ended'>('idle');\n \n const [videoDevices, setVideoDevices] = useState<MediaDeviceInfo[]>([]);\n const [currentVideoDeviceIndex, setCurrentVideoDeviceIndex] = useState(0);\n\n const localVideoRef = useRef<HTMLVideoElement>(null);\n const remoteVideoRef = useRef<HTMLVideoElement>(null);\n const peerConnection = useRef<RTCPeerConnection | null>(null);\n const localStream = useRef<MediaStream | null>(null);\n const timerRef = useRef<NodeJS.Timeout | null>(null);\n\n // Initialize socket connection\n useEffect(() => {\n console.log('Initializing socket with server URL:', serverUrl);\n \n const newSocket = io(serverUrl, {\n query: { userId }, // Make sure userId is passed in query\n transports: ['websocket', 'polling'],\n reconnection: true,\n reconnectionAttempts: 5,\n });\n \n newSocket.on(\"connect\", () => {\n console.log(\"Socket connected with ID:\", newSocket.id);\n newSocket.emit(\"register\", userId);\n });\n\n newSocket.on(\"connect_error\", (error) => {\n console.error(\"Socket connection error:\", error);\n onError?.(new Error(`Failed to connect to video call server: ${error.message}`));\n });\n\n setSocket(newSocket);\n\n return () => {\n newSocket.disconnect();\n };\n }, [serverUrl, userId, onError]);\n\n // Call duration timer\n useEffect(() => {\n if (isInCall && callStatus === 'connected') {\n timerRef.current = setInterval(() => {\n setCallDuration(prev => prev + 1);\n }, 1000);\n } else {\n if (timerRef.current) {\n clearInterval(timerRef.current);\n timerRef.current = null;\n }\n if (callStatus === 'idle') {\n setCallDuration(0);\n }\n }\n \n return () => {\n if (timerRef.current) {\n clearInterval(timerRef.current);\n }\n };\n }, [isInCall, callStatus]);\n\n // Handle incoming WebRTC signaling\n useEffect(() => {\n if (!socket) return;\n\n console.log(\"Setting up video call signal handlers\");\n\n socket.on(\"incoming-call\", ({ from }) => {\n console.log(\"Incoming call from:\", from);\n if (from === receiverId) {\n setIsReceivingCall(true);\n }\n });\n\n socket.on(\"offer\", async ({ from, offer }) => {\n console.log(\"Received offer from:\", from);\n if (from === receiverId) {\n setCallStatus('connecting');\n await handleOffer(offer);\n }\n });\n\n socket.on(\"answer\", async ({ from, answer }) => {\n console.log(\"Received answer from:\", from);\n if (from !== receiverId) return;\n \n if (!peerConnection.current) {\n console.error(\"No peer connection available\");\n return;\n }\n \n try {\n await peerConnection.current.setRemoteDescription(new RTCSessionDescription(answer));\n setCallStatus('connected');\n setIsInCall(true);\n setIsCalling(false);\n onCallStarted?.();\n } catch (err) {\n console.error(\"Error setting remote description:\", err);\n onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n socket.on(\"ice-candidate\", async ({ candidate }) => {\n if (!peerConnection.current) return;\n try {\n if (candidate) {\n await peerConnection.current.addIceCandidate(new RTCIceCandidate(candidate));\n }\n } catch (err) {\n onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n socket.on(\"end-call\", () => {\n handleEndCall();\n setCallStatus('ended');\n setTimeout(() => setCallStatus('idle'), 3000);\n });\n\n return () => {\n socket.off(\"offer\");\n socket.off(\"answer\");\n socket.off(\"ice-candidate\");\n socket.off(\"end-call\");\n socket.off(\"incoming-call\");\n };\n }, [socket, onError, receiverId, onCallStarted]);\n\n const initializePeerConnection = () => {\n peerConnection.current = new RTCPeerConnection({\n iceServers: [\n { urls: \"stun:stun.l.google.com:19302\" },\n { urls: \"stun:stun1.l.google.com:19302\" },\n // Add more STUN servers\n { urls: \"stun:stun.stunprotocol.org:3478\" },\n { urls: \"stun:stun.fwdnet.net\" }\n ]\n });\n\n peerConnection.current.onicecandidate = (event) => {\n if (event.candidate && socket) {\n socket.emit(\"ice-candidate\", {\n to: receiverId,\n candidate: event.candidate\n });\n }\n };\n\n peerConnection.current.ontrack = (event) => {\n if (remoteVideoRef.current) {\n remoteVideoRef.current.srcObject = event.streams[0];\n }\n };\n\n if (localStream.current) {\n localStream.current.getTracks().forEach(track => {\n if (peerConnection.current && localStream.current) {\n peerConnection.current.addTrack(track, localStream.current);\n }\n });\n }\n };\n\n const getVideoDevices = useCallback(async () => {\n try {\n const devices = await navigator.mediaDevices.enumerateDevices();\n const videoInputs = devices.filter(device => device.kind === 'videoinput');\n setVideoDevices(videoInputs);\n console.log('Available video devices:', videoInputs);\n } catch (err) {\n console.error('Error getting video devices:', err);\n onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n }, [onError]);\n\n useEffect(() => {\n getVideoDevices();\n }, [getVideoDevices]);\n\n const switchCamera = async () => {\n if (!localStream.current || videoDevices.length <= 1) return;\n \n try {\n localStream.current.getVideoTracks().forEach(track => track.stop());\n \n const nextDeviceIndex = (currentVideoDeviceIndex + 1) % videoDevices.length;\n setCurrentVideoDeviceIndex(nextDeviceIndex);\n \n const newDeviceId = videoDevices[nextDeviceIndex].deviceId;\n console.log('Switching to camera:', videoDevices[nextDeviceIndex].label || `Device ${nextDeviceIndex + 1}`);\n \n const newVideoStream = await navigator.mediaDevices.getUserMedia({\n video: { deviceId: { exact: newDeviceId } },\n audio: false\n });\n \n const newVideoTrack = newVideoStream.getVideoTracks()[0];\n const audioTracks = localStream.current.getAudioTracks();\n \n const newStream = new MediaStream([...audioTracks, newVideoTrack]);\n localStream.current = newStream;\n \n if (localVideoRef.current) {\n localVideoRef.current.srcObject = newStream;\n }\n \n if (peerConnection.current && isInCall) {\n const senders = peerConnection.current.getSenders();\n const videoSender = senders.find(sender => \n sender.track?.kind === 'video'\n );\n \n if (videoSender) {\n await videoSender.replaceTrack(newVideoTrack);\n }\n }\n } catch (err) {\n console.error('Error switching camera:', err);\n onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n };\n\n const handleStartCall = async () => {\n try {\n console.log(\"Starting call to:\", receiverId);\n setIsCalling(true);\n setCallStatus('connecting');\n \n socket?.emit(\"initiate-call\", { to: receiverId });\n console.log(\"Call initiation event sent\");\n \n localStream.current = await navigator.mediaDevices.getUserMedia({\n video: true,\n audio: true\n });\n \n await getVideoDevices();\n \n const activeTrack = localStream.current.getVideoTracks()[0];\n if (activeTrack) {\n const activeDeviceId = activeTrack.getSettings().deviceId;\n const activeIndex = videoDevices.findIndex(device => device.deviceId === activeDeviceId);\n if (activeIndex >= 0) {\n setCurrentVideoDeviceIndex(activeIndex);\n }\n }\n \n if (localVideoRef.current) {\n localVideoRef.current.srcObject = localStream.current;\n }\n \n initializePeerConnection();\n \n if (peerConnection.current) {\n const offer = await peerConnection.current.createOffer();\n await peerConnection.current.setLocalDescription(offer);\n \n socket?.emit(\"offer\", {\n to: receiverId,\n offer\n });\n }\n } catch (err) {\n setIsCalling(false);\n setCallStatus('idle');\n onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n };\n\n const handleOffer = async (offer: RTCSessionDescriptionInit) => {\n try {\n localStream.current = await navigator.mediaDevices.getUserMedia({\n video: true,\n audio: true\n });\n \n if (localVideoRef.current) {\n localVideoRef.current.srcObject = localStream.current;\n }\n \n initializePeerConnection();\n \n if (peerConnection.current) {\n await peerConnection.current.setRemoteDescription(new RTCSessionDescription(offer));\n await createAnswer();\n }\n } catch (err) {\n onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n };\n\n const createAnswer = async () => {\n try {\n if (!peerConnection.current) return;\n \n const answer = await peerConnection.current.createAnswer();\n await peerConnection.current.setLocalDescription(answer);\n \n socket?.emit(\"answer\", {\n to: receiverId,\n answer\n });\n \n setIsInCall(true);\n setIsReceivingCall(false);\n setCallStatus('connected');\n onCallStarted?.();\n } catch (err) {\n onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n };\n\n const handleEndCall = () => {\n socket?.emit(\"end-call\", { to: receiverId });\n \n if (localStream.current) {\n localStream.current.getTracks().forEach(track => track.stop());\n localStream.current = null;\n }\n\n if (peerConnection.current) {\n peerConnection.current.close();\n peerConnection.current = null;\n }\n \n setIsInCall(false);\n setIsCalling(false);\n setIsReceivingCall(false);\n onCallEnded?.();\n };\n\n const toggleMute = () => {\n if (localStream.current) {\n const audioTracks = localStream.current.getAudioTracks();\n audioTracks.forEach(track => {\n track.enabled = !track.enabled;\n });\n setIsMuted(!isMuted);\n }\n };\n\n const toggleVideo = () => {\n if (localStream.current) {\n const videoTracks = localStream.current.getVideoTracks();\n videoTracks.forEach(track => {\n track.enabled = !track.enabled;\n });\n setIsVideoEnabled(!isVideoEnabled);\n }\n };\n\n const formatDuration = (seconds: number) => {\n const mins = Math.floor(seconds / 60);\n const secs = seconds % 60;\n return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n };\n\n return (\n <div className=\"video-call-container\">\n {/* Remote Video (Main) */}\n <div className={`remote-video-container ${!isInCall ? 'hidden' : ''}`}>\n <video \n ref={remoteVideoRef} \n autoPlay \n playsInline\n className=\"remote-video\"\n />\n <div className=\"remote-name\">{receiverName}</div>\n </div>\n \n {/* Local Video (Picture-in-Picture) */}\n <div className={`local-video-container ${!isInCall && !isCalling && !isReceivingCall ? 'centered' : ''}`}>\n <video \n ref={localVideoRef} \n autoPlay \n muted \n playsInline\n className={`local-video ${!isVideoEnabled ? 'video-disabled' : ''}`}\n />\n {!isVideoEnabled && <div className=\"video-off-indicator\">Camera Off</div>}\n <div className=\"local-name\">{userName}</div>\n </div>\n \n {/* Call Status */}\n <div className=\"call-status-container\">\n {callStatus === 'connecting' && <div className=\"call-status connecting\">Connecting...</div>}\n {callStatus === 'connected' && <div className=\"call-status connected\">Connected • {formatDuration(callDuration)}</div>}\n {callStatus === 'ended' && <div className=\"call-status ended\">Call Ended</div>}\n </div>\n \n {/* Call Controls */}\n <div className=\"call-controls\">\n {!isInCall && !isCalling && !isReceivingCall ? (\n // Idle state - Start call button\n <button \n className=\"start-call-button\"\n onClick={handleStartCall}\n >\n <span className=\"icon-phone\"></span>\n Call {receiverName}\n </button>\n ) : isCalling ? (\n // Calling state\n <div className=\"calling-controls\">\n <div className=\"calling-text\">Calling {receiverName}...</div>\n <button \n className=\"end-call-button\"\n onClick={handleEndCall}\n >\n <span className=\"icon-hangup\"></span>\n Cancel\n </button>\n </div>\n ) : isReceivingCall ? (\n // Receiving call state\n <div className=\"incoming-call-controls\">\n <div className=\"incoming-call-text\">Incoming call from {receiverName}</div>\n <div className=\"incoming-call-buttons\">\n <button \n className=\"accept-call-button\"\n onClick={createAnswer}\n >\n <span className=\"icon-accept\"></span>\n Accept\n </button>\n <button \n className=\"reject-call-button\"\n onClick={handleEndCall}\n >\n <span className=\"icon-reject\"></span>\n Decline\n </button>\n </div>\n </div>\n ) : (\n // In call controls\n <div className=\"in-call-controls\">\n <button \n className={`mute-button ${isMuted ? 'active' : ''}`}\n onClick={toggleMute}\n >\n <span className={`icon-${isMuted ? 'muted' : 'mic'}`}></span>\n </button>\n <button \n className=\"end-call-button\"\n onClick={handleEndCall}\n >\n <span className=\"icon-hangup\"></span>\n </button>\n <button \n className={`video-button ${!isVideoEnabled ? 'active' : ''}`}\n onClick={toggleVideo}\n >\n <span className={`icon-${isVideoEnabled ? 'video' : 'video-off'}`}></span>\n </button>\n \n {/* Add the camera switch button if multiple cameras are available */}\n {videoDevices.length > 1 && (\n <button \n className=\"switch-camera-button\"\n onClick={switchCamera}\n title=\"Switch Camera\"\n >\n <span className=\"icon-switch-camera\"></span>\n </button>\n )}\n </div>\n )}\n </div>\n \n {/* Optionally add a camera indicator */}\n {isInCall && videoDevices.length > 1 && (\n <div className=\"camera-indicator\">\n Camera: {videoDevices[currentVideoDeviceIndex]?.label || `Camera ${currentVideoDeviceIndex + 1}`}\n </div>\n )}\n </div>\n );\n};","import { io, Socket } from 'socket.io-client';\nimport 'webrtc-adapter';\n\nexport interface VideoCallConfig {\n userId: string;\n serverUrl: string; // Make this required since io() needs it\n onCallEnded?: () => void;\n onCallReceived?: (callerId: string) => void; // This is used but was missing\n}\n\nexport class VideoService {\n private socket: Socket | null = null;\n private peerConnection: RTCPeerConnection | null = null;\n private config: VideoCallConfig;\n private localStream: MediaStream | null = null;\n private remoteStream: MediaStream | null = null;\n\n constructor(config: VideoCallConfig) {\n this.config = config;\n this.initialize();\n }\n\n private initialize() {\n this.socket = io(this.config.serverUrl);\n \n this.socket.on('connect', () => {\n this.socket?.emit('register', this.config.userId);\n });\n\n this.socket.on('call-received', (callerId: string) => {\n this.config.onCallReceived?.(callerId);\n });\n\n this.socket.on('call-ended', () => {\n this.endCall();\n this.config.onCallEnded?.();\n });\n\n this.socket.on('offer', async ({ offer, callerId }) => {\n await this.handleOffer(offer, callerId);\n });\n\n this.socket.on('answer', async ({ answer }) => {\n await this.handleAnswer(answer);\n });\n\n this.socket.on('ice-candidate', async ({ candidate }) => {\n await this.handleIceCandidate(candidate);\n });\n }\n\n private async setupPeerConnection() {\n this.peerConnection = new RTCPeerConnection({\n iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]\n });\n\n this.peerConnection.onicecandidate = (event) => {\n if (event.candidate) {\n this.socket?.emit('ice-candidate', { candidate: event.candidate });\n }\n };\n\n this.peerConnection.ontrack = (event) => {\n this.remoteStream = event.streams[0];\n };\n\n if (this.localStream) {\n this.localStream.getTracks().forEach(track => {\n this.peerConnection?.addTrack(track, this.localStream!);\n });\n }\n }\n\n public async startCall(receiverId: string): Promise<void> {\n try {\n this.localStream = await navigator.mediaDevices.getUserMedia({\n video: true,\n audio: true\n });\n\n await this.setupPeerConnection();\n const offer = await this.peerConnection!.createOffer();\n await this.peerConnection!.setLocalDescription(offer);\n\n this.socket?.emit('call-user', {\n receiverId,\n offer\n });\n } catch (error) {\n console.error('Error starting call:', error);\n throw error;\n }\n }\n\n private async handleOffer(offer: RTCSessionDescriptionInit, callerId: string) {\n try {\n await this.setupPeerConnection();\n await this.peerConnection!.setRemoteDescription(new RTCSessionDescription(offer));\n const answer = await this.peerConnection!.createAnswer();\n await this.peerConnection!.setLocalDescription(answer);\n\n this.socket?.emit('answer', {\n callerId,\n answer\n });\n } catch (error) {\n console.error('Error handling offer:', error);\n }\n }\n\n private async handleAnswer(answer: RTCSessionDescriptionInit) {\n try {\n await this.peerConnection?.setRemoteDescription(new RTCSessionDescription(answer));\n } catch (error) {\n console.error('Error handling answer:', error);\n }\n }\n\n private async handleIceCandidate(candidate: RTCIceCandidateInit) {\n try {\n await this.peerConnection?.addIceCandidate(new RTCIceCandidate(candidate));\n } catch (error) {\n console.error('Error handling ICE candidate:', error);\n }\n }\n\n public endCall(): void {\n this.localStream?.getTracks().forEach(track => track.stop());\n this.peerConnection?.close();\n this.socket?.emit('end-call');\n this.localStream = null;\n this.remoteStream = null;\n this.peerConnection = null;\n }\n\n public getLocalStream(): MediaStream | null {\n return this.localStream;\n }\n\n public getRemoteStream(): MediaStream | null {\n return this.remoteStream;\n }\n\n public disconnect(): void {\n this.endCall();\n this.socket?.disconnect();\n this.socket = null;\n }\n}"],"mappings":"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