commit 5720928c282cf58994bda1d0d79e389982f2e9e9 Author: damp11113 Date: Sun Dec 17 22:40:49 2023 +0700 new update 1.2 diff --git a/IDRBfavicon.ico b/IDRBfavicon.ico new file mode 100644 index 0000000..a104ca7 Binary files /dev/null and b/IDRBfavicon.ico differ diff --git a/IDRBfavicon.jpg b/IDRBfavicon.jpg new file mode 100644 index 0000000..2d6e37c Binary files /dev/null and b/IDRBfavicon.jpg differ diff --git a/IDRBfavicon.png b/IDRBfavicon.png new file mode 100644 index 0000000..ae59b7b Binary files /dev/null and b/IDRBfavicon.png differ diff --git a/IDRBlogo.png b/IDRBlogo.png new file mode 100644 index 0000000..938a5e9 Binary files /dev/null and b/IDRBlogo.png differ diff --git a/client.py b/client.py new file mode 100644 index 0000000..cc39a8d --- /dev/null +++ b/client.py @@ -0,0 +1,388 @@ +import time +from datetime import datetime +import cv2 +import dearpygui.dearpygui as dpg +import threading +import socket +import numpy as np +import pickle +import pyaudio +from pyogg import OpusDecoder +from damp11113 import CV22DPG + +librarylist = ["Opencv (opencv.org)", "PyOgg (TeamPyOgg)", "DearPyGui (hoffstadt)"] + +def calculate_speed(start_time, end_time, data_size): + elapsed_time = end_time - start_time + speed_kbps = (data_size / elapsed_time) / 1024 # Convert bytes to kilobytes + return speed_kbps + +def limit_string_in_line(text, limit): + lines = text.split('\n') + new_lines = [] + + for line in lines: + words = line.split() + new_line = '' + + for word in words: + if len(new_line) + len(word) <= limit: + new_line += word + ' ' + else: + new_lines.append(new_line.strip()) + new_line = word + ' ' + + if new_line: + new_lines.append(new_line.strip()) + + return '\n'.join(new_lines) + +class App: + def __init__(self): + self.RDS = None + self.device_name_output = "Speakers (2- USB Audio DAC )" + self.working = False + self.readchannel = 1 + self.firstrun = True + self.firststart = True + self.device_index_output = 0 + + def connecttoserver(self, sender, data): + dpg.configure_item("connectservergroup", show=False) + #protocol = dpg.get_value("serverprotocol") + dpg.configure_item("serverstatus", default_value='connecting...', color=(255, 255, 0)) + ip = dpg.get_value("serverip") + port = dpg.get_value("serverport") + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.connect((ip, port)) + except: + dpg.configure_item("connectbutton", show=True) + self.working = True + p = pyaudio.PyAudio() + + self.device_index_output = 0 + for i in range(p.get_device_count()): + dev = p.get_device_info_by_index(i) + if dev['name'] == self.device_name_output: + self.device_index_output = dev['index'] + break + + thread = threading.Thread(target=self.stream, args=(s, )) + thread.start() + + def disconnectserver(self, sender=None, data=None): + dpg.configure_item("disconnectbutton", show=False) + dpg.configure_item("serverstatus", default_value='disconnecting...', color=(255, 255, 0)) + self.working = False + dpg.configure_item("serverinfobutton", show=False) + dpg.configure_item("mediachannelselect", show=False) + dpg.configure_item("morerdsbutton", show=False) + dpg.configure_item("station_logo_config", show=False) + dpg.configure_item("RDSinfo", show=False) + dpg.configure_item("disconnectbutton", show=False) + dpg.configure_item("connectservergroup", show=True) + dpg.configure_item("serverstatus", default_value='disconnected', color=(255, 0, 0)) + self.firstrun = True + self.firststart = True + + def RDSshow(self): + try: + dpg.configure_item("RDSinfo", + default_value=f'{self.RDS["PS"]} ({self.RDS["ContentInfo"]["Codec"]} {self.RDS["ContentInfo"]["bitrate"] / 1000}Kbps {self.RDS["AudioMode"]})', + show=True) + dpg.configure_item("RDSPS", default_value="PS: " + self.RDS["PS"]) + dpg.configure_item("RDSRT", default_value="RT: " + limit_string_in_line(self.RDS["RT"], 120)) + dpg.configure_item("RDSCTlocal", default_value="Time Local: " + datetime.fromtimestamp(self.RDS["CT"]["Local"]).strftime('%H:%M:%S')) + dpg.configure_item("RDSCTUTC", default_value="Time UTC: " + datetime.fromtimestamp(self.RDS["CT"]["UTC"]).strftime('%H:%M:%S')) + try: + dpg.set_value("station_logo", CV22DPG( + cv2.imdecode(np.frombuffer(self.RDS["images"]["logo"], np.uint8), + cv2.IMREAD_COLOR))) + except: + dpg.configure_item("station_logo_config", show=False) + + except Exception as e: + pass + + def changechannel(self, sender, data): + dpg.configure_item("serverstatus", default_value='please wait...', color=(255, 255, 0)) + dpg.configure_item("station_logo_config", show=False) + self.readchannel = int(dpg.get_value(sender).split(" ")[0]) + self.firstrun = True + + p = pyaudio.PyAudio() + + self.device_index_output = 0 + for i in range(p.get_device_count()): + dev = p.get_device_info_by_index(i) + if dev['name'] == self.device_name_output: + self.device_index_output = dev['index'] + break + + def stream(self, socket): + opus_decoder = None + streamoutput = None + tfrpx = list(range(250)) + altfrpy = [0] * 250 + adcctfrpy = [0] * 250 + imcctfrpy = [0] * 250 + bytesconunt = 0 + bytesconunt_frame = 0 + start_time = time.time() + while True: + try: + if self.working: + #data = b'' + data = socket.recv(1580152) + #while True: + # part = socket.recv(1024) + # data += part + # if len(part) < 1024: + # # either 0 or end of data + # break + + bytesconunt += len(data) + + if bytesconunt_frame >= 10: + speed_kbps = calculate_speed(start_time, time.time(), bytesconunt) + dpg.configure_item("serverstatus", default_value=f'connected {int(speed_kbps)}Kbps ({len(data)})', color=(0, 255, 0)) + start_time = time.time() + bytesconunt_frame = 0 + bytesconunt = 0 + + if len(altfrpy) > 250: + altfrpy.pop(0) + altfrpy.append(len(data)) + dpg.set_value('transferatealldataplot', [tfrpx, altfrpy]) + + if len(data) == 0: + dpg.configure_item("serverstatus", default_value='lost connected', color=(255, 0, 0)) + socket.close() + self.disconnectserver() + break + + try: + datadecoded = pickle.loads(data) + except: + pass + + if len(imcctfrpy) > 250: + imcctfrpy.pop(0) + imcctfrpy.append(len(str(datadecoded["channel"][self.readchannel]["RDS"]["images"]))) + dpg.set_value('transferateimagesoncchannelplot', [tfrpx, imcctfrpy]) + + try: + if datadecoded["channel"][self.readchannel]["RDS"] != self.RDS: + self.RDS = datadecoded["channel"][self.readchannel]["RDS"] + rdshow = threading.Thread(target=self.RDSshow) + rdshow.start() + + dpg.configure_item("ServerListener", default_value="Listener: " + str(datadecoded["serverinfo"]["Listener"]) + " Users") + except: + pass + + if self.firstrun: + p = pyaudio.PyAudio() + opus_decoder = OpusDecoder() + opus_decoder.set_channels(self.RDS["ContentInfo"]["channel"]) + opus_decoder.set_sampling_frequency(self.RDS["ContentInfo"]["samplerates"]) + streamoutput = p.open(format=pyaudio.paInt16, channels=self.RDS["ContentInfo"]["channel"], rate=self.RDS["ContentInfo"]["samplerates"], output=True, output_device_index=self.device_index_output) + if len(datadecoded["channel"]) > 1: + channel_info = [] + for i in range(1, len(datadecoded["channel"]) + 1): + channel_info.append(f'{i} {datadecoded["channel"][i]["Station"]} ({datadecoded["channel"][i]["RDS"]["ContentInfo"]["Codec"]} {datadecoded["channel"][i]["RDS"]["ContentInfo"]["bitrate"] / 1000}Kbps {datadecoded["channel"][i]["RDS"]["AudioMode"]})') + dpg.configure_item("mediachannelselect", show=True, items=channel_info) + dpg.configure_item("morerdsbutton", show=True) + dpg.configure_item("serverinfobutton", show=True) + try: + dpg.set_value("station_logo", CV22DPG(cv2.imdecode(np.frombuffer(datadecoded["channel"][self.readchannel]["RDS"]["images"]["logo"], np.uint8), cv2.IMREAD_COLOR))) + dpg.configure_item("station_logo_config", show=True) + except: + dpg.configure_item("station_logo_config", show=False) + dpg.configure_item("disconnectbutton", show=True) + dpg.configure_item("RDSPI", default_value=f"PI: {hex(self.RDS['PI'])[2:].upper()}") + if self.firststart: + self.readchannel = datadecoded["mainchannel"] + dpg.configure_item("mediachannelselect", show=True, default_value="mainchannel") + dpg.configure_item("serverstatus", default_value='connected --Kbps (----)', color=(0, 255, 0)) + self.firstrun = False + self.firststart = False + + + if not self.firstrun: + decoded_pcm = opus_decoder.decode(memoryview(bytearray(datadecoded["channel"][self.readchannel]["Content"]))) + if len(adcctfrpy) > 250: + adcctfrpy.pop(0) + adcctfrpy.append(len(datadecoded["channel"][self.readchannel]["Content"])) + dpg.set_value('transferateaudiodataoncchannelplot', [tfrpx, adcctfrpy]) + + # Check if the decoded PCM is empty or not + if len(decoded_pcm) > 0: + pcm_to_write = np.frombuffer(decoded_pcm, dtype=np.int16) + + streamoutput.write(pcm_to_write.tobytes()) + else: + print("Decoded PCM is empty") + + bytesconunt_frame += 1 + else: + streamoutput.close() + socket.close() + break + except Exception as e: + if str(e) == "An error occurred while decoding an Opus-encoded packet: corrupted stream": + dpg.configure_item("serverstatus", default_value="Unable to decode audio data", color=(255, 0, 0)) + else: + print("connection lost", e) + try: + streamoutput.close() + except: + pass + socket.close() + self.disconnectserver() + break + + def window(self): + with dpg.window(label="IDRB", width=320, height=520, no_close=True): + dpg.add_button(label="Server info", callback=lambda: dpg.configure_item("Serverinfowindow", show=True), tag="serverinfobutton", show=False) + dpg.add_button(label="disconnect", callback=self.disconnectserver, tag="disconnectbutton", show=False) + dpg.add_text("not connect", tag="serverstatus", color=(255, 0, 0)) + dpg.add_combo([], label="Channel", tag="mediachannelselect", default_value="Main Channel", show=False, callback=self.changechannel) + dpg.add_spacer() + dpg.add_image("station_logo", show=False, tag="station_logo_config") + dpg.add_text("", tag="RDSinfo", show=False) + with dpg.child_window(tag="connectservergroup", label="Server", use_internal_label=True, height=105): + dpg.add_button(label="select server", tag="selectserverbutton") + dpg.add_input_text(label="server ip", tag="serverip", default_value="localhost") + dpg.add_input_int(label="port", tag="serverport", max_value=65535, default_value=6980) + #dpg.add_combo(["TCP", "Websocket"], label="protocol", tag="serverprotocol", default_value="TCP") + dpg.add_button(label="connect", callback=self.connecttoserver, tag="connectbutton") + dpg.add_spacer() + dpg.add_button(label="More RDS info", callback=lambda: dpg.configure_item("RDSwindow", show=True), tag="morerdsbutton", show=False) + + with dpg.window(label="IDRB RDS Info", tag="RDSwindow", show=False, width=250): + with dpg.tab_bar(): + with dpg.tab(label="Program"): + with dpg.child_window(label="Basic", use_internal_label=True, height=100): + dpg.add_text("PS: ...", tag="RDSPS") + dpg.add_text("PI: ...", tag="RDSPI") + dpg.add_text("RT: ...", tag="RDSRT") + + dpg.add_text("Time Local: ...", tag="RDSCTlocal") + dpg.add_text("Time UTC: ...", tag="RDSCTUTC") + with dpg.tab(label="EPG"): + pass + with dpg.tab(label="Images"): + pass + with dpg.tab(label="AS"): + pass + with dpg.tab(label="EOM"): + pass + + with dpg.window(label="IDRB Server Info", tag="Serverinfowindow", show=False): + dpg.add_text("Listener: ...", tag="ServerListener") + dpg.add_spacer() + #dpg.add_simple_plot(label="Transfer Rates", autosize=True, height=250, width=500, tag="transferateplot") + + + with dpg.plot(label="Transfer Rates", height=250, width=500): + # optionally create legend + dpg.add_plot_legend() + + # REQUIRED: create x and y axes + dpg.add_plot_axis(dpg.mvXAxis, label="x", tag="x_axis", no_gridlines=True) + dpg.add_plot_axis(dpg.mvYAxis, label="y", tag="y_axis1", no_gridlines=True) + dpg.add_plot_axis(dpg.mvYAxis, label="y", tag="y_axis2", no_gridlines=True) + dpg.add_plot_axis(dpg.mvYAxis, label="y", tag="y_axis3", no_gridlines=True) + + # series belong to a y axis + dpg.add_line_series([], [], label="All Data", parent="y_axis1", tag="transferatealldataplot") + dpg.add_line_series([], [], label="Audio Data", parent="y_axis2", tag="transferateaudiodataoncchannelplot") + dpg.add_line_series([], [], label="Images Data", parent="y_axis3", tag="transferateimagesoncchannelplot") + + with dpg.window(label="IDRB About", tag="aboutwindow", show=False, no_resize=True): + dpg.add_image("app_logo") + dpg.add_spacer() + dpg.add_text("IDRB (Internet Digital Radio Broadcasting System) Client") + dpg.add_spacer() + dpg.add_text(f"IDRB Client v1.2 Beta") + dpg.add_spacer() + + desc = "IDRB is a novel internet radio broadcasting alternative that uses HLS/DASH/HTTP streams, transferring over TCP/IP. This system supports images and RDS (Dynamic update) capabilities, enabling the transmission of station information. Additionally, it allows for setting station logos and images. IDRB offers multi-broadcasting functionalities and currently supports the Opus codec, with plans to incorporate PCM, MP2/3, AAC/AAC+, and more in the future, ensuring low delay. If you find this project intriguing, you can support it at damp11113.xyz/support." + + dpg.add_text(limit_string_in_line(desc, 75)) + + dpg.add_spacer() + with dpg.table(header_row=True): + + # use add_table_column to add columns to the table, + # table columns use slot 0 + dpg.add_table_column(label="Libraries") + + # add_table_next_column will jump to the next row + # once it reaches the end of the columns + # table next column use slot 1 + for i in librarylist: + with dpg.table_row(): + dpg.add_text(i) + + dpg.add_spacer(height=20) + dpg.add_text(f"Copyright (C) 2023 ThaiSDR All rights reserved. (MIT)") + + def menubar(self): + with dpg.viewport_menu_bar(): + with dpg.menu(label="File"): + dpg.add_menu_item(label="Exit", callback=lambda: self.exit()) + with dpg.menu(label="Settings"): + dpg.add_menu_item(label="StyleEditor", callback=dpg.show_style_editor) + with dpg.menu(label="Help"): + dpg.add_menu_item(label="About", callback=lambda: dpg.configure_item("aboutwindow", show=True)) + + def init(self): + dpg.create_context() + dpg.create_viewport(title=f'IDRB Client V1.2 Beta', width=1280, height=720, large_icon="IDRBfavicon.ico") # set viewport window + dpg.setup_dearpygui() + # -------------- add code here -------------- + noimage_texture_data = [] + for i in range(0, 128 * 128): + noimage_texture_data.append(20 / 255) + noimage_texture_data.append(0) + noimage_texture_data.append(20 / 255) + noimage_texture_data.append(20 / 255) + + with dpg.texture_registry(): + dpg.add_raw_texture(128, 128, noimage_texture_data, tag="station_logo", format=dpg.mvFormat_Float_rgb) + width, height, channels, data = dpg.load_image("IDRBlogo.png") + + dpg.add_static_texture(width=512, height=256, default_value=data, tag="app_logo") + + self.window() + self.menubar() + # ------------------------------------------- + dpg.show_viewport() + # Start a separate thread for a task + self.thread_stop_event = threading.Event() + + while dpg.is_dearpygui_running(): + self.render() + dpg.render_dearpygui_frame() + + # Signal the thread to stop and wait for it to finish + self.thread_stop_event.set() + dpg.destroy_context() + + def render(self): + # insert here any code you would like to run in the render loop + # you can manually stop by using stop_dearpygui() or self.exit() + dpg.fit_axis_data("x_axis") + dpg.fit_axis_data("y_axis1") + dpg.fit_axis_data("y_axis2") + dpg.fit_axis_data("y_axis3") + + def exit(self): + dpg.destroy_context() + + +app = App() +app.init() \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..7a13ba9 Binary files /dev/null and b/favicon.ico differ diff --git a/server.py b/server.py new file mode 100644 index 0000000..d9781c5 --- /dev/null +++ b/server.py @@ -0,0 +1,295 @@ +import socket +import time +import pyaudio +from pyogg import OpusBufferedEncoder +import numpy as np +import pickle +import threading +from damp11113 import scrollTextBySteps +from queue import Queue +from datetime import datetime, timezone +import cv2 + +# create tcp +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +# wait for connection +server_port = ('localhost', 6980) +s.bind(server_port) + +s.listen(1) + + +p = pyaudio.PyAudio() + +sample_rate = 48000 +bytes_per_sample = p.get_sample_size(pyaudio.paInt16) + +codec = "opus" # opus, pcm, aac + +# Create an Opus encoder +bitrates = 64000 #Kbps +channel = 2 # Stereo +framesize = 60 + + +if bitrates >= 500000: + bitrates = 500000 + +device_name_input = "Line 5 (Virtual Audio Cable)" +device_index_input = 0 +for i in range(p.get_device_count()): + dev = p.get_device_info_by_index(i) + if dev['name'] == device_name_input: + device_index_input = dev['index'] + break + +device_name_input = "Line 4 (Virtual Audio Cable)" +device_index_input2 = 0 +for i in range(p.get_device_count()): + dev = p.get_device_info_by_index(i) + if dev['name'] == device_name_input: + device_index_input2 = dev['index'] + break + +streaminput = p.open(format=pyaudio.paInt16, channels=2, rate=sample_rate, input=True, input_device_index=device_index_input) +streaminput2 = p.open(format=pyaudio.paInt16, channels=2, rate=sample_rate, input=True, input_device_index=device_index_input2) + + +def encodelogoimage(path, quality=50): + image = cv2.resize(cv2.imread(path), (128, 128)) + # Encode the image as JPEG with higher compression (lower quality) + encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality] # Adjust quality (50 is just an example) + result, encoded_image = cv2.imencode('.jpg', image, encode_param) + encoded_bytes = np.array(encoded_image).tobytes() + return encoded_bytes + + +RDS = { + "PS": "DPRadio", + "RT": "Testing internet radio", + "PI": 0x27C8, # static + "PTY": 0, + "PTY+": "Testing", + "Country": "TH", + "Coverage": "All", + "CT": { + "Local": None, + "UTC": None, + }, + "PIN": 12345, + "TMC": { + "TP": False, + "TA": False, + "Messages": None + }, + "ECC": None, + "LIC": None, + "AudioMode": "Stereo", # mono, stereo, surround 5.1/7.1, HRTF + "ArtificialHead": False, + "Compressed": False, + "DyPTY": False, + "EPG": None, + "AS": [ # AS = Alternative Server + # can add more server here + ], + "EON": [ + # can add more here + ], + "ContentInfo": { + "Codec": codec, + "bitrate": bitrates, + "channel": channel, + "samplerates": sample_rate + }, + "images": { + "logo": encodelogoimage(r"C:\Users\sansw\3D Objects\dpstream iptv logo.png") + } +} + +RDS2 = { + "PS": "DPTest", + "RT": "Testing internet radio", + "PI": 0x27C6, + "PTY": 0, + "PTY+": "Testing", + "Country": "TH", + "Coverage": "All", + "CT": { + "Local": None, + "UTC": None, + }, + "PIN": 12345, + "TMC": { + "TP": False, + "TA": False, + "Messages": None + }, + "ECC": None, + "LIC": None, + "AudioMode": "Stereo", # mono, stereo, surround 5.1/7.1, HRTF + "ArtificialHead": False, + "Compressed": False, + "DyPTY": False, + "EPG": None, + "AS": [ # AS = Alternative Server + # can add more server here + ], + "EON": [ + # can add more server here + ], + "ContentInfo": { + "Codec": codec, + "bitrate": 8000, + "channel": channel, + "samplerates": sample_rate + }, + "images": { + "logo": None + } +} + + +lock = threading.Lock() + +def update_RDS(): + global RDS + with lock: + while True: + pstext = "DPRadio Testing Broadcasting " + for i in range(0, len(pstext)): + RDS["PS"] = scrollTextBySteps(pstext, i) + time.sleep(1) + +def update_RDS_time(): + global RDS + while True: + RDS["CT"]["Local"] = datetime.now().timestamp() + RDS["CT"]["UTC"] = datetime.utcnow().timestamp() + time.sleep(1) + +def update_RDS_images(): + global RDS + while True: + RDS["images"]["logo"] = encodelogoimage(r"C:\Users\sansw\3D Objects\dpstream iptv logo.png", 25) + time.sleep(10) + RDS["images"]["logo"] = encodelogoimage(r"C:\Users\sansw\3D Objects\140702_hi-res-logo.jpg", 25) + time.sleep(10) + RDS["images"]["logo"] = encodelogoimage(r"IDRBfavicon.jpg", 25) + time.sleep(10) + +thread = threading.Thread(target=update_RDS) +thread.start() + +thread2 = threading.Thread(target=update_RDS_time) +thread2.start() + +thread4 = threading.Thread(target=update_RDS_images) +thread4.start() + +# Create a shared queue for encoded audio packets +channel1 = Queue() + +channel2 = Queue() + +# Function to continuously encode audio and put it into the queue +def encode_audio(): + encoder = OpusBufferedEncoder() + encoder.set_application("audio") + encoder.set_sampling_frequency(sample_rate) + encoder.set_channels(channel) + encoder.set_bitrates(bitrates) + encoder.set_frame_size(framesize) + + while True: + pcm = np.frombuffer(streaminput.read(1024, exception_on_overflow=False), dtype=np.int16) + + encoded_packets = encoder.buffered_encode(memoryview(bytearray(pcm))) + for encoded_packet, _, _ in encoded_packets: + # Put the encoded audio into the buffer + channel1.put(encoded_packet.tobytes()) + + +def encode_audio2(): + encoder2 = OpusBufferedEncoder() + encoder2.set_application("audio") + encoder2.set_sampling_frequency(sample_rate) + encoder2.set_channels(channel) + encoder2.set_bitrates(8000) + encoder2.set_frame_size(framesize) + + while True: + pcm2 = np.frombuffer(streaminput2.read(1024, exception_on_overflow=False), dtype=np.int16) + + encoded_packets = encoder2.buffered_encode(memoryview(bytearray(pcm2))) + for encoded_packet, _, _ in encoded_packets: + # Put the encoded audio into the buffer + channel2.put(encoded_packet.tobytes()) + +audio_thread = threading.Thread(target=encode_audio) +audio_thread.start() + +audio_thread2 = threading.Thread(target=encode_audio2) +audio_thread2.start() + +connectionlist = [] +connected_users = 0 + +def handle_client(): + global connected_users + try: + while True: + # Get the encoded audio from the buffer + ENchannel1 = channel1.get() + ENchannel2 = channel2.get() + content = { + "mainchannel": 1, + "channel": { + 1: { + "Station": "DPRadio+", + "ContentSize": len(ENchannel1), + "Content": ENchannel1, + "RDS": RDS + }, + 2: { + "Station": "DPTest", + "ContentSize": len(ENchannel2), + "Content": ENchannel2, + "RDS": RDS2 + } + }, + "serverinfo": { + "Listener": connected_users, + "Country": "TH", + "Startat": time.time() + } + } + #connection.sendall(pickle.dumps(content)) + for i in connectionlist: + try: + i.sendall(pickle.dumps(content)) + except Exception as e: + #print(f'Error sending data to {i.getpeername()}: {e}') + # Remove disconnected client from the list + if i in connectionlist: + i.close() + connectionlist.remove(i) + connected_users -= 1 + except Exception as e: + print(f'Error: {e}') + +first = True + +# Your main server logic using threading for handling connections +while True: + print("Waiting for a connection...") + connection, client_address = s.accept() + print(f"Connected to {client_address}") + + connectionlist.append(connection) + connected_users += 1 + if first: + # Start a: new thread to handle the client + client_thread = threading.Thread(target=handle_client) + #client_thread.daemon = True # Set the thread as a daemon so it exits when the main thread exits + client_thread.start() + first = False \ No newline at end of file