diff --git a/html/webpage/assets/js/overview.js b/html/webpage/assets/js/overview.js
index 83a9e68..35e0fd1 100644
--- a/html/webpage/assets/js/overview.js
+++ b/html/webpage/assets/js/overview.js
@@ -55,7 +55,6 @@ function UpdateStreamerCard(values) {
const v = Number(value ?? 0);
const pct = Math.max(0, Math.min(100, Math.round((v / max) * 100)));
//if (index!==1) return; // only update index 1 for testing
- //console.log(`setProgress: index=${index}, value=${v}, pct=${pct}`);
$bar
.attr('aria-valuenow', v) // semantic value
.css('width', pct + '%') // visual width
@@ -162,7 +161,6 @@ function fill_automaticqueuetablebody(vv) {
if (!Array.isArray(vv) || vv.length === 0) return;
vv.forEach(item => {
// fill index and description columns using item properties
- //console.log("fill_automaticqueuetablebody: item", item);
$('#automaticqueuetable').append(`
| ${item.index} |
${item.date_Time} |
@@ -218,7 +216,6 @@ function reloadAutomaticQueue(APIURL = "QueueTable/") {
function RemovePagingQueueByIndex(index, APIURL = "QueuePaging/") {
fetchAPI(APIURL + "DeleteByIndex/" + index, "DELETE", {}, null, (okdata) => {
- console.log("RemovePagingQueueByIndex: okdata", okdata);
reloadPagingQueue(APIURL);
}, (errdata) => {
console.log("RemovePagingQueueByIndex: errdata", errdata);
@@ -227,7 +224,6 @@ function RemovePagingQueueByIndex(index, APIURL = "QueuePaging/") {
function RemoveAutomaticQueueByIndex(index, APIURL = "QueueTable/") {
fetchAPI(APIURL + "DeleteByIndex/" + index, "DELETE", {}, null, (okdata) => {
- console.log("RemoveAutomaticQueueByIndex: okdata", okdata);
reloadAutomaticQueue(APIURL);
}, (errdata) => {
console.log("RemoveAutomaticQueueByIndex: errdata", errdata);
@@ -258,16 +254,16 @@ function GetListeningZones() {
* @param {Function} cbFail callback function on failure
*/
function LiveAudioCommand(command, bz, cbOK = null, cbFail = null) {
- function raise_cbOK(value=null){
+ function raise_cbOK(value = null) {
if (cbOK) cbOK(value);
}
- function raise_cbFail(value){
+ function raise_cbFail(value) {
if (cbFail) cbFail(value);
}
- if (command && command.length>0){
- if (bz && bz.length>0){
- if (command === 'Open' || command === 'Close'){
+ if (command && command.length > 0) {
+ if (bz && bz.length > 0) {
+ if (command === 'Open' || command === 'Close') {
let url = `/api/LiveAudio`;
let payload = {
method: 'POST',
@@ -281,22 +277,19 @@ function LiveAudioCommand(command, bz, cbOK = null, cbFail = null) {
};
fetch(url, payload)
.then(response => {
- console.log(`Fetch response for Live Audio Command ${command}:`, JSON.stringify(response));
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
- console.log(`Live Audio Command ${command} for Broadcast Zone: ${bz} executed on server.`, data);
raise_cbOK(data);
})
.catch(error => {
- console.log(`Error executing Live Audio Command ${command} for Broadcast Zone: ${bz} on server. ${error}`);
raise_cbFail(error);
});
- } else raise_cbFail("LiveAudioCommand: Unknown command "+command);
+ } else raise_cbFail("LiveAudioCommand: Unknown command " + command);
} else raise_cbFail("LiveAudioCommand: Broadcast Zone is empty");
} else raise_cbFail("LiveAudioCommand: command is empty");
}
@@ -308,7 +301,6 @@ let streamws = null;
let mediasource = null;
$(document).ready(function () {
- console.log("overview.js loaded");
GetListeningZones();
@@ -316,8 +308,10 @@ $(document).ready(function () {
let bz = $("#listenzone").val();
let $icon = $(this).find('svg');
if ($icon.hasClass('fa-stop')) {
- console.log("Stopping Live Audio for Broadcast Zone:", bz);
- LiveAudioCommand('Close', bz, (okdata) =>{
+ LiveAudioCommand('Close', bz, (okdata) => {
+ if (okdata.message && okdata.message.length > 0) {
+ console.log("Live Audio Session Closed:", okdata.message);
+ }
$icon.toggleClass('fa-stop fa-play');
$("#listenzone").prop('disabled', false);
if (streamws) {
@@ -331,34 +325,37 @@ $(document).ready(function () {
let audio = document.getElementById('listenaudio');
audio.src = "";
- }, (errdata) =>{
+ }, (errdata) => {
alert("Error stopping Live Audio: " + errdata);
});
} else {
- console.log("Starting Live Audio for Broadcast Zone:", bz);
- LiveAudioCommand('Open', bz, (okdata) =>{
+ LiveAudioCommand('Open', bz, (okdata) => {
+ if (okdata.message && okdata.message.length > 0) {
+ let uuid = okdata.message;
+ console.log("Live Audio Session UUID:", uuid);
+ }
$icon.toggleClass('fa-stop fa-play');
$("#listenzone").prop('disabled', true);
- streamws = new WebSocket(`ws://${window.location.host}/LiveAudio/ws`);
+ streamws = new WebSocket(`ws://${window.location.host}/api/LiveAudio/ws`);
+ streamws.binaryType = 'arraybuffer';
mediasource = new MediaSource();
let audio = document.getElementById('listenaudio');
audio.src = URL.createObjectURL(mediasource);
mediasource.addEventListener('sourceopen', () => {
- const sourceBuffer = mediasource.addSourceBuffer('audio/mpeg; codecs="mp3"');
- streamws.binaryType = 'arraybuffer';
+ const sourceBuffer = mediasource.addSourceBuffer('audio/mpeg');
streamws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const chunk = new Uint8Array(event.data);
sourceBuffer.appendBuffer(chunk);
- }
+ }
};
});
- }, (errdata) =>{
+ }, (errdata) => {
alert("Error starting Live Audio: " + errdata);
});
}
-
+
});
$('#clearpagingqueue').off('click').on('click', function () {
DoClear("QueuePaging/", "Paging Queue", (okdata) => {
@@ -445,7 +442,6 @@ $(document).ready(function () {
switch (cmd) {
case "getPagingQueue":
let pq = JSON.parse(data);
- //console.log("getPagingQueue:", pq);
window.PagingQueue = [];
if (Array.isArray(pq) && pq.length > 0) {
window.PagingQueue.push(...pq);
@@ -454,7 +450,6 @@ $(document).ready(function () {
break;
case "getAASQueue":
let aq = JSON.parse(data);
- //console.log("getAASQueue:", aq);
window.QueueTable = [];
if (Array.isArray(aq) && aq.length > 0) {
window.QueueTable.push(...aq);
diff --git a/src/Main.kt b/src/Main.kt
index aa61ad5..bf200f8 100644
--- a/src/Main.kt
+++ b/src/Main.kt
@@ -231,7 +231,8 @@ fun main() {
db.Add_Log("AAS"," Application started")
// shutdown hook
- Runtime.getRuntime().addShutdownHook(Thread {
+ Runtime.getRuntime().addShutdownHook(Thread ({
+
db.Add_Log("AAS"," Application stopping")
Logger.info { "Shutdown hook called, stopping services..." }
barixserver.StopTcpCommand()
@@ -240,11 +241,12 @@ fun main() {
web.Stop()
udpreceiver.Stop()
tcpreceiver.Stop()
+ StreamerOutputs.values.forEach { it.close() }
audioPlayer.Close()
db.close()
Logger.info { "All services stopped, exiting application." }
ProviderRegistry.getLoggingProvider().shutdown()
- })
+ },"ShutdownHook") )
}
diff --git a/src/audio/Mp3Encoder.kt b/src/audio/Mp3Encoder.kt
index d55be5c..8a50a6b 100644
--- a/src/audio/Mp3Encoder.kt
+++ b/src/audio/Mp3Encoder.kt
@@ -48,9 +48,10 @@ class Mp3Encoder(val samplingrate: Int=44100, val channels: Int=1) {
push_handle = bass.BASS_StreamCreate(samplingrate, channels, Bass.BASS_STREAM_DECODE, Pointer(-1), null)
if (push_handle!=0){
Logger.info{"MP3 Encoder initialized with sampling rate $samplingrate Hz and $channels channel(s)" }
- //val options = "lame -b 128 --cbr --write-xing 0 --nohist --noid3v2 - -"
- val flag = BassEnc.BASS_ENCODE_AUTOFREE or BassEnc.BASS_ENCODE_NOHEAD
- mp3_handle = bassencmp3.BASS_Encode_MP3_Start(push_handle, null, flag,proc, null)
+ val options = "lame -b 128 --cbr --write-xing 0 --nohist --noid3v2 - -"
+ //val flag = BassEnc.BASS_ENCODE_AUTOFREE or BassEnc.BASS_ENCODE_NOHEAD
+ val flag = BassEnc.BASS_ENCODE_AUTOFREE
+ mp3_handle = bassencmp3.BASS_Encode_MP3_Start(push_handle, options, flag,proc, null)
if (mp3_handle!=0){
callback = cb
@@ -100,7 +101,6 @@ class Mp3Encoder(val samplingrate: Int=44100, val channels: Int=1) {
Logger.error{"Failed to push data to MP3 Encoder. BASS error code: $err" }
return 0
}
- //println("MP3 Encoder: Pushed $written bytes of PCM data.")
return written
}
@@ -122,6 +122,5 @@ class Mp3Encoder(val samplingrate: Int=44100, val channels: Int=1) {
// auto close by BASS_ENCODE_AUTOFREE
mp3_handle = 0
callback = null
- println("MP3 Encoder: Stopped.")
}
}
\ No newline at end of file
diff --git a/src/barix/BarixConnection.kt b/src/barix/BarixConnection.kt
index 131f7f5..24599e6 100644
--- a/src/barix/BarixConnection.kt
+++ b/src/barix/BarixConnection.kt
@@ -16,7 +16,7 @@ import java.nio.ByteBuffer
import java.util.function.Consumer
@Suppress("unused")
-class BarixConnection(val index: UInt, var channel: String, val ipaddress: String, val port: Int = 5002) {
+class BarixConnection(val index: UInt, var channel: String, val ipaddress: String, val port: Int = 5002) : AutoCloseable {
private var _bR: Int = 0
private var _sd: Int = 0
private var _vu: Int = 0
@@ -26,6 +26,13 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
private var _tcp: Socket? = null
private val mp3encoder = Mp3Encoder()
private val mp3Consumer = mutableMapOf>()
+ private val udp = DatagramSocket()
+
+ init {
+ mp3encoder.Start { data ->
+ mp3Consumer.values.forEach { it.accept(data) }
+ }
+ }
fun Add_Mp3_Consumer(key: String, cb: Consumer) {
mp3Consumer[key] = cb
@@ -118,33 +125,26 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
fun SendData(data: ByteArray, cbOK: Consumer, cbFail: Consumer) {
if (data.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
- DatagramSocket().use{ udp ->
- val bb = ByteBuffer.wrap(data)
- if (!mp3encoder.isStarted()) {
- mp3encoder.Start { data ->
- mp3Consumer.values.forEach { it.accept(data) }
+ val bb = ByteBuffer.wrap(data)
+
+ while(bb.hasRemaining()){
+ try {
+ val chunk = ByteArray(if (bb.remaining() > maxUDPsize) maxUDPsize else bb.remaining())
+ bb.get(chunk)
+ while(bufferRemain maxUDPsize) maxUDPsize else bb.remaining())
- bb.get(chunk)
- while(bufferRemain>, val
lateinit var app: Javalin
lateinit var semiauto: Javalin
val objectmapper = jacksonObjectMapper()
- val WsContextMap = mutableMapOf()
+ val WsContextMap = mutableMapOf()
private fun SendReply(context: WsMessageContext, command: String, value: String) {
try {
@@ -247,79 +248,100 @@ class WebApp(val listenPort: Int, val userlist: List>, val
path("api") {
//TODO https://stackoverflow.com/questions/70002015/streaming-into-audio-element
path("LiveAudio") {
- post{
- val json : JsonNode = objectmapper.readTree(it.body())
+ post{ ctx->
+ val json : JsonNode = objectmapper.readTree(ctx.body())
val broadcastzone = json.get("broadcastzone")?.asText("") ?: ""
val command = json.get("command")?.asText("") ?: ""
- println("LiveAudio command=$command for zone $broadcastzone from ${it.host()}" )
+ if (command == "Open" || command == "Close"){
+ if (broadcastzone.isNotEmpty()){
+ val bc = Get_Barix_Connection_by_ZoneName(broadcastzone)
+ if (bc!=null){
+ val key = ctx.cookie("client-stream-id")
+ if (command == "Open"){
+ // open command
+ if (key!=null && key.isNotEmpty()){
+ // ada connection sebelumnya, kemungkinan reconnect
+ val prev = WsContextMap[key]
+ if (prev!=null){
+ prev.bc?.Remove_Mp3_Consumer(key)
+ prev.ws?.closeSession(WsCloseStatus.NORMAL_CLOSURE, "Reopen Live Audio Stream")
+ }
+ WsContextMap.remove(key)
+ ctx.cookie("client-stream-id", "")
+ }
+ val newkey = UUID.randomUUID().toString()
+ .also { ctx.cookie("client-stream-id", it) }
+ WsContextMap[newkey] = LiveListenData(newkey, bc, null)
+ ResultMessageString(ctx, 200, newkey)
+
+
+ } else {
+ // close command
+ if (key!=null && key.isNotEmpty()){
+ // close connection
+ val prev = WsContextMap[key]
+ if (prev!=null){
+ prev.bc?.Remove_Mp3_Consumer(key)
+ prev.ws?.closeSession(WsCloseStatus.NORMAL_CLOSURE, "Close Live Audio Stream")
+ }
+ WsContextMap.remove(key)
+ ctx.cookie("client-stream-id", "")
+ ResultMessageString(ctx, 200, "OK")
+ } else ResultMessageString(ctx, 400, "No client-id cookie found")
+ }
+ } else ResultMessageString(ctx, 400, "Broadcastzone not found")
+ } else ResultMessageString(ctx, 400, "Invalid broadcastzone")
+ } else ResultMessageString(ctx, 400, "Invalid command")
}
- ws("/ws/{uuid}"){ws ->
- ws.onConnect {
- ctx ->
- val uuid = ctx.pathParam("uuid")
- val cookieresult = ctx.cookie("client-stream-id")
- println("Ws connected with uuid=$uuid, cookie=$cookieresult")
- WsContextMap[uuid] = ctx
- }
-
- ws.onClose {
- ctx ->
- val uuid = ctx.pathParam("uuid")
- println("Ws close on uuid=$uuid")
- WsContextMap.remove(uuid)
- }
- ws.onError {
- ctx ->
- val uuid = ctx.pathParam("uuid")
- println("Ws error on uuid=$uuid")
- WsContextMap.remove(uuid)
- }
- }
-
- get("Open/{broadcastzone}") { ctx ->
- val param = ctx.pathParam("broadcastzone")
- println("LiveAudio Open for zone $param from ${ctx.host()}" )
- if (param.isNotEmpty()) {
- val bc = Get_Barix_Connection_by_ZoneName(param)
- if (bc != null) {
-
- val key = ctx.cookie("client-stream-id") ?: UUID.randomUUID().toString()
- .also { ctx.cookie("client-stream-id", it) }
-
- bc.Add_Mp3_Consumer(key){ mp3data ->
- WsContextMap[key]?.send(mp3data)
- println("Send ${mp3data.size} to $key")
+ ws("ws"){ wscontext ->
+ wscontext.onConnect {
+ val key = it.cookie("client-stream-id")
+ if (key!=null && key.isNotEmpty()){
+ val lld = WsContextMap[key]
+ if (lld!=null){
+ it.enableAutomaticPings()
+ lld.ws = it
+ lld.bc?.Add_Mp3_Consumer(key){ mp3data->
+ try {
+ if (it.session.isOpen){
+ it.send(ByteBuffer.wrap(mp3data))
+ }
+ } catch (e: Exception){
+ Logger.error {"Error sending LiveAudio mp3 data for key $key, Message: ${e.message}"}
+ }
+ }
+ } else {
+ it.closeSession(WsCloseStatus.POLICY_VIOLATION, "WsContextMap key not found")
+ Logger.info{"LiveAudio WebSocket connection rejected, WsContextMap key not found"}
}
-
- ResultMessageString(ctx, 200, key)
- } 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")
- println("LiveAudio Close for zone $param")
- val key = ctx.cookie("client-stream-id")
- if (key != null && key.isNotEmpty()) {
- if (param.isNotEmpty()) {
- val bc = Get_Barix_Connection_by_ZoneName(param)
- if (bc != null) {
- bc.Remove_Mp3_Consumer(key)
- WsContextMap.remove(key)
- ResultMessageString(ctx, 200, "OK")
- println("LiveAudio Close for zone $param SUCCESS")
- } else ctx.status(400)
- .result(objectmapper.writeValueAsString(resultMessage("Broadcastzone not found")))
- } else ctx.status(400)
- .result(objectmapper.writeValueAsString(resultMessage("Invalid broadcastzone")))
- } else {
- ctx.status(400)
- .result(objectmapper.writeValueAsString(resultMessage("No client-id cookie found")))
+ } else {
+ it.closeSession(WsCloseStatus.POLICY_VIOLATION, "Invalid client-stream-id")
+ Logger.info{"LiveAudio WebSocket connection rejected, invalid client-stream-id"}
+ }
}
+ wscontext.onClose {
+ val key = it.cookie("client-stream-id")
+ if (key!=null && key.isNotEmpty()){
+ val lld = WsContextMap[key]
+ lld?.bc?.Remove_Mp3_Consumer(key)
+ lld?.ws?.closeSession()
+ lld?.bc = null
+ lld?.ws= null
+ WsContextMap.remove(key)
+ Logger.info{"LiveAudio WebSocket closed for key $key"}
+
+ }
+
+ }
+ wscontext.onError {
+ val key = it.cookie("client-stream-id")
+ val msg = it.error()?.message ?: ""
+ Logger.info{"LiveAudio WebSocket error for key $key, Message: $msg"}
+ }
+
}
+
}
path("VoiceType") {
get {
@@ -1876,7 +1898,6 @@ class WebApp(val listenPort: Int, val userlist: List>, val
val language = it.pathParam("language")
val voice = it.pathParam("voice")
val category = it.pathParam("category")
- //println("ListSoundbank called with language=$language, voice=$voice, category=$category")
if (ValidString(language) && Language.entries.any { lang -> lang.name == language }) {
if (ValidString(voice) && VoiceType.entries.any { vtype -> vtype.name == voice }) {
if (ValidString(category) && Category.entries.any { cat -> cat.name == category }) {
@@ -1889,7 +1910,6 @@ class WebApp(val listenPort: Int, val userlist: List>, val
// just the filename, without path
mm.substring(mm.lastIndexOf(File.separator) + 1)
}
- //println("ListSoundbank result: $result")
it.result(objectmapper.writeValueAsString(result))
} else {
it.status(400)
@@ -1911,7 +1931,6 @@ class WebApp(val listenPort: Int, val userlist: List>, val
val category = it.pathParam("category")
val uploaded = it.uploadedFiles()
- println("UploadSoundbank called with language=$language, voice=$voice, category=$category, uploaded files count=${uploaded.size}")
if (ValidString(language) && Language.entries.any { lang -> lang.name == language }) {
if (ValidString(voice) && VoiceType.entries.any { vtype -> vtype.name == voice }) {
if (ValidString(category) && Category.entries.any { cat -> cat.name == category }) {
@@ -2218,7 +2237,6 @@ class WebApp(val listenPort: Int, val userlist: List>, val
if (db.queuetableDB.Add(qt)) {
db.queuetableDB.Resort()
Logger.info { "SemiAutoWeb added to queue table: $qt" }
- //println("SemiAuto added to queue table: ${objectmapper.writeValueAsString(qt)}")
ctx.result(objectmapper.writeValueAsString(resultMessage("OK")))
} else ResultMessageString(ctx, 500, "Failed to add to queue table")
} else ResultMessageString(ctx, 400, "Broadcast zones cannot be empty")
@@ -2232,10 +2250,8 @@ class WebApp(val listenPort: Int, val userlist: List>, val
get("/{datelog}") { ctx ->
val datelog = ctx.pathParam("datelog")
if (ValidDate(datelog)) {
- println("SemiAuto Get Log for date $datelog")
db.GetLogForHtml(datelog) { loghtml ->
val resultstring = objectmapper.writeValueAsString(loghtml)
- println("Log HTML for date $datelog: $resultstring")
ctx.result(resultstring)
}