From b0c15aff712714aa8e8936d84fddeaf56f75804f Mon Sep 17 00:00:00 2001 From: damp11113 Date: Mon, 26 Feb 2024 20:01:08 +0700 Subject: [PATCH] new update 1.6.1 new 1. compression with Zlib 2. Buffer 3. Public Server Selector 4. spilt code --- .../__pycache__/appcomponent.cpython-310.pyc | Bin 0 -> 7833 bytes Client/__pycache__/utils.cpython-310.pyc | Bin 0 -> 1990 bytes Client/appcomponent.py | 166 +++++++ Client/client.py | 429 ++++++++---------- Client/utils.py | 69 +++ Server/Encoder.py | 100 ++++ Server/RDS.py | 58 ++- Server/Settings.py | 43 ++ Server/ThaiSDRDir.py | 97 ++++ Server/__pycache__/Encoder.cpython-310.pyc | Bin 0 -> 3063 bytes Server/__pycache__/RDS.cpython-310.pyc | Bin 3246 -> 4438 bytes Server/__pycache__/Settings.cpython-310.pyc | Bin 0 -> 1149 bytes Server/__pycache__/ThaiSDRDir.cpython-310.pyc | Bin 0 -> 2857 bytes Server/__pycache__/utils.cpython-310.pyc | Bin 0 -> 1545 bytes Server/server.py | 206 +++------ Server/utils.py | 46 ++ 16 files changed, 825 insertions(+), 389 deletions(-) create mode 100644 Client/__pycache__/appcomponent.cpython-310.pyc create mode 100644 Client/__pycache__/utils.cpython-310.pyc create mode 100644 Client/appcomponent.py create mode 100644 Client/utils.py create mode 100644 Server/Encoder.py create mode 100644 Server/Settings.py create mode 100644 Server/ThaiSDRDir.py create mode 100644 Server/__pycache__/Encoder.cpython-310.pyc create mode 100644 Server/__pycache__/Settings.cpython-310.pyc create mode 100644 Server/__pycache__/ThaiSDRDir.cpython-310.pyc create mode 100644 Server/__pycache__/utils.cpython-310.pyc create mode 100644 Server/utils.py diff --git a/Client/__pycache__/appcomponent.cpython-310.pyc b/Client/__pycache__/appcomponent.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6099fb9e35fc9682e85c8b23bc76baf30691e9cb GIT binary patch literal 7833 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 DXbarB literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3eaf79e5b0db3cb4b00e56b97e8105f691fad263 GIT binary patch literal 3063 zcmcImUvC@75#Reqo=&7hQGYBsPO?dx)+|)2RuiLDouIP*Bm@*$p{S%js6CvxOY-F7 z-Dz(rnT7=vaGwhJJ4irZ@(l{~n`|HZq>s>`C_1yG6**1XyyOlyyE8jGJ2U&+xlPMu zkHGVyQFn7bq|j7{?U zS$`h51wIdRZt+{pT_Swui77sX{=^dTs|HDolQrh)xr^Ux?vwG{MLqX-ymGw6-Q(MA zl9gDQSJ~7{$A9A+(nT%3!>t#@e^b-9*fgvBiTH1`8CHdsvRO6z;R$MYWFLoaSjqo`r(1(P28E?J%K&vnZD$ zIvPl@CTWyt+Z zExNw@n0~R|Y_9LN9{&b3WiN$Me#%F7qJ9=f5FDgZgozx2&)%n7&5ch$fBj)&r_p*0 zZqw~XYjD(sjDG-fT5C4tCa?bnl?K_h^5sPU$|R1>*PqM>;!tI|QeU%P@*_ z2=y^+UJfqBjP}A)4x8BK(J43-(sr7mJO5{oyf99at`6*V+)&!+&?J@1GzZTf^rX!4 zwUw2#v$J|P8PrqJU4gB}ZO&Ids(V*>taodMay}dI0f)})qim2r9CSJykhPVxQ$UX@ zjyuoh8VO=-z@n7q!(8%yy1E-Y$TmjP39rM7!I)Hxhjs6q>4w8MA6j)2-&wj zf7rajV&1wUHX++z8fu;u{XJz!m*A3COWdvUm@e4@T zS08Sy9UkOR0}t~s$Q38FTvepywH;dE2O(Q%1!y|0z~M;kyC<7a09v01x1Hk0%h|%w-eI18}>? zB{n$*cUex!>F)%046ZyB0yp4n+c?SqU^J~zoiXWl;$4K5o@)IOK1#*pyRZ&CP z&w$h%-yQ9aSoLjy#j5x@j@6N@AVC0($UW$(^3AsnekJb1;AL+6Ww=oAFE2?l0Z!}MlB<7*|2t>RrV#^F5tk@aR^Z1 z3n1!pSt}}UYlOg^w5#k0Ys!S|;emn)s?gG1jWg||a&k0QGyRf=X)x0D%Qv~zZ)T&U zJs2hH1Aus!C2W=x0N{ zud0|0^~wT|$_NC-7&OuO2?!9P>T)4|g`;?+RB>;(jz1=4WN!_jX@SzyVZ{cm%t%7zO}0-lErELh@Q z^zD5lcaeO61kbh3x6aIsbZVGsgt-r(U=Cvb5nR7Xz6r!8?u^c;;p*2jJ<~O9(>5wl bHz3o7ZJ7VI?brDQd%-AJ1=F?M*;4gCMATX; literal 0 HcmV?d00001 diff --git a/Server/__pycache__/RDS.cpython-310.pyc b/Server/__pycache__/RDS.cpython-310.pyc index a7f4785f51f2d0b25e4ac2cd077bb2bae815ddf9..46653dfc9afeaedf8c8c199edb4ad076d18c43bf 100644 GIT binary patch literal 4438 zcmcgv%WoUU8Q+~q$uDjyW zvqPyAC?J4R#3;~1(NhZnl@0}(Yi~u-YtcWU*!0#ztG+Jjp>>?3znPURI&sreyTp9+ z&Fh=*{SBkRLD#_d+*=>cqt z^qTEu(A!~lIp^he*BsB$Yk4oPd)IThyLEtNwoPwy7G1Z0sJ1N$ z&S@Mi$yHzQ3$z<`Xx(p7!C4SX5iE8js7dO#*mxx9a>RnAu8WCIi*Z3^lT*pXa;$Dr zgR9eX^eS(0;fHjlvls?-dOfJ~R?Mjnp43d-w>Ou5o1ix?Om`-?G%lK`?4<;!-7U_b*KO?yEOE{(B8Sn2)r*Bqd`kE-Hh50qX}vd zY&8f&y2xoK=1ZOMlnV{eTb0_in{zcT6Saf$(3YspMi34+T z87y<)2jjYk!m!4_D=&AelDFf;nw_lfoWci?z6-STGAV>+_*JcsZA}sGc$E{;ahVhWk1h>Fs*vT zctTojFQY8oO>#0?>`ERNa*5scWi!d$?f79JyIv+(_8Z(ANY5-FDE;L`UDpYvH<-?{ zg>=)yp{DhrA%?){{qyAu^K&u6U_SO+@#_3!RPTTV8PB)7@W0k>XwR#_p09F&h?$34 zi|uYQoYD`YMx<|e8uVgS5e}I{1kJQfF@oVAfAVAX{qpU)Qv2SQ2cpInTKs!}9%1H~ z(X+-3M#hZz&^@zn#+pERsTv;`ADByo*(~$EBMp|7<~?)6yk~4!lg5S#!%2c{EPAGV4UJM`DOiBpeSQt7#l%K7k9eyaTEFk1W+K*Q)6@4D*{vxnb3IQY) zQ^wk}F>f)=onoyx5k_@Cj4uM&+btB6%K3y$GBT2WnB?jmAt1oVSTPEjB!isP;E5IT zR+8r}IFUebCbkOCBol|6w+neOhRxzQfak!AQpe2ePIlYa$9M>b&uSTBZ$MwQkd6c% zAV!kWgH)ztikKE`@DT}KzEl_8M1XmLqJx;9e@ehbQ; zHB-C2W&^z9<#zXqlejStYdba4K(qEow3 zzEZF}Qky|5dCm;dpKt zCP!9?IrB1;UGRnTGgm!3YIowmxNjIc$`yk5ex;R?DT5evuoy^f!(6@Tw^}^(4#Ylc zkT$IHV+6i25qZRZU zr8fJf;^)!{WNfTG{1k6Cn$Fl4fvAj)OpUP28_ZfqXfs<{J*0pQf&d>{H>KS(dKk$J z%OH%AuUS@y@$lVjAve8tq>8}MCWISOodAo)Vr}ikT{F`?3ttwI&+2|^Zo2pBco3(+ z%8rVRf|F!H=X%sg>_CYI2@F9MQ0Yx5=EZXgJP%MHUbY=e6e{h~`(d2=vfF8^vbF$% zKgI4?rD@kB#5Pq<9WcoQF@dQ^G=#BEq*_Bw4Ei-B$(FTkY}wlew4u_gt&-tZ_K`WI zrTu-m$Lvez)IMh`zpKs1=(2`*kCiUPIQS6oVndu!;5s#H#ElosA;o)erb5qJ+sWY3<=l z;LBQ*7PFYWL0CrGEW2Tw(BD>;<&+;uyb+l5m8<#xvlDL1e`i0_-iF#8r}m4>;cmu4xsoZ<_o4!u8HoT>c*1X!UBux3kzvDsMrxX055}5 z+wKbWoWkE{gwPp$=hD1$6_bfm!XjBUxJU(7;i4;|cv)cu1$4Q} zr{xhGr*^-jR#f)cSA*uZPtg6ybSChBt`qW$Dn-UwK-+K*LQ4En=$J0S&#{l$sMEyE zJ|KDYpWB3dW?SS_+a?d}4Ee;)lKXa!{EIjQyX?=%KkWhX4>G7m7=KDWwhxiN+lR^D z>{0Sp`w01qeU$v!K1O!z@a}r;I3Ep3>ogxNO-({+(iBqS6p|DuqDm2o5L#4H{vILH` z7~I#=N)(Q_^qaI`#}udVlpTuGk~aSX$Nz)k37FYKk?xT4h8;Kw_g^Po5*~nAlrEft z2T`Vgu8`ckd|MxvpXt2Ez^ZNP$1c~_b9ST^uTDquF=LiS@>SzpZJ^_CgdZ6-UJ68{i&6zAVcGXCIzN|nO8@7b}C zWozu20rW&h@r8QuB9EyKs`WA7ROkl~uPhffHLl#(;8KP9Iag@TT$Z<-Qx{D>rhe&=0#*A+!#wdLYC4XPW1d+Hh5xeP zDp#E(?I~)#2AKRc-18c`Ll=QDovQ2RQuYg^kzF-VJ(Bl$}sz+wB{C9e0Fj1ct@#bceE^`J(*X9lEp=qM*P)+hU*#2F8|- z&qQnMV5+8$)Hb*^c|H!gnFaX@znL517BTdlSD zW`|P6q+(PtIT%%Eu-p7mH0CJRJEB*Kbmw@6IyVubrh%`ow!zhR!?OX?I)&{-e2+S$ z)Pv|STTt4(f<*=D_`jtYZIP9)RH0t>Pvc(`32z}-#HAK?mpYnbv3{Mh-*m=))iwD; P=4tsw<`rYYvRwOj_<)p! diff --git a/Server/__pycache__/Settings.cpython-310.pyc b/Server/__pycache__/Settings.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31dc15f18e7e7194b6171ef1050e165dc6415655 GIT binary patch literal 1149 zcmah}&2Hm15SA4uv6XJo_EeykIW|RLuj5UMq>G@6f7WUp#Vg53EEq5tk&e{TqDoRW zw2!cFvcNu(u(zJ}-cwHO(L`yP+~pd* zX&@^BMfD;7t zbnagW|ILd+l5RoAB10N6dP8EHgmbuKkSn&#`H_VgfEy<6^e zvA|unW)FUnRmKw}hpCh#)dgar9=u+ndd%b)V)fA@AE*>Z;7DebKVYz3}Sw;>JHT z%i4Dz``;MG$0l}m*z8YwT;!Ck-+Uf0r4tP1Gl@kG&0rLiO)etBV}YHwgZ6KJ?zku8 zsps^@j@y}@y>@5ei>uL3r$hJF@rSp=!8o*UJKdAs@ap-&i>( z0iR1&?CHDDR21&_uvxV%%Drty8bIvZRUx)e{Mct@> TRj=W*Q%TdTTXnN;HfnzX`JijQ literal 0 HcmV?d00001 diff --git a/Server/__pycache__/ThaiSDRDir.cpython-310.pyc b/Server/__pycache__/ThaiSDRDir.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75e7c44ed1eb7aa52e73f6f066ce30e65b2cfcbe GIT binary patch literal 2857 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 MCk@gdzTNQt2A&SMKL7v# literal 0 HcmV?d00001 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