diff --git a/Client/__pycache__/appcomponent.cpython-310.pyc b/Client/__pycache__/appcomponent.cpython-310.pyc new file mode 100644 index 0000000..6099fb9 Binary files /dev/null and b/Client/__pycache__/appcomponent.cpython-310.pyc differ diff --git a/Client/__pycache__/utils.cpython-310.pyc b/Client/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000..23aa5d9 Binary files /dev/null and b/Client/__pycache__/utils.cpython-310.pyc differ diff --git a/Client/appcomponent.py b/Client/appcomponent.py new file mode 100644 index 0000000..2fd8dcb --- /dev/null +++ b/Client/appcomponent.py @@ -0,0 +1,166 @@ +import dearpygui.dearpygui as dpg + +from utils import * + +librarylist = ["Opencv (opencv.org)", "PyOgg (TeamPyOgg) (Forked)", "DearPyGui (hoffstadt)"] + +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("Logo not available", tag="logostatus", color=(255, 0, 0), show=False) + dpg.add_text("", tag="RDSinfo", show=False) + + with dpg.child_window(tag="connectservergroup", label="Server", use_internal_label=True, height=130): + dpg.add_button(label="select server", tag="selectserverbutton", callback=self.pubserverselectopen) + 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", "ZeroMQ", "ZeroMQ (WS)"], 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("Server: ...", tag="ServerNamedisp") + dpg.add_text("Description: ...", tag="ServerDescdisp") + 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.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.6.1 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-2024 ThaiSDR All rights reserved. (GPLv3)") + + with dpg.window(label="IDRB Public Server", tag="pubserverselectwindow", show=False, modal=True, popup=True, height=500, width=1200): + dpg.add_text("N/A", tag="pubserverselectstatus") + dpg.add_input_text(hint="search server here", tag="serversearchinput") + dpg.add_button(label="search", callback=lambda: self.pubserverselectsearch(dpg.get_value("serversearchinput"))) + with dpg.table(header_row=True, tag="pubserverlist"): + dpg.add_table_column(label="IP") + dpg.add_table_column(label="Server") + dpg.add_table_column(label="Description") + dpg.add_table_column(label="listeners") + dpg.add_spacer() + dpg.add_button(label="connect", callback=self.connecttoserverwithpubselect, tag="connectbuttonpubserverselect", show=False) + + + with dpg.window(label="IDRB Evaluation", tag="evaluationwindow", show=False): + with dpg.tab_bar(): + with dpg.tab(label="Audio"): + with dpg.plot(label="FFT Spectrum", height=250, width=500): + # optionally create legend + dpg.add_plot_legend() + + # REQUIRED: create x and y axes + dpg.add_plot_axis(dpg.mvXAxis, tag="x_axis_1", no_gridlines=True, label="Frequencies") + dpg.add_plot_axis(dpg.mvYAxis, tag="audioL_y_axis", no_gridlines=True) + dpg.add_plot_axis(dpg.mvYAxis, tag="audioR_y_axis", no_gridlines=True) + + dpg.set_axis_limits("audioL_y_axis", 0, 2500000) + dpg.set_axis_limits("audioR_y_axis", 0, 2500000) + + # series belong to a y axis + dpg.add_line_series([], [], label="Left Channel", parent="audioL_y_axis", tag="audioinfoleftplot") + dpg.add_line_series([], [], label="Right Channel", parent="audioR_y_axis", tag="audioinforightplot") + + with dpg.tab(label="Network"): + dpg.add_text("NA", tag="codecbitratestatus", show=True) + dpg.add_text("Buffer 0/0", tag="bufferstatus", show=True) + 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, tag="x_axis", no_gridlines=True) + dpg.add_plot_axis(dpg.mvYAxis, tag="y_axis1", no_gridlines=True) + dpg.add_plot_axis(dpg.mvYAxis, tag="y_axis2", no_gridlines=True) + dpg.add_plot_axis(dpg.mvYAxis, 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="Password Required", tag="requestpasswordpopup", modal=True, no_resize=True, + no_close=True, no_move=True, show=False): + dpg.add_text("This channel is encrypt! Please enter password for decrypt.") + dpg.add_spacer() + dpg.add_input_text(label="password", tag="requestpasswordinputpopup") + dpg.add_spacer() + dpg.add_button(label="confirm", callback=self.submitpassworddecrypt) + + with dpg.window(label="Config", tag="configwindow", show=False, width=500): + dpg.add_text("Please restart software when configured") + with dpg.tab_bar(): + with dpg.tab(label="Audio"): + dpg.add_combo([], label="Output Device", tag="selectaudiooutputdevicecombo", + callback=self.changeaudiodevice) + +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="View"): + dpg.add_menu_item(label="Evaluation", callback=lambda: dpg.configure_item("evaluationwindow", show=True)) + with dpg.menu(label="Settings"): + dpg.add_menu_item(label="Config", callback=lambda: dpg.configure_item("configwindow", show=True)) + dpg.add_spacer() + 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)) diff --git a/Client/client.py b/Client/client.py index dba7344..c8c3aa3 100644 --- a/Client/client.py +++ b/Client/client.py @@ -1,81 +1,38 @@ +""" +This file is part of IDRB Project. + +IDRB Project is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +IDRB Project is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with IDRB Project. If not, see . +""" +import queue import time from datetime import datetime import cv2 import dearpygui.dearpygui as dpg import threading import socket -import numpy as np +import requests import pickle import pyaudio import zmq from pyogg import OpusDecoder -from Crypto.Cipher import AES -from Crypto.Protocol.KDF import scrypt import configparser import ctypes +import zlib -librarylist = ["Opencv (opencv.org)", "PyOgg (TeamPyOgg)", "DearPyGui (hoffstadt)"] +from utils import * +import appcomponent -def CV22DPG(cv2_array): - try: - if cv2_array is None or len(cv2_array.shape) < 3: - print("Invalid or empty array received.") - return None - - if len(cv2_array.shape) == 2: - cv2_array = cv2_array[:, :, np.newaxis] - - data = np.flip(cv2_array, 2) - data = data.ravel() - data = np.asfarray(data, dtype='f') - return np.true_divide(data, 255.0) - except Exception as e: - print("Error in CV22DPG:", e) - return None - -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) - -def unpad_message(padded_message): - padding_length = padded_message[-1] - return padded_message[:-padding_length] - -def decrypt_data(encrypted_message, password, salt, iv): - # Derive the key from the password and salt - key = scrypt(password, salt, key_len=32, N=2 ** 14, r=8, p=1) - - # Initialize AES cipher in CBC mode - cipher = AES.new(key, AES.MODE_CBC, iv) - - # Decrypt the message - decrypted_message = cipher.decrypt(encrypted_message) - - # Unpad the decrypted message - unpadded_message = unpad_message(decrypted_message) - - return unpadded_message class App: def __init__(self): @@ -83,6 +40,9 @@ class App: self.config = configparser.ConfigParser() self.config.read("config.ini") self.device_name_output = self.config["audio"]["device"] + self.buffersize = 64 # can configable + + self.working = False self.readchannel = 1 self.firstrun = True @@ -96,14 +56,35 @@ class App: self.cprotocol = None self.cciswaitlogoim = True self.ccthreadlogorecisworking = False + self.ccserversecount = 0 + self.lsitem = None + self.ccconwithpubselect = False + self.buffer = queue.Queue(maxsize=self.buffersize) + self.okbuffer = False + self.firstrunbuffer = True + + def connecttoserverwithpubselect(self, sender, data): + self.ccconwithpubselect = True + dpg.configure_item("pubserverselectwindow", show=False) + self.connecttoserver(None, None) def connecttoserver(self, sender, data): dpg.configure_item("connectservergroup", show=False) - protocol = dpg.get_value("serverprotocol") - self.cprotocol = protocol dpg.configure_item("serverstatus", default_value='connecting...', color=(255, 255, 0)) - ip = dpg.get_value("serverip") - port = dpg.get_value("serverport") + if self.ccconwithpubselect: + serverlabel = str(dpg.get_item_configuration(self.lsitem)["label"]) + protocol = serverlabel.split("|")[0].strip() + ip = serverlabel.split("|")[1].split(":")[0].strip() + port = serverlabel.split("|")[1].split(":")[1].strip() + + self.ccconwithpubselect = False + else: + protocol = dpg.get_value("serverprotocol").strip() + ip = dpg.get_value("serverip").strip() + port = dpg.get_value("serverport") + + self.cprotocol = protocol + if protocol == "TCP": s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: @@ -145,6 +126,7 @@ class App: dpg.configure_item("disconnectbutton", show=False) dpg.configure_item("connectservergroup", show=True) dpg.configure_item("serverstatus", default_value='disconnected', color=(255, 0, 0)) + dpg.configure_item("logostatus", show=False) self.firstrun = True self.firststart = True self.ccdecryptpassword = None @@ -153,12 +135,16 @@ class App: self.ccisdecryptpassword = None self.cciswaitlogoim = True self.ccthreadlogorecisworking = False + self.buffer = queue.Queue(maxsize=self.buffersize) + self.okbuffer = False + self.firstrunbuffer = 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"]})', + default_value=f'{self.RDS["PS"]} ({self.RDS["ContentInfo"]["Codec"].upper()} {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')) @@ -175,10 +161,11 @@ class App: logoreciveprocessingthread.start() else: if not self.RDS["images"]["logo"]["lazy"]: - dpg.set_value("station_logo", CV22DPG(cv2.imdecode(np.frombuffer(self.RDS["images"]["logo"], np.uint8), cv2.IMREAD_COLOR))) + dpg.set_value("station_logo", CV22DPG(cv2.imdecode(np.frombuffer(self.RDS["images"]["logo"]["contents"], np.uint8), cv2.IMREAD_COLOR))) + dpg.configure_item("logostatus", show=False) except Exception as e: dpg.configure_item("station_logo_config", show=False) - print(e) + #print(e) except Exception as e: pass @@ -243,6 +230,90 @@ class App: self.config["audio"]["device"] = dpg.get_value(sender) self.config.write(open('config.ini', 'w')) + def pubserverselectone(self, sender, data): + if data == False: + dpg.configure_item("connectbuttonpubserverselect", show=False) + return + else: + dpg.configure_item("connectbuttonpubserverselect", show=True) + + if self.lsitem == None: + self.lsitem = sender + + if self.lsitem != sender: + dpg.set_value(self.lsitem, False) + self.lsitem = sender + + if dpg.get_item_configuration(self.lsitem)["label"] == "N/A": + dpg.configure_item("connectbuttonpubserverselect", show=False) + + + def pubserverselectsearch(self, serversearch="", limit=10): + self.lsitem = None + if serversearch == "": + dpg.configure_item("pubserverselectstatus", default_value="Please wait...", color=(255, 255, 0)) + else: + dpg.configure_item("pubserverselectstatus", default_value="Searching...", color=(255, 255, 0)) + + if self.ccserversecount != 0: + # clear list + for i in range(self.ccserversecount): + dpg.delete_item(f"pubserverid{i}") + + self.ccserversecount = 0 + + if serversearch == "": + response = requests.get(f"https://thaisdr.damp11113.xyz/api/idrbdir/getallstation?limit={limit}") + else: + response = requests.get(f"https://thaisdr.damp11113.xyz/api/idrbdir/getallstation?limit={limit}&serversearch={serversearch}") + + # Check if the request was successful (status code 200) + if response.status_code == 200: + # Parse JSON data + allstationdata = response.json() + + # Iterate over each station + for server_id, server_details in allstationdata.items(): + with dpg.table_row(parent="pubserverlist", tag=f"pubserverid{self.ccserversecount}"): + if server_details['ServerURL'] == "" or server_details['ServerPort'] == "" or server_details['ServerProtocol'] == "": + dpg.add_selectable(label=f"N/A", span_columns=True, disable_popup_close=True, callback=self.pubserverselectone) + else: + dpg.add_selectable(label=f"{server_details['ServerProtocol']} | {server_details['ServerURL']}:{server_details['ServerPort']}", span_columns=True, disable_popup_close=True, callback=self.pubserverselectone) + + dpg.add_selectable(label=server_details["ServerName"], span_columns=True, disable_popup_close=True, callback=self.pubserverselectone) + dpg.add_selectable(label=server_details["ServerDesc"], span_columns=True, disable_popup_close=True, callback=self.pubserverselectone) + dpg.add_selectable(label=server_details["ConnectionUser"], span_columns=True, disable_popup_close=True, callback=self.pubserverselectone) + + + self.ccserversecount += 1 + + dpg.configure_item("pubserverselectstatus", default_value=f"Founded {self.ccserversecount} server", color=(0, 255, 0)) + + def pubserverselectopen(self, sender, data): + dpg.configure_item("pubserverselectwindow", show=True) + self.pubserverselectsearch() + + def streambuffer(self, socket): + consecutive_above_threshold = 0 # Counter to track consecutive iterations above threshold + tolerance_iterations = 5 # Number of consecutive iterations required above threshold + while self.working: + if self.cprotocol == "TCP": + tempdata = b'' + # data = socket.recv(1580152) + while True: + part = socket.recv(1024) + tempdata += part + if len(part) < 1024: + # either 0 or end of data + break + self.buffer.put(tempdata) + elif self.cprotocol == "ZeroMQ": + self.buffer.put(socket.recv()) + else: + self.buffer.put(b"") + + dpg.configure_item("bufferstatus", default_value=f'Buffer: {self.buffer.qsize()}/{self.buffersize}') + def stream(self, socket): opus_decoder = None streamoutput = None @@ -252,34 +323,41 @@ class App: imcctfrpy = [0] * 2500 bytesconunt = 0 bytesconunt_frame = 0 + codecbytesconunt = 0 start_time = time.time() evaluation_audio_X = None decodecodec = None + + + SBT = threading.Thread(target=self.streambuffer, args=(socket,)) + SBT.start() + while True: try: if self.working: - if self.cprotocol == "TCP": - 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 - elif self.cprotocol == "ZeroMQ": - data = socket.recv() + if self.buffer.not_empty: + data = self.buffer.get() else: - data = b"" + continue bytesconunt += len(data) + if bytesconunt_frame >= 10: - speed_kbps = calculate_speed(start_time, time.time(), bytesconunt) + stoptime = time.time() + speed_kbps = calculate_speed(start_time, stoptime, bytesconunt) dpg.configure_item("serverstatus", default_value=f'connected {int(speed_kbps)}Kbps ({len(data)})', color=(0, 255, 0)) + + codec_kbps = calculate_throughput(start_time, stoptime, codecbytesconunt) + + dpg.configure_item("codecbitratestatus", default_value=f'{self.RDS["ContentInfo"]["Codec"].upper()} {self.RDS["ContentInfo"]["bitrate"] / 1000}/{codec_kbps:.2f} Kbps') + + dpg.configure_item("bufferstatus", default_value=f'Buffer: {self.buffer.qsize()}/{self.buffersize}') + start_time = time.time() bytesconunt_frame = 0 bytesconunt = 0 + codecbytesconunt = 0 if len(altfrpy) > 250: altfrpy.pop(0) @@ -310,7 +388,9 @@ class App: break try: - datadecoded = pickle.loads(data) + decompressed_data = zlib.decompress(data) + + datadecoded = pickle.loads(decompressed_data) except: pass @@ -326,25 +406,33 @@ class App: rdshow.start() dpg.configure_item("ServerListener", default_value="Listener: " + str(datadecoded["serverinfo"]["Listener"]) + " Users") + dpg.configure_item("ServerNamedisp", default_value="Server: " + str(datadecoded["serverinfo"]["RDS"]["ServerName"])) + dpg.configure_item("ServerDescdisp", default_value="Description: " + str(datadecoded["serverinfo"]["RDS"]["ServerDesc"])) + except: pass if self.firstrun: decodecodec = datadecoded["channel"][self.readchannel]["RDS"]["ContentInfo"]["Codec"] + if decodecodec.upper() == "OPUS": opus_decoder = OpusDecoder() opus_decoder.set_channels(self.RDS["ContentInfo"]["channel"]) opus_decoder.set_sampling_frequency(self.RDS["ContentInfo"]["samplerates"]) + streamoutput = self.paudio.open(format=pyaudio.paInt16, channels=self.RDS["ContentInfo"]["channel"], rate=self.RDS["ContentInfo"]["samplerates"], output=True, output_device_index=self.device_index_output) evaluation_audio_X = np.fft.fftfreq(1024, 1.0 / self.RDS["ContentInfo"]["samplerates"])[:1024 // 2] + if len(datadecoded["channel"]) > 1: channel_info = [] for i in range(1, len(datadecoded["channel"]) + 1): channel_info.append(f'{i} {"[Encrypt]" if datadecoded["channel"][i]["Encrypt"] else "[No Encrypt]"} {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) dpg.configure_item("logostatus", show=True) + try: if self.RDS["images"]["logo"]["lazy"] and not self.ccthreadlogorecisworking: if not self.RDS["images"]["logo"]["contents"] == b'' or \ @@ -357,8 +445,10 @@ class App: else: if not self.RDS["images"]["logo"]["lazy"]: dpg.set_value("station_logo", CV22DPG( - cv2.imdecode(np.frombuffer(self.RDS["images"]["logo"], np.uint8), + cv2.imdecode(np.frombuffer(self.RDS["images"]["logo"]["contents"], np.uint8), cv2.IMREAD_COLOR))) + dpg.configure_item("station_logo_config", show=True) + dpg.configure_item("logostatus", show=False) except: dpg.configure_item("station_logo_config", show=False) dpg.configure_item("disconnectbutton", show=True) @@ -403,6 +493,8 @@ class App: dpg.configure_item("serverstatus", default_value="Decrypt Error", color=(255, 0, 0)) if self.ccisdecrypt or not self.ccisencrypt: + codecbytesconunt += len(data) + if decodecodec.upper() == "OPUS": decoded_pcm = opus_decoder.decode(memoryview(bytearray(data))) else: # pcm @@ -435,7 +527,9 @@ class App: dpg.set_value('transferateaudiodataoncchannelplot', [tfrpx, adcctfrpy]) bytesconunt_frame += 1 + else: + SBT.join() streamoutput.close() if self.cprotocol == "TCP": socket.close() @@ -484,143 +578,9 @@ class App: else: socket.close() self.disconnectserver() - raise + 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("Logo not available", tag="logostatus", color=(255, 0, 0), show=False) - dpg.add_text("", tag="RDSinfo", show=False) - with dpg.child_window(tag="connectservergroup", label="Server", use_internal_label=True, height=130): - 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", "ZeroMQ", "ZeroMQ (WS)"], 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.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.5 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 damp11113 All rights reserved. (GPLv3)") - - with dpg.window(label="Password Required", tag="requestpasswordpopup", modal=True, no_resize=True, no_close=True, no_move=True, show=False): - dpg.add_text("This channel is encrypt! Please enter password for decrypt.") - dpg.add_spacer() - dpg.add_input_text(label="password", tag="requestpasswordinputpopup") - dpg.add_spacer() - dpg.add_button(label="confirm", callback=self.submitpassworddecrypt) - - with dpg.window(label="IDRB Evaluation", tag="evaluationwindow", show=False): - with dpg.tab_bar(): - with dpg.tab(label="Audio"): - with dpg.plot(label="FFT Spectrum", height=250, width=500): - # optionally create legend - dpg.add_plot_legend() - - # REQUIRED: create x and y axes - dpg.add_plot_axis(dpg.mvXAxis, tag="x_axis_1", no_gridlines=True, label="Frequencies") - dpg.add_plot_axis(dpg.mvYAxis, tag="audioL_y_axis", no_gridlines=True) - dpg.add_plot_axis(dpg.mvYAxis, tag="audioR_y_axis", no_gridlines=True) - - dpg.set_axis_limits("audioL_y_axis", 0, 2500000) - dpg.set_axis_limits("audioR_y_axis", 0, 2500000) - - # series belong to a y axis - dpg.add_line_series([], [], label="Left Channel", parent="audioL_y_axis", tag="audioinfoleftplot") - dpg.add_line_series([], [], label="Right Channel", parent="audioR_y_axis", tag="audioinforightplot") - - with dpg.tab(label="Network"): - 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, tag="x_axis", no_gridlines=True) - dpg.add_plot_axis(dpg.mvYAxis, tag="y_axis1", no_gridlines=True) - dpg.add_plot_axis(dpg.mvYAxis, tag="y_axis2", no_gridlines=True) - dpg.add_plot_axis(dpg.mvYAxis, 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="Config", tag="configwindow", show=False, width=500): - dpg.add_text("Please restart software when configured") - with dpg.tab_bar(): - with dpg.tab(label="Audio"): - dpg.add_combo([], label="Output Device", tag="selectaudiooutputdevicecombo", callback=self.changeaudiodevice) - - 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="View"): - dpg.add_menu_item(label="Evaluation", callback=lambda: dpg.configure_item("evaluationwindow", show=True)) - with dpg.menu(label="Settings"): - dpg.add_menu_item(label="Config", callback=lambda: dpg.configure_item("configwindow", show=True)) - dpg.add_spacer() - 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): if self.config["debug"]["hideconsole"] == "true": ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0) @@ -628,7 +588,7 @@ class App: ctypes.CDLL("opus.dll") dpg.create_context() - dpg.create_viewport(title=f'IDRB Client v1.5 Beta', width=1280, height=720, large_icon="IDRBfavicon.ico") # set viewport window + dpg.create_viewport(title=f'IDRB Client v1.6.1 Beta', width=1280, height=720, large_icon="IDRBfavicon.ico", clear_color=(43, 45, 48)) # set viewport window dpg.setup_dearpygui() # -------------- add code here -------------- noimage_texture_data = [] @@ -643,9 +603,14 @@ class App: width, height, channels, data = dpg.load_image("IDRBlogo.png") dpg.add_static_texture(width=512, height=256, default_value=data, tag="app_logo") + dpg.add_static_texture(width=512, height=256, default_value=data, tag="app_logo_background") - self.window() - self.menubar() + with dpg.window(no_background=True, no_title_bar=True, no_move=True, no_resize=True, tag="backgroundviewportlogo"): + dpg.add_image("app_logo_background") + dpg.add_text("ThaiSDR Solutions", pos=(230, 230)) + + appcomponent.window(self) + appcomponent.menubar(self) num_devices = self.paudio.get_device_count() output_devices = [] @@ -672,16 +637,22 @@ class App: 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") + try: + dpg.fit_axis_data("x_axis") + dpg.fit_axis_data("y_axis1") + dpg.fit_axis_data("y_axis2") + dpg.fit_axis_data("y_axis3") - dpg.fit_axis_data("x_axis_1") + dpg.fit_axis_data("x_axis_1") + + dpg.configure_item("backgroundviewportlogo", pos=(dpg.get_viewport_width() - 550, dpg.get_viewport_height() - 300)) + except: + pass def exit(self): dpg.destroy_context() + app = App() app.init() diff --git a/Client/utils.py b/Client/utils.py new file mode 100644 index 0000000..6bf6f06 --- /dev/null +++ b/Client/utils.py @@ -0,0 +1,69 @@ +from Crypto.Cipher import AES +from Crypto.Protocol.KDF import scrypt +import numpy as np + +def CV22DPG(cv2_array): + try: + if cv2_array is None or len(cv2_array.shape) < 3: + print("Invalid or empty array received.") + return None + + if len(cv2_array.shape) == 2: + cv2_array = cv2_array[:, :, np.newaxis] + + data = np.flip(cv2_array, 2) + data = data.ravel() + data = np.asfarray(data, dtype='f') + return np.true_divide(data, 255.0) + except Exception as e: + print("Error in CV22DPG:", e) + return None + +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 calculate_throughput(start_time, end_time, total_bytes): + duration = end_time - start_time + throughput_kbps = (total_bytes * 8) / (duration * 1000) + return throughput_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) + +def unpad_message(padded_message): + padding_length = padded_message[-1] + return padded_message[:-padding_length] + +def decrypt_data(encrypted_message, password, salt, iv): + # Derive the key from the password and salt + key = scrypt(password, salt, key_len=32, N=2 ** 14, r=8, p=1) + + # Initialize AES cipher in CBC mode + cipher = AES.new(key, AES.MODE_CBC, iv) + + # Decrypt the message + decrypted_message = cipher.decrypt(encrypted_message) + + # Unpad the decrypted message + unpadded_message = unpad_message(decrypted_message) + + return unpadded_message \ No newline at end of file diff --git a/Server/Encoder.py b/Server/Encoder.py new file mode 100644 index 0000000..2cff720 --- /dev/null +++ b/Server/Encoder.py @@ -0,0 +1,100 @@ +""" +This file is part of IDRB Project. + +IDRB Project is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +IDRB Project is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with IDRB Project. If not, see . +""" + +import threading +from queue import Queue +from pyogg import OpusBufferedEncoder +import numpy as np +import pyaudio +import RDS as _RDS +import logging + +EncoderLog = logging.getLogger("Encoder") + +EncoderLog.info("Init audio system...") + +p = pyaudio.PyAudio() + +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=48000, input=True, input_device_index=device_index_input) +streaminput2 = p.open(format=pyaudio.paInt16, channels=2, rate=48000, input=True, input_device_index=device_index_input2) + +# 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(_RDS.RDS["ContentInfo"]["samplerates"]) + encoder.set_channels(_RDS.RDS["ContentInfo"]["channel"]) + encoder.set_bitrates(_RDS.RDS["ContentInfo"]["bitrate"]) + encoder.set_frame_size(60) + encoder.set_bitrate_mode("VBR") + encoder.set_compresion_complex(10) + + 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(_RDS.RDS2["ContentInfo"]["samplerates"]) + encoder2.set_channels(_RDS.RDS2["ContentInfo"]["channel"]) + encoder2.set_bitrates(_RDS.RDS2["ContentInfo"]["bitrate"]) + encoder2.set_frame_size(60) + + 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()) + + #channel2.put(pcm2.tobytes()) # if you use pcm + +def StartEncoder(): + EncoderLog.info("Starting encoder") + audio_thread = threading.Thread(target=encode_audio) + audio_thread2 = threading.Thread(target=encode_audio2) + + audio_thread.start() + audio_thread2.start() \ No newline at end of file diff --git a/Server/RDS.py b/Server/RDS.py index 551c724..8833964 100644 --- a/Server/RDS.py +++ b/Server/RDS.py @@ -1,9 +1,30 @@ +""" +This file is part of IDRB Project. + +IDRB Project is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +IDRB Project is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with IDRB Project. If not, see . +""" + import time from datetime import datetime import cv2 import numpy as np from damp11113 import scrollTextBySteps +import threading +import Settings +import logging +RDSLog = logging.getLogger("RDS") def encodelogoimage(path, quality=50): image = cv2.resize(cv2.imread(path), (128, 128)) @@ -28,13 +49,22 @@ def sendimagelazy(data, chunk_size, RDSimage, imagetype, delay=0.1): RDSimage["images"][imagetype]["contents"] = chunk RDSimage["images"][imagetype]["part"]["current"] = i - print(f"[contentpart={chunk}, currentpart={i}, totalpart={total_chunks}]") + #print(f"[contentpart={chunk}, currentpart={i}, totalpart={total_chunks}]") time.sleep(delay) RDSimage["images"][imagetype]["contents"] = b'' RDSimage["images"][imagetype]["part"]["current"] = 0 RDSimage["images"][imagetype]["part"]["total"] = 0 +ServerRDS = { + "ServerName": Settings.ServerName, + "ServerDesc": Settings.ServerDesc, + "Country": Settings.Country, + "AS": [ # AS = Alternative Server + # can add more server here + ] +} + RDS = { "PS": "DPRadio", "RT": "Testing internet radio", @@ -60,12 +90,12 @@ RDS = { "Compressed": False, "DyPTY": False, "EPG": None, - "AS": [ # AS = Alternative Server - # can add more server here - ], "EON": [ # can add more here ], + "AS": [ # AS = Alternative Server + # can add more server here + ], "ContentInfo": { "Codec": "opus", "bitrate": 64000, @@ -74,7 +104,7 @@ RDS = { }, "images": { "logo": { - "lazy": True, + "lazy": False, 'contents': b'', "part": { "current": 0, @@ -109,10 +139,10 @@ RDS2 = { "Compressed": False, "DyPTY": False, "EPG": None, - "AS": [ # AS = Alternative Server + "EON": [ # can add more server here ], - "EON": [ + "AS": [ # AS = Alternative Server # can add more server here ], "ContentInfo": { @@ -126,8 +156,8 @@ RDS2 = { } } - def update_RDS(): + RDSLog.info("Starting RDS Users...") global RDS while True: pstext = "DPRadio Testing Broadcasting " @@ -136,6 +166,7 @@ def update_RDS(): time.sleep(1) def update_RDS_time(): + RDSLog.info("Starting RDS Times...") global RDS while True: RDS["CT"]["Local"] = datetime.now().timestamp() @@ -145,6 +176,7 @@ def update_RDS_time(): time.sleep(1) def update_RDS_images(): + RDSLog.info("Starting RDS Images...") global RDS while True: sendimagelazy(encodelogoimage(r"C:\Users\sansw\3D Objects\dpstream iptv logo.png", 25), 100, RDS, "logo") @@ -154,3 +186,13 @@ def update_RDS_images(): sendimagelazy(encodelogoimage(r"IDRBfavicon.jpg", 25), 100, RDS, "logo") time.sleep(10) +def startRDSThread(): + RDSLog.info("Starting RDS...") + thread = threading.Thread(target=update_RDS) + thread2 = threading.Thread(target=update_RDS_time) + thread3 = threading.Thread(target=update_RDS_images) + + thread.start() + thread2.start() + thread3.start() + diff --git a/Server/Settings.py b/Server/Settings.py new file mode 100644 index 0000000..c2bd6de --- /dev/null +++ b/Server/Settings.py @@ -0,0 +1,43 @@ +""" +This file is part of IDRB Project. + +IDRB Project is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +IDRB Project is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with IDRB Project. If not, see . +""" + +# Server Settings +protocol = "ZMQ_WS" # TCP ZMQ ZMQ_WS +server_port = ('*', 6980) # if use other protocol ZMQ please use 0.0.0.0 +compression_level = 9 # 0-9 + +# Server Info +ServerName = "DPCloudev" +ServerDesc = "Testing Server" +Country = "TH" + +""" +If you want your server to be listed publicly on ThaiSDR Directory, following this steps +1. goto dashboard.damp11113.xyz and login or signup your account +2. goto click "APIKey" +3. click create +4. select api type "ThaiSDR Directory" +5. click done +6. copy api key + +""" +public = True +#ServerIP = "IDRB.damp11113.xyz" # do not add protocol before ip +ServerIP = "localhost" +#ServerPort = server_port[1] +ServerPort = 6980 +ThaiSDRkey = "1N5LURICLIN1U9QNYZ4MHJ6FNXISFXFELZAX135CFM0HSD17O2.63E60BE9EEA2339C113A15EB" \ No newline at end of file diff --git a/Server/ThaiSDRDir.py b/Server/ThaiSDRDir.py new file mode 100644 index 0000000..17d85b3 --- /dev/null +++ b/Server/ThaiSDRDir.py @@ -0,0 +1,97 @@ +""" +This file is part of IDRB Project. + +IDRB Project is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +IDRB Project is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with IDRB Project. If not, see . +""" + +import requests +import threading +import time +import Settings +import json +import logging + +ThaiSDRDirLog = logging.getLogger("ThaiSDRDir") + +content = {} + +protocolclientconvert = { + "TCP": "TCP", + "ZMQ": "ZeroMQ", + "ZMQ_WS": "ZeroMQ (WS)" +} + +def mainprocess(): + while True: + currentcontent = content + try: + Station = {} + + try: + for channel_number, channel_info in currentcontent["channel"].items(): + Station.update( + { + channel_number: { + "StationName": channel_info["Station"], + "StationDesc": channel_info["StationDesc"], + "StationEncrypted": channel_info["Encrypt"], + "Audio": { + "AudioChannel": channel_info["RDS"]["ContentInfo"]["channel"], + "Codec": channel_info["RDS"]["ContentInfo"]["Codec"].upper(), + "Bitrate": channel_info["RDS"]["ContentInfo"]["bitrate"], + } + } + } + ) + except: + continue + + + + Sendcontent = { + "ServerName": currentcontent["serverinfo"]["RDS"]["ServerName"], + "ServerDesc": currentcontent["serverinfo"]["RDS"]["ServerDesc"], + "ConnectionUser": currentcontent["serverinfo"]["Listener"], + "ServerURL": Settings.ServerIP, + "ServerPort": Settings.ServerPort, + "ServerProtocol": protocolclientconvert.get(Settings.protocol.upper(), "Unknown"), + "Station": Station + } + + jsondata = json.dumps(Sendcontent) + + response = requests.post("https://thaisdr.damp11113.xyz/api/idrbdir/updateserver", data=jsondata, headers={"apikey": Settings.ThaiSDRkey, "Content-Type": "application/json"}) + + if response.status_code == 200: + ThaiSDRDirLog.info("Update succeeded!") + else: + response_json = response.json() + ThaiSDRDirLog.error(f"Update failed, your server cannot be listed on ThaiSDR Directory! Reason: {response_json['message']}") + + time.sleep(300) + except Exception as e: + ThaiSDRDirLog.error(f"ThaiSDR Directory Error: {str(e)}") + time.sleep(5) + continue + +def run(): + if Settings.protocol != None and Settings.ThaiSDRkey != "": + ThaiSDRDirLog.info("server is soon getting listed on ThaiSDR Directory") + TDIR = threading.Thread(target=mainprocess) + TDIR.start() + else: + if Settings.ThaiSDRkey == "": + ThaiSDRDirLog.error("ThaiSDR Directory can't work without APIKey.") + else: + ThaiSDRDirLog.error("ThaiSDR Directory can't work without protocol.") \ No newline at end of file diff --git a/Server/__pycache__/Encoder.cpython-310.pyc b/Server/__pycache__/Encoder.cpython-310.pyc new file mode 100644 index 0000000..3eaf79e Binary files /dev/null and b/Server/__pycache__/Encoder.cpython-310.pyc differ diff --git a/Server/__pycache__/RDS.cpython-310.pyc b/Server/__pycache__/RDS.cpython-310.pyc index a7f4785..46653df 100644 Binary files a/Server/__pycache__/RDS.cpython-310.pyc and b/Server/__pycache__/RDS.cpython-310.pyc differ diff --git a/Server/__pycache__/Settings.cpython-310.pyc b/Server/__pycache__/Settings.cpython-310.pyc new file mode 100644 index 0000000..31dc15f Binary files /dev/null and b/Server/__pycache__/Settings.cpython-310.pyc differ diff --git a/Server/__pycache__/ThaiSDRDir.cpython-310.pyc b/Server/__pycache__/ThaiSDRDir.cpython-310.pyc new file mode 100644 index 0000000..75e7c44 Binary files /dev/null and b/Server/__pycache__/ThaiSDRDir.cpython-310.pyc differ diff --git a/Server/__pycache__/utils.cpython-310.pyc b/Server/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000..9eea4ce Binary files /dev/null and b/Server/__pycache__/utils.cpython-310.pyc differ diff --git a/Server/server.py b/Server/server.py index ad4d12f..48de425 100644 --- a/Server/server.py +++ b/Server/server.py @@ -1,54 +1,45 @@ +""" +This file is part of IDRB Project. + +IDRB Project is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +IDRB Project is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with IDRB Project. If not, see . +""" + import socket import time -import pyaudio -from pyogg import OpusBufferedEncoder -import numpy as np import pickle import threading -import RDS as _RDS -from queue import Queue -from Crypto.Cipher import AES -from Crypto.Protocol.KDF import scrypt -from Crypto.Random import get_random_bytes import zmq import logging +import zlib -logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') +logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s') +ServerLog = logging.getLogger("IDRBServer") -def pad_message(message_bytes): - block_size = AES.block_size - padding_length = block_size - (len(message_bytes) % block_size) - padding = bytes([padding_length] * padding_length) - return message_bytes + padding +ServerLog.info("Server is starting...") -def encrypt_data(message_bytes, password): - # Derive a key from the password - salt = get_random_bytes(50) - key = scrypt(password, salt, key_len=32, N=2 ** 14, r=8, p=1) +import ThaiSDRDir +import RDS as _RDS +import Encoder +import utils +import Settings - # Generate an IV (Initialization Vector) - iv = get_random_bytes(AES.block_size) - - # Pad the message - padded_message = pad_message(message_bytes) - - # Initialize AES cipher in CBC mode - cipher = AES.new(key, AES.MODE_CBC, iv) - - # Encrypt the padded message - encrypted_message = cipher.encrypt(padded_message) - - # Return the encrypted message, salt, and IV (for decryption) - return encrypted_message, salt, iv - - -protocol = "ZMQ_WS" -server_port = ('*', 6980) +protocol = Settings.protocol +server_port = Settings.server_port +public = Settings.public if protocol == "TCP": - # create tcp s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # wait for connection s.bind(server_port) s.listen(1) elif protocol == "ZMQ": @@ -64,91 +55,12 @@ else: print(f"{protocol} not supported") exit() -p = pyaudio.PyAudio() +ServerLog.info('starting RDS') +_RDS.startRDSThread() -sample_rate = 48000 -bytes_per_sample = p.get_sample_size(pyaudio.paInt16) +ServerLog.info('starting audio encoding') +Encoder.StartEncoder() -logging.info('init audio device') -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) - -logging.info('starting RDS') -thread = threading.Thread(target=_RDS.update_RDS) -thread.start() - -thread2 = threading.Thread(target=_RDS.update_RDS_time) -thread2.start() - -thread4 = threading.Thread(target=_RDS.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(_RDS.RDS["ContentInfo"]["channel"]) - encoder.set_bitrates(_RDS.RDS["ContentInfo"]["bitrate"]) - encoder.set_frame_size(60) - - 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(_RDS.RDS2["ContentInfo"]["channel"]) - encoder2.set_bitrates(_RDS.RDS2["ContentInfo"]["bitrate"]) - encoder2.set_frame_size(60) - - 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()) - - #channel2.put(pcm2.tobytes()) # if you use pcm - -logging.info('starting audio encoding') - -audio_thread = threading.Thread(target=encode_audio) -audio_thread.start() - -audio_thread2 = threading.Thread(target=encode_audio2) -audio_thread2.start() - -connectionlist = [] if protocol == "TCP": connected_users = 0 @@ -157,49 +69,31 @@ elif protocol == "ZMQ": else: print(f"{protocol} not supported") exit() + timestart = time.time() - +connectionlist = [] first = True -firstcontent = { - "first": True, - "mainchannel": 1, - "channel": { - 1: { - "Station": "DPRadio+", - "RDS": _RDS.RDS - }, - 2: { - "Station": "DPTest", - "RDS": _RDS.RDS2 - } - }, - "serverinfo": { - "Listener": connected_users, - "Country": "TH", - "Startat": timestart - } -} - def handle_client(): global connected_users, first try: while True: # Get the encoded audio from the buffer - ENchannel1 = channel1.get() + ENchannel1 = Encoder.channel1.get() # encrypt data - #ENC1encrypted, ENC1salt, ENC1iv = encrypt_data(ENchannel1, "password") + #ENC1encrypted, ENC1salt, ENC1iv = utils.encrypt_data(ENchannel1, "password") #ENchannel1 = ENC1encrypted + b'|||||' + ENC1salt + b'|||||' + ENC1iv - ENchannel2 = channel2.get() + ENchannel2 = Encoder.channel2.get() content = { "first": False, "mainchannel": 1, "channel": { 1: { "Station": "DPRadio+", + "StationDesc": "The best station in the world!", "Encrypt": b'|||||' in ENchannel1, # check if encrypt "ContentSize": len(ENchannel1), "Content": ENchannel1, @@ -207,6 +101,7 @@ def handle_client(): }, 2: { "Station": "DPTest", + "StationDesc": "", "Encrypt": b'|||||' in ENchannel2, "ContentSize": len(ENchannel2), "Content": ENchannel2, @@ -215,15 +110,19 @@ def handle_client(): }, "serverinfo": { "Listener": connected_users, - "Country": "TH", - "Startat": timestart + "Startat": timestart, + "RDS": _RDS.ServerRDS } } + ThaiSDRDir.content = content + + compressedcontent = zlib.compress(pickle.dumps(content), level=Settings.compression_level) + #connection.sendall(pickle.dumps(content)) if protocol == "TCP": for i in connectionlist: try: - i.sendall(pickle.dumps(content)) + i.sendall(compressedcontent) except Exception as e: #print(f'Error sending data to {i.getpeername()}: {e}') # Remove disconnected client from the list @@ -234,20 +133,23 @@ def handle_client(): # check if no user if not connectionlist: first = True + ServerLog.info('server is standby now') break elif protocol == "ZMQ": - s.send(pickle.dumps(content)) + s.send(compressedcontent) except Exception as e: print(f'Error: {e}') # Your main server logic using threading for handling connections if __name__ == "__main__": - logging.info('server is running') + if public: + ServerLog.info('starting ThaiSDR Directory') + ThaiSDRDir.run() + ServerLog.info('server is running') if protocol == "TCP": while True: - print("Waiting for a connection...") connection, client_address = s.accept() - print(f"Connected to {client_address}") + ServerLog.info(f'{client_address} is connected') connectionlist.append(connection) connected_users += 1 diff --git a/Server/utils.py b/Server/utils.py new file mode 100644 index 0000000..7188f5b --- /dev/null +++ b/Server/utils.py @@ -0,0 +1,46 @@ +""" +This file is part of IDRB Project. + +IDRB Project is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +IDRB Project is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with IDRB Project. If not, see . +""" + +from Crypto.Cipher import AES +from Crypto.Protocol.KDF import scrypt +from Crypto.Random import get_random_bytes + +def pad_message(message_bytes): + block_size = AES.block_size + padding_length = block_size - (len(message_bytes) % block_size) + padding = bytes([padding_length] * padding_length) + return message_bytes + padding + +def encrypt_data(message_bytes, password): + # Derive a key from the password + salt = get_random_bytes(50) + key = scrypt(password, salt, key_len=32, N=2 ** 14, r=8, p=1) + + # Generate an IV (Initialization Vector) + iv = get_random_bytes(AES.block_size) + + # Pad the message + padded_message = pad_message(message_bytes) + + # Initialize AES cipher in CBC mode + cipher = AES.new(key, AES.MODE_CBC, iv) + + # Encrypt the padded message + encrypted_message = cipher.encrypt(padded_message) + + # Return the encrypted message, salt, and IV (for decryption) + return encrypted_message, salt, iv