new update 1.6.1

new
1. compression with Zlib
2. Buffer
3. Public Server Selector
4. spilt code
This commit is contained in:
dharm pimsen 2024-02-26 20:01:08 +07:00
parent 5f2c2c54c6
commit b0c15aff71
16 changed files with 825 additions and 389 deletions

Binary file not shown.

Binary file not shown.

166
Client/appcomponent.py Normal file
View File

@ -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))

View File

@ -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 <https://www.gnu.org/licenses/>.
"""
import queue
import time import time
from datetime import datetime from datetime import datetime
import cv2 import cv2
import dearpygui.dearpygui as dpg import dearpygui.dearpygui as dpg
import threading import threading
import socket import socket
import numpy as np import requests
import pickle import pickle
import pyaudio import pyaudio
import zmq import zmq
from pyogg import OpusDecoder from pyogg import OpusDecoder
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import scrypt
import configparser import configparser
import ctypes 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: class App:
def __init__(self): def __init__(self):
@ -83,6 +40,9 @@ class App:
self.config = configparser.ConfigParser() self.config = configparser.ConfigParser()
self.config.read("config.ini") self.config.read("config.ini")
self.device_name_output = self.config["audio"]["device"] self.device_name_output = self.config["audio"]["device"]
self.buffersize = 64 # can configable
self.working = False self.working = False
self.readchannel = 1 self.readchannel = 1
self.firstrun = True self.firstrun = True
@ -96,14 +56,35 @@ class App:
self.cprotocol = None self.cprotocol = None
self.cciswaitlogoim = True self.cciswaitlogoim = True
self.ccthreadlogorecisworking = False 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): def connecttoserver(self, sender, data):
dpg.configure_item("connectservergroup", show=False) 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)) dpg.configure_item("serverstatus", default_value='connecting...', color=(255, 255, 0))
ip = dpg.get_value("serverip") if self.ccconwithpubselect:
port = dpg.get_value("serverport") 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": if protocol == "TCP":
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try: try:
@ -145,6 +126,7 @@ class App:
dpg.configure_item("disconnectbutton", show=False) dpg.configure_item("disconnectbutton", show=False)
dpg.configure_item("connectservergroup", show=True) dpg.configure_item("connectservergroup", show=True)
dpg.configure_item("serverstatus", default_value='disconnected', color=(255, 0, 0)) dpg.configure_item("serverstatus", default_value='disconnected', color=(255, 0, 0))
dpg.configure_item("logostatus", show=False)
self.firstrun = True self.firstrun = True
self.firststart = True self.firststart = True
self.ccdecryptpassword = None self.ccdecryptpassword = None
@ -153,12 +135,16 @@ class App:
self.ccisdecryptpassword = None self.ccisdecryptpassword = None
self.cciswaitlogoim = True self.cciswaitlogoim = True
self.ccthreadlogorecisworking = False self.ccthreadlogorecisworking = False
self.buffer = queue.Queue(maxsize=self.buffersize)
self.okbuffer = False
self.firstrunbuffer = True
def RDSshow(self): def RDSshow(self):
try: try:
dpg.configure_item("RDSinfo", 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) show=True)
dpg.configure_item("RDSPS", default_value="PS: " + self.RDS["PS"]) 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("RDSRT", default_value="RT: " + limit_string_in_line(self.RDS["RT"], 120))
dpg.configure_item("RDSCTlocal", default_value="Time Local: " + datetime.fromtimestamp(self.RDS["CT"]["Local"]).strftime('%H:%M:%S')) dpg.configure_item("RDSCTlocal", default_value="Time Local: " + datetime.fromtimestamp(self.RDS["CT"]["Local"]).strftime('%H:%M:%S'))
@ -175,10 +161,11 @@ class App:
logoreciveprocessingthread.start() logoreciveprocessingthread.start()
else: else:
if not self.RDS["images"]["logo"]["lazy"]: 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: except Exception as e:
dpg.configure_item("station_logo_config", show=False) dpg.configure_item("station_logo_config", show=False)
print(e) #print(e)
except Exception as e: except Exception as e:
pass pass
@ -243,6 +230,90 @@ class App:
self.config["audio"]["device"] = dpg.get_value(sender) self.config["audio"]["device"] = dpg.get_value(sender)
self.config.write(open('config.ini', 'w')) 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): def stream(self, socket):
opus_decoder = None opus_decoder = None
streamoutput = None streamoutput = None
@ -252,34 +323,41 @@ class App:
imcctfrpy = [0] * 2500 imcctfrpy = [0] * 2500
bytesconunt = 0 bytesconunt = 0
bytesconunt_frame = 0 bytesconunt_frame = 0
codecbytesconunt = 0
start_time = time.time() start_time = time.time()
evaluation_audio_X = None evaluation_audio_X = None
decodecodec = None decodecodec = None
SBT = threading.Thread(target=self.streambuffer, args=(socket,))
SBT.start()
while True: while True:
try: try:
if self.working: if self.working:
if self.cprotocol == "TCP": if self.buffer.not_empty:
data = b'' data = self.buffer.get()
#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()
else: else:
data = b"" continue
bytesconunt += len(data) bytesconunt += len(data)
if bytesconunt_frame >= 10: 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)) 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() start_time = time.time()
bytesconunt_frame = 0 bytesconunt_frame = 0
bytesconunt = 0 bytesconunt = 0
codecbytesconunt = 0
if len(altfrpy) > 250: if len(altfrpy) > 250:
altfrpy.pop(0) altfrpy.pop(0)
@ -310,7 +388,9 @@ class App:
break break
try: try:
datadecoded = pickle.loads(data) decompressed_data = zlib.decompress(data)
datadecoded = pickle.loads(decompressed_data)
except: except:
pass pass
@ -326,25 +406,33 @@ class App:
rdshow.start() rdshow.start()
dpg.configure_item("ServerListener", default_value="Listener: " + str(datadecoded["serverinfo"]["Listener"]) + " Users") 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: except:
pass pass
if self.firstrun: if self.firstrun:
decodecodec = datadecoded["channel"][self.readchannel]["RDS"]["ContentInfo"]["Codec"] decodecodec = datadecoded["channel"][self.readchannel]["RDS"]["ContentInfo"]["Codec"]
if decodecodec.upper() == "OPUS": if decodecodec.upper() == "OPUS":
opus_decoder = OpusDecoder() opus_decoder = OpusDecoder()
opus_decoder.set_channels(self.RDS["ContentInfo"]["channel"]) opus_decoder.set_channels(self.RDS["ContentInfo"]["channel"])
opus_decoder.set_sampling_frequency(self.RDS["ContentInfo"]["samplerates"]) 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) 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] evaluation_audio_X = np.fft.fftfreq(1024, 1.0 / self.RDS["ContentInfo"]["samplerates"])[:1024 // 2]
if len(datadecoded["channel"]) > 1: if len(datadecoded["channel"]) > 1:
channel_info = [] channel_info = []
for i in range(1, len(datadecoded["channel"]) + 1): 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"]})') 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("mediachannelselect", show=True, items=channel_info)
dpg.configure_item("morerdsbutton", show=True) dpg.configure_item("morerdsbutton", show=True)
dpg.configure_item("serverinfobutton", show=True) dpg.configure_item("serverinfobutton", show=True)
dpg.configure_item("logostatus", show=True) dpg.configure_item("logostatus", show=True)
try: try:
if self.RDS["images"]["logo"]["lazy"] and not self.ccthreadlogorecisworking: if self.RDS["images"]["logo"]["lazy"] and not self.ccthreadlogorecisworking:
if not self.RDS["images"]["logo"]["contents"] == b'' or \ if not self.RDS["images"]["logo"]["contents"] == b'' or \
@ -357,8 +445,10 @@ class App:
else: else:
if not self.RDS["images"]["logo"]["lazy"]: if not self.RDS["images"]["logo"]["lazy"]:
dpg.set_value("station_logo", CV22DPG( 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))) cv2.IMREAD_COLOR)))
dpg.configure_item("station_logo_config", show=True)
dpg.configure_item("logostatus", show=False)
except: except:
dpg.configure_item("station_logo_config", show=False) dpg.configure_item("station_logo_config", show=False)
dpg.configure_item("disconnectbutton", show=True) dpg.configure_item("disconnectbutton", show=True)
@ -403,6 +493,8 @@ class App:
dpg.configure_item("serverstatus", default_value="Decrypt Error", color=(255, 0, 0)) dpg.configure_item("serverstatus", default_value="Decrypt Error", color=(255, 0, 0))
if self.ccisdecrypt or not self.ccisencrypt: if self.ccisdecrypt or not self.ccisencrypt:
codecbytesconunt += len(data)
if decodecodec.upper() == "OPUS": if decodecodec.upper() == "OPUS":
decoded_pcm = opus_decoder.decode(memoryview(bytearray(data))) decoded_pcm = opus_decoder.decode(memoryview(bytearray(data)))
else: # pcm else: # pcm
@ -435,7 +527,9 @@ class App:
dpg.set_value('transferateaudiodataoncchannelplot', [tfrpx, adcctfrpy]) dpg.set_value('transferateaudiodataoncchannelplot', [tfrpx, adcctfrpy])
bytesconunt_frame += 1 bytesconunt_frame += 1
else: else:
SBT.join()
streamoutput.close() streamoutput.close()
if self.cprotocol == "TCP": if self.cprotocol == "TCP":
socket.close() socket.close()
@ -484,143 +578,9 @@ class App:
else: else:
socket.close() socket.close()
self.disconnectserver() self.disconnectserver()
raise
break 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): def init(self):
if self.config["debug"]["hideconsole"] == "true": if self.config["debug"]["hideconsole"] == "true":
ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0) ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0)
@ -628,7 +588,7 @@ class App:
ctypes.CDLL("opus.dll") ctypes.CDLL("opus.dll")
dpg.create_context() 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() dpg.setup_dearpygui()
# -------------- add code here -------------- # -------------- add code here --------------
noimage_texture_data = [] noimage_texture_data = []
@ -643,9 +603,14 @@ class App:
width, height, channels, data = dpg.load_image("IDRBlogo.png") 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")
dpg.add_static_texture(width=512, height=256, default_value=data, tag="app_logo_background")
self.window() with dpg.window(no_background=True, no_title_bar=True, no_move=True, no_resize=True, tag="backgroundviewportlogo"):
self.menubar() 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() num_devices = self.paudio.get_device_count()
output_devices = [] output_devices = []
@ -672,16 +637,22 @@ class App:
def render(self): def render(self):
# insert here any code you would like to run in the render loop # insert here any code you would like to run in the render loop
# you can manually stop by using stop_dearpygui() or self.exit() # you can manually stop by using stop_dearpygui() or self.exit()
dpg.fit_axis_data("x_axis") try:
dpg.fit_axis_data("y_axis1") dpg.fit_axis_data("x_axis")
dpg.fit_axis_data("y_axis2") dpg.fit_axis_data("y_axis1")
dpg.fit_axis_data("y_axis3") 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): def exit(self):
dpg.destroy_context() dpg.destroy_context()
app = App() app = App()
app.init() app.init()

69
Client/utils.py Normal file
View File

@ -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

100
Server/Encoder.py Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
"""
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()

View File

@ -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 <https://www.gnu.org/licenses/>.
"""
import time import time
from datetime import datetime from datetime import datetime
import cv2 import cv2
import numpy as np import numpy as np
from damp11113 import scrollTextBySteps from damp11113 import scrollTextBySteps
import threading
import Settings
import logging
RDSLog = logging.getLogger("RDS")
def encodelogoimage(path, quality=50): def encodelogoimage(path, quality=50):
image = cv2.resize(cv2.imread(path), (128, 128)) 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]["contents"] = chunk
RDSimage["images"][imagetype]["part"]["current"] = i 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) time.sleep(delay)
RDSimage["images"][imagetype]["contents"] = b'' RDSimage["images"][imagetype]["contents"] = b''
RDSimage["images"][imagetype]["part"]["current"] = 0 RDSimage["images"][imagetype]["part"]["current"] = 0
RDSimage["images"][imagetype]["part"]["total"] = 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 = { RDS = {
"PS": "DPRadio", "PS": "DPRadio",
"RT": "Testing internet radio", "RT": "Testing internet radio",
@ -60,12 +90,12 @@ RDS = {
"Compressed": False, "Compressed": False,
"DyPTY": False, "DyPTY": False,
"EPG": None, "EPG": None,
"AS": [ # AS = Alternative Server
# can add more server here
],
"EON": [ "EON": [
# can add more here # can add more here
], ],
"AS": [ # AS = Alternative Server
# can add more server here
],
"ContentInfo": { "ContentInfo": {
"Codec": "opus", "Codec": "opus",
"bitrate": 64000, "bitrate": 64000,
@ -74,7 +104,7 @@ RDS = {
}, },
"images": { "images": {
"logo": { "logo": {
"lazy": True, "lazy": False,
'contents': b'', 'contents': b'',
"part": { "part": {
"current": 0, "current": 0,
@ -109,10 +139,10 @@ RDS2 = {
"Compressed": False, "Compressed": False,
"DyPTY": False, "DyPTY": False,
"EPG": None, "EPG": None,
"AS": [ # AS = Alternative Server "EON": [
# can add more server here # can add more server here
], ],
"EON": [ "AS": [ # AS = Alternative Server
# can add more server here # can add more server here
], ],
"ContentInfo": { "ContentInfo": {
@ -126,8 +156,8 @@ RDS2 = {
} }
} }
def update_RDS(): def update_RDS():
RDSLog.info("Starting RDS Users...")
global RDS global RDS
while True: while True:
pstext = "DPRadio Testing Broadcasting " pstext = "DPRadio Testing Broadcasting "
@ -136,6 +166,7 @@ def update_RDS():
time.sleep(1) time.sleep(1)
def update_RDS_time(): def update_RDS_time():
RDSLog.info("Starting RDS Times...")
global RDS global RDS
while True: while True:
RDS["CT"]["Local"] = datetime.now().timestamp() RDS["CT"]["Local"] = datetime.now().timestamp()
@ -145,6 +176,7 @@ def update_RDS_time():
time.sleep(1) time.sleep(1)
def update_RDS_images(): def update_RDS_images():
RDSLog.info("Starting RDS Images...")
global RDS global RDS
while True: while True:
sendimagelazy(encodelogoimage(r"C:\Users\sansw\3D Objects\dpstream iptv logo.png", 25), 100, RDS, "logo") 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") sendimagelazy(encodelogoimage(r"IDRBfavicon.jpg", 25), 100, RDS, "logo")
time.sleep(10) 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()

43
Server/Settings.py Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
"""
# 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"

97
Server/ThaiSDRDir.py Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
"""
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.")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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 <https://www.gnu.org/licenses/>.
"""
import socket import socket
import time import time
import pyaudio
from pyogg import OpusBufferedEncoder
import numpy as np
import pickle import pickle
import threading 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 zmq
import logging 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): ServerLog.info("Server is starting...")
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): import ThaiSDRDir
# Derive a key from the password import RDS as _RDS
salt = get_random_bytes(50) import Encoder
key = scrypt(password, salt, key_len=32, N=2 ** 14, r=8, p=1) import utils
import Settings
# Generate an IV (Initialization Vector) protocol = Settings.protocol
iv = get_random_bytes(AES.block_size) server_port = Settings.server_port
public = Settings.public
# 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)
if protocol == "TCP": if protocol == "TCP":
# create tcp
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# wait for connection
s.bind(server_port) s.bind(server_port)
s.listen(1) s.listen(1)
elif protocol == "ZMQ": elif protocol == "ZMQ":
@ -64,91 +55,12 @@ else:
print(f"{protocol} not supported") print(f"{protocol} not supported")
exit() exit()
p = pyaudio.PyAudio() ServerLog.info('starting RDS')
_RDS.startRDSThread()
sample_rate = 48000 ServerLog.info('starting audio encoding')
bytes_per_sample = p.get_sample_size(pyaudio.paInt16) 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": if protocol == "TCP":
connected_users = 0 connected_users = 0
@ -157,49 +69,31 @@ elif protocol == "ZMQ":
else: else:
print(f"{protocol} not supported") print(f"{protocol} not supported")
exit() exit()
timestart = time.time() timestart = time.time()
connectionlist = []
first = True 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(): def handle_client():
global connected_users, first global connected_users, first
try: try:
while True: while True:
# Get the encoded audio from the buffer # Get the encoded audio from the buffer
ENchannel1 = channel1.get() ENchannel1 = Encoder.channel1.get()
# encrypt data # encrypt data
#ENC1encrypted, ENC1salt, ENC1iv = encrypt_data(ENchannel1, "password") #ENC1encrypted, ENC1salt, ENC1iv = utils.encrypt_data(ENchannel1, "password")
#ENchannel1 = ENC1encrypted + b'|||||' + ENC1salt + b'|||||' + ENC1iv #ENchannel1 = ENC1encrypted + b'|||||' + ENC1salt + b'|||||' + ENC1iv
ENchannel2 = channel2.get() ENchannel2 = Encoder.channel2.get()
content = { content = {
"first": False, "first": False,
"mainchannel": 1, "mainchannel": 1,
"channel": { "channel": {
1: { 1: {
"Station": "DPRadio+", "Station": "DPRadio+",
"StationDesc": "The best station in the world!",
"Encrypt": b'|||||' in ENchannel1, # check if encrypt "Encrypt": b'|||||' in ENchannel1, # check if encrypt
"ContentSize": len(ENchannel1), "ContentSize": len(ENchannel1),
"Content": ENchannel1, "Content": ENchannel1,
@ -207,6 +101,7 @@ def handle_client():
}, },
2: { 2: {
"Station": "DPTest", "Station": "DPTest",
"StationDesc": "",
"Encrypt": b'|||||' in ENchannel2, "Encrypt": b'|||||' in ENchannel2,
"ContentSize": len(ENchannel2), "ContentSize": len(ENchannel2),
"Content": ENchannel2, "Content": ENchannel2,
@ -215,15 +110,19 @@ def handle_client():
}, },
"serverinfo": { "serverinfo": {
"Listener": connected_users, "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)) #connection.sendall(pickle.dumps(content))
if protocol == "TCP": if protocol == "TCP":
for i in connectionlist: for i in connectionlist:
try: try:
i.sendall(pickle.dumps(content)) i.sendall(compressedcontent)
except Exception as e: except Exception as e:
#print(f'Error sending data to {i.getpeername()}: {e}') #print(f'Error sending data to {i.getpeername()}: {e}')
# Remove disconnected client from the list # Remove disconnected client from the list
@ -234,20 +133,23 @@ def handle_client():
# check if no user # check if no user
if not connectionlist: if not connectionlist:
first = True first = True
ServerLog.info('server is standby now')
break break
elif protocol == "ZMQ": elif protocol == "ZMQ":
s.send(pickle.dumps(content)) s.send(compressedcontent)
except Exception as e: except Exception as e:
print(f'Error: {e}') print(f'Error: {e}')
# Your main server logic using threading for handling connections # Your main server logic using threading for handling connections
if __name__ == "__main__": 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": if protocol == "TCP":
while True: while True:
print("Waiting for a connection...")
connection, client_address = s.accept() connection, client_address = s.accept()
print(f"Connected to {client_address}") ServerLog.info(f'{client_address} is connected')
connectionlist.append(connection) connectionlist.append(connection)
connected_users += 1 connected_users += 1

46
Server/utils.py Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
"""
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