commit 28/10/2025

This commit is contained in:
2025-10-28 15:42:40 +07:00
parent 1f979fba9a
commit a4e655a932
23 changed files with 29088 additions and 318 deletions

View File

@@ -1,157 +0,0 @@
package audio
import audio.Bass.BASS_STREAMPROC_END
import audio.BassEnc.BASS_ENCODE_PCM
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import org.tinylog.Logger
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetSocketAddress
import java.util.function.BiConsumer
import java.util.function.Consumer
@Deprecated("Sepertinya gak jadi pake")
@Suppress("unused")
/**
* UDPReceiverToFile is a class that listens for UDP packets on a specified address and port
* and writes the received data to a specified file.
* It is designed to run in a separate thread and can be stopped when no longer needed.
* @param listeningAddress The address to listen for incoming UDP packets.
* @param listeningPort The port to listen for incoming UDP packets.
* @param samplingrate The sampling rate for the audio data, default is 44,100 Hz.
* @param channel The number of audio channels, default is 1 (mono).
* @param outputFilePath The path to the file where the received data will be written.
* @param senderIP The IP address of the sender from which to accept packets.
*/
class UDPReceiverToFile(listeningAddress: String, listeningPort: Int, val samplingrate: Int=44100, val channel: Int=1, val outputFilePath: String, val senderIP: String) {
private var socket: DatagramSocket? = null
private val bass : Bass = Bass.Instance
private val bassenc : BassEnc = BassEnc.Instance
private var isReceiving: Boolean = false
private val pipeIn= PipedInputStream(16*1024) // 16K
var isReady: Boolean = false; private set
var bytesReceived: Long = 0; private set
var bytesWritten: Long = 0; private set
init {
try{
val socketaddress = InetSocketAddress(listeningAddress, listeningPort)
socket = DatagramSocket(socketaddress)
isReady = true
} catch (e : Exception) {
Logger.error {"Failed to create UDP socket: ${e.message}" }
}
}
private val streamProc = Bass.STREAMPROC { handle, buffer, length, user ->
try{
val dd = ByteArray(length)
val bytesread = pipeIn.read(dd)
if (bytesread>0){
buffer?.write(0, dd, 0, bytesread) // Write the data to the buffer
bytesWritten += bytesread
// Return the number of bytes read
bytesread
} else {
// if bytesread is 0, it means the pipe is empty, return BASS_STREAMPROC_END
BASS_STREAMPROC_END
}
} catch (e : Exception){
// If an error occurs, log it and return BASS_STREAMPROC_END
Logger.error { "STREAMPROC exception on UDPReceiverToFile $senderIP $outputFilePath" }
BASS_STREAMPROC_END
}
}
/**
* Starts receiving data from the UDP socket and writing it to the specified file.
* This method runs in a separate thread.
* @param callback A BiConsumer that accepts a Boolean indicating success or failure and a String message.
* @param udpIsReceiving A Consumer that accepts a Boolean indicating whether UDP is currently receiving
*/
fun startReceiving(callback : BiConsumer<Boolean, String>, udpIsReceiving: Consumer<Boolean>) {
var isReceiving = false
if (isReady){
val scope = CoroutineScope(Dispatchers.Default)
scope.launch(CoroutineName("UDPReceiverToFile UDP $senderIP $outputFilePath")) {
Logger.info { "UDPReceiverToFile started, listening on ${socket?.localSocketAddress} , saving to $outputFilePath" }
PipedOutputStream(pipeIn).use { pipeOut ->
while (isReceiving) {
try{
val xx = DatagramPacket(ByteArray(1500),1500)
socket?.receive(xx)
if (xx.address.hostAddress!= senderIP) continue
if (xx.length < 1) continue
pipeOut.write(xx.data, 0, xx.length)
bytesReceived += xx.length
if (!isReceiving){
isReceiving = true
udpIsReceiving.accept(true)
}
} catch (e : Exception){
Logger.error { "Error receiving UDP packet: ${e.message}" }
continue
}
}
}
Logger.info { "UDPReceiverToFile ended" }
}
scope.launch(CoroutineName("UDPReceiverToFile BASS $senderIP $outputFilePath")) {
bass.BASS_SetDevice(0) // Set to No Sound device, we are not playing audio
val streamhandle = bass.BASS_StreamCreate(samplingrate, channel, 0, streamProc, null)
if (streamhandle!=0){
bass.BASS_ChannelPlay(streamhandle,false)
val encodehandle = bassenc.BASS_Encode_Start(streamhandle, outputFilePath, BASS_ENCODE_PCM, null, null)
if (encodehandle!=0){
Logger.info { "UDPReceiverToFile started writing to $outputFilePath" }
callback.accept(true, "UDPReceiverToFile started successfully, writing to $outputFilePath")
while (isReceiving) {
try {
delay(1000)
} catch (e: InterruptedException) {
Logger.error { "UDPReceiverToFile thread interrupted: ${e.message}" }
break
}
}
bassenc.BASS_Encode_Stop(encodehandle)
bass.BASS_StreamFree(streamhandle)
Logger.info { "UDPReceiverToFile stopped writing to $outputFilePath" }
callback.accept(false, "UDPReceiverToFile stopped successfully, written bytes: $bytesWritten")
} else {
callback.accept(false, "Failed to start encoding: ${bass.BASS_ErrorGetCode()}")
}
} else {
callback.accept(false, "Failed to create stream: ${bass.BASS_ErrorGetCode()}")
}
}
} else callback.accept(false, "UDPReceiverToFile is not ready. Check if the socket was created successfully.")
}
/**
* Stop UDPReceiverToFile from receiving data.
*/
fun stopReceiving() {
isReceiving = false
}
}

View File

@@ -1,101 +0,0 @@
package audio
import audio.Bass.BASS_STREAM_DECODE
import codes.Somecodes.Companion.ValidFile
import com.sun.jna.Memory
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetSocketAddress
import java.net.SocketAddress
import java.util.function.BiConsumer
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Deprecated("Sepertinya gak jadi pake ini")
@Suppress("unused")
class UDPSenderFromFile(val fileName: String, val bytesPerPackage: Int=1024, targetIP: Array<String>, targetPort: Int ) {
val bass: Bass = Bass.Instance
var filehandle: Int = 0
var listSocketAddress = ArrayList<SocketAddress>()
var initialized: Boolean = false; private set
var isRunning: Boolean = false; private set
var bytesSent: Int = 0; private set
init {
if (ValidFile(fileName)){
bass.BASS_SetDevice(0)
val handle = bass.BASS_StreamCreateFile(false, fileName, 0,0, BASS_STREAM_DECODE)
if (handle!=0){
// test buka file berhasil, tutup lagi
bass.BASS_StreamFree(handle)
//if (targetPort>0 && targetPort<65535){
if (targetPort in 0..65535){
if (targetIP.isNotEmpty()){
var validIPs = true
for(ip in targetIP){
try{
var so = InetSocketAddress(ip, targetPort)
listSocketAddress.add(so)
} catch (e : Exception){
validIPs = false
}
}
if (validIPs){
initialized = true
}
}
}
}
}
}
fun Start(callback: BiConsumer<Boolean, String>){
if (initialized){
val scope = CoroutineScope(Dispatchers.Default)
scope.launch(CoroutineName("UDPSenderFromFile $fileName")) {
try {
val socket = DatagramSocket()
bass.BASS_SetDevice(0) // Set to No Sound Device
val handle = bass.BASS_StreamCreateFile(false, fileName, 0, 0, BASS_STREAM_DECODE)
if (handle!=0){
isRunning = true
bytesSent = 0
callback.accept(true,"UDPSenderFromFile started, sending $fileName to ${listSocketAddress.size} targets")
while(isRunning){
val buffer = Memory(bytesPerPackage.toLong())
val bytesRead = bass.BASS_ChannelGetData(handle, buffer, bytesPerPackage)
if (bytesRead > 0) {
for(so in listSocketAddress){
val bytes = buffer.getByteArray(0, bytesRead)
socket.send(DatagramPacket(bytes, bytes.size, so))
bytesSent += bytes.size
}
} else isRunning = false
}
callback.accept(false,"UDPSenderFromFile finished sending $fileName")
bass.BASS_StreamFree(handle)
socket.close()
} else callback.accept(false, "Failed to open file $fileName for reading")
} catch (e : Exception){
callback.accept(false, "Error in UDPSenderFromFile: ${e.message}")
isRunning = false
}
}
} else callback.accept(false, "UDP Sender not initialized, check file and target IP/Port")
}
fun Stop(){
isRunning = false
}
}

View File

@@ -7,6 +7,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.tinylog.Logger
import java.io.PipedOutputStream
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetSocketAddress
@@ -23,8 +24,35 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
private val inet = InetSocketAddress(ipaddress, port)
private val maxUDPsize = 1000
private var _tcp: Socket? = null
private val PipeOuts = mutableMapOf<String,PipedOutputStream>()
fun AddPipeOut(key: String, pipeOut: PipedOutputStream) {
RemovePipeOut(key)
PipeOuts[key] = pipeOut
}
fun RemovePipeOut(key: String) {
if (PipeOuts.contains(key)){
val pipe = PipeOuts[key]
try {
pipe?.close()
} catch (e: Exception) {
// ignore
}
PipeOuts.remove(key)
}
}
fun ClearPipeOuts() {
PipeOuts.values.forEach { piped ->
try {
piped.close()
} catch (e: Exception) {
// ignore
}
}
PipeOuts.clear()
}
/**
* Buffer remain in bytes
*/
@@ -113,10 +141,20 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
//println("Buffer remain: $bufferRemain, sending chunk size: ${chunk.size}")
while(bufferRemain<chunk.size){
delay(10)
//println("Waiting until buffer enough..")
}
udp.send(DatagramPacket(chunk, chunk.size, inet))
delay(2)
PipeOuts.keys.forEach { kk ->
val pp = PipeOuts[kk]
try {
pp?.write(chunk)
} catch (e: Exception) {
Logger.error { "Failed to write to pipeOut $kk, message: ${e.message}" }
pp?.close()
PipeOuts.remove(kk)
}
}
} catch (e: Exception) {
cbFail.accept("SendData to $ipaddress failed, message: ${e.message}")
return@launch

View File

@@ -1,14 +0,0 @@
package codes
@Suppress("unused")
interface QuadConsumer<A,B,C,D> {
/**
* Performs this operation on the given arguments.
*
* @param a the first input argument
* @param b the second input argument
* @param c the third input argument
* @param d the fourth input argument
*/
fun accept(a: A, b: B, c: C, d: D)
}

View File

@@ -1,13 +0,0 @@
package codes
@Suppress("unused")
interface TriConsumer<A,B,C> {
/**
* Performs this operation on the given arguments.
*
* @param a the first input argument
* @param b the second input argument
* @param c the third input argument
*/
fun accept(a: A, b: B, c: C)
}

View File

@@ -1,6 +1,7 @@
package web
import StreamerOutputs
import barix.BarixConnection
import codes.Somecodes
import codes.Somecodes.Companion.GetSensorsInfo
import codes.Somecodes.Companion.GetUptime
@@ -46,6 +47,8 @@ import java.time.LocalDateTime
import codes.configKeys
import org.tinylog.Logger
import java.io.File
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.nio.file.Path
@@ -188,7 +191,9 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
}
}
} catch (e: Exception) {
println("Error processing WebSocket message: ${e.message}")
if (e.message!=null && (e.message is String) && e.message!!.isNotEmpty()) {
Logger.error { "Error processing WebSocket message: ${e.message}" }
}
}
}
@@ -234,6 +239,36 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
before { CheckUsers(it) }
}
path("api") {
path("LiveAudio"){
get("Open/{broadcastzone}"){ ctx ->
val param = ctx.pathParam("broadcastzone")
if (param.isNotEmpty()){
val bc = Get_Barix_Connection_by_ZoneName(param)
if (bc!=null){
val key = ctx.req().remoteAddr+":"+ctx.req().remotePort
val pipeIN = PipedInputStream(8192)
val pipeOUT = PipedOutputStream(pipeIN)
bc.AddPipeOut(key,pipeOUT)
ctx.contentType("audio/wav")
ctx.header("Cache-Control", "no-cache")
ctx.header("Connection", "keep-alive")
ctx.result(pipeIN)
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Broadcastzone not found")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid broadcastzone")))
}
get("Close/{broadcastzone}"){ ctx ->
val param = ctx.pathParam("broadcastzone")
if (param.isNotEmpty()){
val bc = Get_Barix_Connection_by_ZoneName(param)
if (bc!=null){
val key = ctx.req().remoteAddr+":"+ctx.req().remotePort
bc.RemovePipeOut(key)
ctx.result(objectmapper.writeValueAsString(resultMessage("OK")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Broadcastzone not found")))
} else ctx.status(400).result(objectmapper.writeValueAsString(resultMessage("Invalid broadcastzone")))
}
}
path("VoiceType") {
get {
it.result(objectmapper.writeValueAsString(VoiceType.entries.map { vt -> vt.name }))
@@ -2001,6 +2036,20 @@ fun CheckUsers(ctx: Context) {
}
}
fun Get_Barix_Connection_by_ZoneName(zonename: String) : BarixConnection? {
if (ValidString(zonename)){
val bz = db.broadcastDB.List.find{ it.description == zonename }
val sc = if (bz!=null) db.soundchannelDB.List.find { it.channel == bz.SoundChannel } else null
val ip = sc?.ip ?: ""
if (ValidIPV4(ip)){
// ketemu ip-nya
return StreamerOutputs[ip]
}
}
return null
}
fun Stop() {
app?.stop()