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