diff --git a/client.py b/client.py index 14a754c..db2f08c 100644 --- a/client.py +++ b/client.py @@ -7,39 +7,53 @@ import time import argparse +# NEW: import register from the register_client file +from register_client import register + class VoiceClient: - # Audio parameters CHUNK = 1024 FORMAT = pyaudio.paInt16 CHANNELS = 1 RATE = 44100 - + def __init__(self, host, port): + # FIRST: perform registration and store the assigned number + try: + response = register() # this prints and returns a number by default + assigned = None + + # If response is a dict: extract number + if isinstance(response, dict): + assigned = response.get("number") + # If it's a string (JSON text etc.), store it anyway + elif isinstance(response, str): + assigned = response + self.assigned_number = assigned + if assigned: + print(colored(f"✓ Device registered. Assigned Number: {assigned}", "green")) + except Exception as e: + print(colored(f"[!] Registration failed: {e}", "red")) + self.assigned_number = None + self.host = host self.port = port self.running = True self.name = '' self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.audio = pyaudio.PyAudio() - - # Set up audio streams + self.call_active = False + self.input_stream = self.audio.open( - format=self.FORMAT, - channels=self.CHANNELS, - rate=self.RATE, - input=True, + format=self.FORMAT, channels=self.CHANNELS, + rate=self.RATE, input=True, frames_per_buffer=self.CHUNK ) - self.output_stream = self.audio.open( - format=self.FORMAT, - channels=self.CHANNELS, - rate=self.RATE, - output=True, + format=self.FORMAT, channels=self.CHANNELS, + rate=self.RATE, output=True, frames_per_buffer=self.CHUNK ) - - # Connect to server + print(colored(f"Connecting to voice server at {host}:{port}...", "yellow")) try: self.sock.connect((host, port)) @@ -48,40 +62,37 @@ def __init__(self, host, port): self.cleanup() sys.exit(1) print(colored("Connected to voice server.", "green")) - + def start(self): - """Start the voice client""" - # Set up signal handler signal.signal(signal.SIGINT, self.handle_signal) - - # Get user's name + + # Ask user name while not self.name: self.name = input(colored("Enter your name: ", "blue")).strip() - - # Send name to server + + # Attach assigned number + if self.assigned_number: + self.name = f"{self.name}#{self.assigned_number}" + self.sock.send(f"NAME:{self.name}".encode()) - - # Start audio threads - receive_thread = threading.Thread(target=self.receive_audio) - send_thread = threading.Thread(target=self.send_audio) - - receive_thread.daemon = True - send_thread.daemon = True - + + receive_thread = threading.Thread(target=self.receive_audio, daemon=True) + send_thread = threading.Thread(target=self.send_audio, daemon=True) + text_thread = threading.Thread(target=self.user_input_loop, daemon=True) + receive_thread.start() send_thread.start() - + text_thread.start() + print(colored("Voice chat started! Press Ctrl+C to exit.", "yellow")) - - # Keep main thread alive (cross-platform) + try: while self.running: - time.sleep(0.1) # Sleep briefly to prevent high CPU usage + time.sleep(0.1) except KeyboardInterrupt: self.handle_signal(None, None) def send_audio(self): - """Capture and send audio to server""" while self.running: try: data = self.input_stream.read(self.CHUNK, exception_on_overflow=False) @@ -91,64 +102,92 @@ def send_audio(self): if self.running: print(colored(f"Error sending audio: {e}", "red")) break - + def receive_audio(self): - """Receive and play audio from server""" while self.running: try: data = self.sock.recv(self.CHUNK * 2) if not data: break - - # Check for control messages + + # Handle control / text messages if data.startswith(b"CONTROL:"): message = data[8:].decode() - print(colored(message, "cyan")) + + if message.startswith("INCOMING_CALL:"): + caller = message[len("INCOMING_CALL:"):] + print(colored(f"[!] Incoming call from {caller}. Type /accept or /reject", "cyan")) + + elif message == "CALL_ACCEPTED": + self.call_active = True + print(colored("[✓] Call accepted!", "green")) + + elif "CALL_ENDED" in message or "CALL_REJECTED" in message: + self.call_active = False + print(colored("[!] Call ended or rejected.", "red")) + + else: + print(colored(message, "cyan")) continue - - # Check for server full message - if data == b"SERVER_FULL": - print(colored("Server is full. Try again later.", "red")) - self.running = False - break - - # Play audio data - self.output_stream.write(data) - + + # If actual audio + if self.call_active or True: + self.output_stream.write(data) + except Exception as e: if self.running: print(colored(f"Error receiving audio: {e}", "red")) break - + + def user_input_loop(self): + """Handle slash commands from user (text)""" + while self.running: + cmd = input().strip() + if cmd: + if cmd == "/quit": + self.handle_signal(None, None) + else: + self.sock.send(cmd.encode()) + def handle_signal(self, signum, frame): - """Handle Ctrl+C""" print(colored("\nExiting voice chat...", "yellow")) self.running = False self.cleanup() sys.exit(0) - + def cleanup(self): - """Cleanup resources""" self.running = False if hasattr(self, 'input_stream'): - self.input_stream.stop_stream() - self.input_stream.close() + try: + self.input_stream.stop_stream() + self.input_stream.close() + except: + pass if hasattr(self, 'output_stream'): - self.output_stream.stop_stream() - self.output_stream.close() + try: + self.output_stream.stop_stream() + self.output_stream.close() + except: + pass if hasattr(self, 'audio'): - self.audio.terminate() + try: + self.audio.terminate() + except: + pass if hasattr(self, 'sock'): - self.sock.close() - + try: + self.sock.close() + except: + pass + def main(): parser = argparse.ArgumentParser(description='Voice Chat Client') parser.add_argument('host', help='server address') parser.add_argument('port', type=int, help='server port') args = parser.parse_args() - + client = VoiceClient(args.host, args.port) client.start() - + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/server.py b/server.py index d391147..f87f5b4 100644 --- a/server.py +++ b/server.py @@ -1,137 +1,194 @@ import socket import threading -from typing import List, Dict +from typing import Dict import logging import signal import sys -# Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s') logger = logging.getLogger(__name__) MAX_CLIENTS = 10 -CHUNK_SIZE = 4096 # Larger chunk size for audio data +CHUNK_SIZE = 4096 class VoiceServer: def __init__(self, host='0.0.0.0', port=8081): self.host = host self.port = port - self.clients: Dict[socket.socket, str] = {} # Socket to client name mapping + self.clients: Dict[socket.socket, str] = {} self.clients_lock = threading.Lock() self.running = True - - # Initialize server socket + self.active_calls: Dict[socket.socket, socket.socket] = {} # socket -> partner or pending target + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - + def start(self): - """Start the voice server""" try: self.server_socket.bind((self.host, self.port)) self.server_socket.listen(MAX_CLIENTS) logger.info(f"Voice Server listening on port {self.port} (max {MAX_CLIENTS} clients)...") - - # Handle Ctrl+C gracefully signal.signal(signal.SIGINT, self.handle_shutdown) - + while self.running: try: client_socket, client_address = self.server_socket.accept() + if len(self.clients) >= MAX_CLIENTS: - logger.info(f"Max clients reached. Rejecting {client_address}") client_socket.send(b"SERVER_FULL") client_socket.close() continue - - # Start new client thread + client_thread = threading.Thread( target=self.handle_client, args=(client_socket, client_address) ) client_thread.daemon = True client_thread.start() - + except Exception as e: logger.error(f"Error accepting connection: {e}") - + except Exception as e: logger.error(f"Server error: {e}") finally: self.cleanup() - - def handle_client(self, client_socket: socket.socket, address): - """Handle individual client connection""" + + def handle_client(self, client_socket, address): try: - # First message should be the client's name name_data = client_socket.recv(1024).decode() if name_data.startswith("NAME:"): client_name = name_data[5:] with self.clients_lock: self.clients[client_socket] = client_name logger.info(f"New client connected: {client_name} from {address}") - - # Broadcast join notification self.broadcast_control_message(f"SERVER: {client_name} joined", client_socket) - + while self.running: - # Receive audio data - audio_data = client_socket.recv(CHUNK_SIZE) - if not audio_data: + data = client_socket.recv(CHUNK_SIZE) + if not data: break - - # Broadcast to other clients - self.broadcast_audio(audio_data, client_socket) - + + # Handle slash commands text + try: + text = data.decode().strip() + if text.startswith("/"): + self.handle_command(text, client_socket) + continue + except: + pass + + self.broadcast_audio(data, client_socket) + except Exception as e: logger.error(f"Error handling client {address}: {e}") finally: self.remove_client(client_socket) - - def broadcast_audio(self, audio_data: bytes, sender_socket: socket.socket): - """Broadcast audio data to all other clients""" + + def handle_command(self, cmd, client_socket): + user = self.clients.get(client_socket) + + if cmd.startswith("/call "): + target_name = cmd.split(" ", 1)[1].strip() + with self.clients_lock: + for sock, name in self.clients.items(): + if name == target_name: + sock.send(f"CONTROL:INCOMING_CALL:{user}".encode()) + client_socket.send(b"CONTROL:CALLING...") + # mark as pending: caller_socket -> target_socket + self.active_calls[client_socket] = sock + return + client_socket.send(b"CONTROL:User not found") + + elif cmd == "/accept": + # find a pending call where this socket is target + for caller_socket, target_socket in list(self.active_calls.items()): + if target_socket == client_socket: + # establish call both directions + self.active_calls[caller_socket] = client_socket + self.active_calls[client_socket] = caller_socket + caller_socket.send(b"CONTROL:CALL_ACCEPTED") + client_socket.send(b"CONTROL:CALL_ACCEPTED") + return + client_socket.send(b"CONTROL:No call to accept") + + elif cmd == "/reject": + # find pending caller + for caller_socket, target_socket in list(self.active_calls.items()): + if target_socket == client_socket: + self.active_calls.pop(caller_socket, None) + caller_socket.send(b"CONTROL:CALL_REJECTED") + client_socket.send(b"CONTROL:CALL_REJECTED") + return + client_socket.send(b"CONTROL:No call to reject") + + elif cmd == "/end": + if client_socket in self.active_calls: + partner = self.active_calls.pop(client_socket) + # Remove reverse link if exists + if partner in self.active_calls: + self.active_calls.pop(partner, None) + client_socket.send(b"CONTROL:CALL_ENDED") + partner.send(b"CONTROL:CALL_ENDED") + else: + client_socket.send(b"CONTROL:No active call") + + def broadcast_audio(self, audio_data, sender_socket): + # Private call routing + if sender_socket in self.active_calls: + partner = self.active_calls[sender_socket] + try: + partner.send(audio_data) + except: + pass + return + + # Group lobby with self.clients_lock: - for client_socket in self.clients: - if client_socket != sender_socket: + for sock in self.clients: + if sock != sender_socket: try: - client_socket.send(audio_data) + sock.send(audio_data) except: continue - + def broadcast_control_message(self, message: str, exclude_socket=None): - """Broadcast control messages to clients""" with self.clients_lock: - for client_socket in self.clients: - if client_socket != exclude_socket: + for sock in self.clients: + if sock != exclude_socket: try: - client_socket.send(f"CONTROL:{message}".encode()) + sock.send(f"CONTROL:{message}".encode()) except: continue - - def remove_client(self, client_socket: socket.socket): - """Remove a client and cleanup""" + + def remove_client(self, client_socket): with self.clients_lock: if client_socket in self.clients: client_name = self.clients[client_socket] del self.clients[client_socket] logger.info(f"Client disconnected: {client_name}") self.broadcast_control_message(f"SERVER: {client_name} left") + # End any active call involving this client + if client_socket in self.active_calls: + partner = self.active_calls.pop(client_socket) + if partner in self.active_calls: + self.active_calls.pop(partner, None) + partner.send(b"CONTROL:CALL_ENDED") client_socket.close() - + def handle_shutdown(self, signum, frame): - """Handle server shutdown""" logger.info("Shutting down server...") self.running = False self.cleanup() sys.exit(0) - + def cleanup(self): - """Cleanup server resources""" with self.clients_lock: for client_socket in self.clients: client_socket.close() self.clients.clear() self.server_socket.close() - + if __name__ == "__main__": server = VoiceServer() server.start() \ No newline at end of file