commit 07/02/2026
This commit is contained in:
@@ -58,10 +58,6 @@
|
||||
margin-right: .5rem!important;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: .5rem!important;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 1rem!important;
|
||||
}
|
||||
|
||||
@@ -25,22 +25,38 @@
|
||||
* @property {JQuery<HTMLElement> | null} ip - The jQuery result should be <h6> element.
|
||||
* @property {JQuery<HTMLElement> | null} buffer - The jQuery result should be <h6> element.
|
||||
* @property {JQuery<HTMLElement> | null} status - The jQuery result should be <p> element.
|
||||
* @property {JQuery<HTMLElement> | null} filename - The jQuery result should be <h6> element.
|
||||
* @property {JQuery<HTMLElement> | null} duration - The jQuery result should be <h6> element.
|
||||
* @property {JQuery<HTMLElement> | null} elapsed - The jQuery result should be <h6> element.
|
||||
* @property {JQuery<HTMLElement> | null} broadcastzones - The jQuery result should be <h6> element.
|
||||
* @property {JQuery<HTMLElement> | null} vu - The jQuery result should be <progress-bar> element.
|
||||
*/
|
||||
|
||||
function getCardByIndex(index) {
|
||||
let cardname = "ch" + index.toString().padStart(2, '0');
|
||||
let obj = {
|
||||
// title is <h4> element wiht id `streamertitle${index}`, with index as two digit number, e.g. 01, 02, 03
|
||||
title: $(`#streamertitle${index.toString().padStart(2, '0')}`),
|
||||
//title: $(`#streamertitle${index.toString().padStart(2, '0')}`),
|
||||
// ip is <h6> element with id `streamerip${index}`, with index as two digit number, e.g. 01, 02, 03
|
||||
ip: $(`#streamerip${index.toString().padStart(2, '0')}`),
|
||||
//ip: $(`#streamerip${index.toString().padStart(2, '0')}`),
|
||||
// buffer is <h6> element with id `streamerbuffer${index}`, with index as two digit number, e.g. 01, 02, 03
|
||||
buffer: $(`#streamerbuffer${index.toString().padStart(2, '0')}`),
|
||||
//buffer: $(`#streamerbuffer${index.toString().padStart(2, '0')}`),
|
||||
// status is <p> element with id `streamerstatus${index}`, with index as two digit number, e.g. 01, 02, 03
|
||||
status: $(`#streamerstatus${index.toString().padStart(2, '0')}`),
|
||||
//status: $(`#streamerstatus${index.toString().padStart(2, '0')}`),
|
||||
// vu is <progress-bar> element with id `streamervu${index}`, with index as two digit number, e.g. 01, 02, 03
|
||||
vu: $(`#streamervu${index.toString().padStart(2, '0')} .progress-bar`),
|
||||
//vu: $(`#streamervu${index.toString().padStart(2, '0')} .progress-bar`),
|
||||
card: $(`#${cardname}`),
|
||||
title: $(`#${cardname} .streamertitle`),
|
||||
ip: $(`#${cardname} .streamerip`),
|
||||
buffer: $(`#${cardname} .streamerbuffer`),
|
||||
status: $(`#${cardname} .streamerstatus`),
|
||||
vu: $(`#${cardname} .streamervu .progress-bar`),
|
||||
filename: $(`#${cardname} .streamerfile`),
|
||||
duration: $(`#${cardname} .streamerduration`),
|
||||
elapsed: $(`#${cardname} .streamerelapsed`),
|
||||
broadcastzones: $(`#${cardname} .streamerzones`),
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -64,29 +80,71 @@ function UpdateStreamerCard(values) {
|
||||
values = [];
|
||||
}
|
||||
|
||||
let visiblilitychanged = false;
|
||||
|
||||
for (let i = 1; i <= 64; i++) {
|
||||
let vv = values.find(v => v.index === i);
|
||||
let card = getCardByIndex(i);
|
||||
const vv = values.find(v => v.index === i);
|
||||
const cardname = "ch" + i.toString().padStart(2, '0');
|
||||
const $card = $(`#${cardname}`);
|
||||
|
||||
|
||||
if (vv) {
|
||||
// there is value for this index
|
||||
if (card.title) card.title.text(vv.channel ? vv.channel : `Channel ${i.toString().padStart(2, '0')}`);
|
||||
if (card.ip) card.ip.text(`IP Address: ${vv.ipaddress ? vv.ipaddress : 'N/A'}`);
|
||||
if (card.buffer) card.buffer.text(`Buffer: ${vv.bufferRemain !== undefined && vv.bufferRemain !== null ? vv.bufferRemain.toString() : 'N/A'}`);
|
||||
if (card.status) card.status.text(`Status: ${vv.isPlaying ? 'Playing' : 'Idle'}`);
|
||||
if (card.vu) {
|
||||
setProgress(i, card.vu, vv.vu, 100);
|
||||
// ada data untuk index i
|
||||
if ($card.length > 0) {
|
||||
// ada card untuk index i, show card
|
||||
if ($card.hasClass('d-none')) {
|
||||
visiblilitychanged = true;
|
||||
$card.removeClass('d-none');
|
||||
$card.closest('.streamercol').removeClass('d-none'); // show the column as well
|
||||
}
|
||||
const $title = $(`#${cardname} .streamertitle`);
|
||||
const $ip = $(`#${cardname} .streamerip`);
|
||||
const $buffer = $(`#${cardname} .streamerbuffer`);
|
||||
const $status = $(`#${cardname} .streamerstatus`);
|
||||
const $vu = $(`#${cardname} .streamervu .progress-bar`);
|
||||
const $filename = $(`#${cardname} .streamerfile`);
|
||||
const $duration = $(`#${cardname} .streamerduration`);
|
||||
const $elapsed = $(`#${cardname} .streamerelapsed`);
|
||||
const $broadcastzones = $(`#${cardname} .streamerzones`);
|
||||
|
||||
//console.log(`Updating card for index ${i}`, vv);
|
||||
$title.text(vv.channel ? vv.channel : `Channel ${i.toString().padStart(2, '0')}`);
|
||||
$ip.text(vv.ipaddress ? vv.ipaddress : 'N/A');
|
||||
$buffer.text(vv.bufferRemain !== undefined && vv.bufferRemain !== null ? vv.bufferRemain.toString() : 'N/A');
|
||||
$status.text(vv.isPlaying ? 'Playing' : 'Idle');
|
||||
setProgress(i, $vu, vv.vu ? vv.vu : 0, 100);
|
||||
$filename.text(vv.filename ? vv.filename : 'N/A');
|
||||
$duration.text(vv.duration ? vv.duration : 'N/A');
|
||||
$elapsed.text(vv.elapsed ? vv.elapsed : 'N/A');
|
||||
$broadcastzones.text(vv.broadcastzones ? vv.broadcastzones : 'N/A');
|
||||
}
|
||||
} else {
|
||||
// no value for this index, disable the card
|
||||
if (card.title) card.title.text(`Channel ${i.toString().padStart(2, '0')}`);
|
||||
if (card.ip) card.ip.text(`IP Address: N/A`);
|
||||
if (card.buffer) card.buffer.text(`Buffer: N/A`);
|
||||
if (card.status) card.status.text(`Status: Disconnected`);
|
||||
if (card.vu) {
|
||||
setProgress(i, card.vu, 0, 100);
|
||||
// tidak ada data untuk index i, hide card
|
||||
if ($card.length > 0) {
|
||||
// ada card untuk index i, hide card
|
||||
if (!$card.hasClass('d-none')) {
|
||||
visiblilitychanged = true;
|
||||
$card.addClass('d-none');
|
||||
$card.closest('.streamercol').addClass('d-none'); // hide the column as well
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// hide rows that have all cards hidden
|
||||
if (visiblilitychanged) {
|
||||
$('.streamerrow').each(function () {
|
||||
const $row = $(this);
|
||||
const visiblecards = $row.find('.streamercard:not(.d-none)');
|
||||
if (visiblecards.length === 0) {
|
||||
$row.addClass('d-none');
|
||||
} else {
|
||||
$row.removeClass('d-none');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -428,51 +486,51 @@ $(document).ready(function () {
|
||||
|
||||
runIntervalJob();
|
||||
|
||||
|
||||
|
||||
|
||||
window.addEventListener('ws_connected', () =>{
|
||||
|
||||
|
||||
window.addEventListener('ws_connected', () => {
|
||||
console.log("overview.js ws_connected event triggered");
|
||||
runIntervalJob();
|
||||
runIntervalJob();
|
||||
});
|
||||
window.addEventListener('ws_disconnected', ()=>{
|
||||
window.addEventListener('ws_disconnected', () => {
|
||||
console.log("overview.js ws_disconnected event triggered");
|
||||
if (intervaljob1) clearInterval(intervaljob1);
|
||||
if (intervaljob2) clearInterval(intervaljob2);
|
||||
intervaljob1 = null;
|
||||
intervaljob2 = null;
|
||||
if (intervaljob1) clearInterval(intervaljob1);
|
||||
if (intervaljob2) clearInterval(intervaljob2);
|
||||
intervaljob1 = null;
|
||||
intervaljob2 = null;
|
||||
});
|
||||
window.addEventListener('ws_message', ()=>{
|
||||
window.addEventListener('ws_message', () => {
|
||||
let rep = event.detail;
|
||||
let cmd = rep.reply;
|
||||
let data = rep.data;
|
||||
if (cmd && cmd.length > 0) {
|
||||
switch (cmd) {
|
||||
case "getPagingQueue":
|
||||
let pq = JSON.parse(data);
|
||||
window.PagingQueue = [];
|
||||
if (Array.isArray(pq) && pq.length > 0) {
|
||||
window.PagingQueue.push(...pq);
|
||||
}
|
||||
fill_pagingqueuetablebody(window.PagingQueue);
|
||||
break;
|
||||
case "getAASQueue":
|
||||
let aq = JSON.parse(data);
|
||||
window.QueueTable = [];
|
||||
if (Array.isArray(aq) && aq.length > 0) {
|
||||
window.QueueTable.push(...aq);
|
||||
}
|
||||
fill_automaticqueuetablebody(window.QueueTable);
|
||||
break;
|
||||
case "getStreamerOutputs":
|
||||
/**
|
||||
* @type {StreamerOutputData[]}
|
||||
*/
|
||||
let so = JSON.parse(data);
|
||||
UpdateStreamerCard(so);
|
||||
break;
|
||||
}
|
||||
let cmd = rep.reply;
|
||||
let data = rep.data;
|
||||
if (cmd && cmd.length > 0) {
|
||||
switch (cmd) {
|
||||
case "getPagingQueue":
|
||||
let pq = JSON.parse(data);
|
||||
window.PagingQueue = [];
|
||||
if (Array.isArray(pq) && pq.length > 0) {
|
||||
window.PagingQueue.push(...pq);
|
||||
}
|
||||
fill_pagingqueuetablebody(window.PagingQueue);
|
||||
break;
|
||||
case "getAASQueue":
|
||||
let aq = JSON.parse(data);
|
||||
window.QueueTable = [];
|
||||
if (Array.isArray(aq) && aq.length > 0) {
|
||||
window.QueueTable.push(...aq);
|
||||
}
|
||||
fill_automaticqueuetablebody(window.QueueTable);
|
||||
break;
|
||||
case "getStreamerOutputs":
|
||||
/**
|
||||
* @type {StreamerOutputData[]}
|
||||
*/
|
||||
let so = JSON.parse(data);
|
||||
UpdateStreamerCard(so);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(window).on('beforeunload', function () {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,20 +20,72 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card" id="streamercard">
|
||||
<div class="card streamercard" id="streamercard">
|
||||
<div class="card-body card-channel">
|
||||
<h4 class="card-title" id="streamertitle">Channel 01</h4>
|
||||
<h4 class="card-title streamertitle" id="streamertitle">Channel 01</h4>
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 col-xxl-6">
|
||||
<h6 class="text-muted mb-2" id="streamerip">IP : 192.168.10.10</h6>
|
||||
<div class="col-3">
|
||||
<p class="w-100 h-100 align-content-center">IP</p>
|
||||
</div>
|
||||
<div class="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 col-xxl-6">
|
||||
<h6 class="text-muted mb-2" id="streamerbuffer">Free : 64KB</h6>
|
||||
<div class="col">
|
||||
<p class="w-100 h-100 align-content-center streamerip" id="streamerip">N/A</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card-text" id="streamerstatus">Status : Idle</p>
|
||||
<div class="progress" id="streamervu">
|
||||
<div class="progress-bar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 50%;">50%</div>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<p class="w-100 h-100 align-content-center">Buffer</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p class="w-100 h-100 align-content-center streamerbuffer" id="streamerbuffer">N/A</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<p class="w-100 h-100 align-content-center">Status</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p class="w-100 h-100 align-content-center streamerstatus" id="streamerstatus">N/A</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<p class="w-100 h-100 align-content-center">File</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p class="w-100 h-100 align-content-center streamerfile" id="streamerfile">N/A</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<p class="w-100 h-100 align-content-center">Zones</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p class="w-100 h-100 align-content-center streamerzones" id="streamerzones">N/A</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<p class="w-100 h-100 align-content-center">Duration</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p class="w-100 h-100 align-content-center streamerduration" id="streamerduration">N/A</p>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<p class="w-100 h-100 align-content-center">Elapsed</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p class="w-100 h-100 align-content-center streamerelapsed" id="streamerelapsed">N/A</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<p class="w-100 h-100 align-content-center">VU</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="progress w-100 h-50 streamervu" id="streamervu">
|
||||
<div class="progress-bar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 50%;">50%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
24
src/Main.kt
24
src/Main.kt
@@ -177,13 +177,29 @@ fun main(args: Array<String>) {
|
||||
while (isActive) {
|
||||
delay(1000)
|
||||
// prioritas 1 , habisin queue paging
|
||||
subcode01.Read_Queue_Paging()
|
||||
if (subcode01.Read_Queue_Paging()){
|
||||
// processing paging, skip selanjutnya
|
||||
delay(2000)
|
||||
continue
|
||||
}
|
||||
// prioritas 2, habisin queue shalat
|
||||
subcode01.Read_Queue_Shalat()
|
||||
if (subcode01.Read_Queue_Shalat()){
|
||||
// processing shalat, skip selanjutnya
|
||||
delay(2000)
|
||||
continue
|
||||
}
|
||||
// prioritas 3, habisin queue timer
|
||||
subcode01.Read_Queue_Timer()
|
||||
if (subcode01.Read_Queue_Timer()){
|
||||
// processing timer, skip selanjutnya
|
||||
delay(2000)
|
||||
continue
|
||||
}
|
||||
// prioritas 4, habisin queue soundbank
|
||||
subcode01.Read_Queue_Soundbank()
|
||||
if (subcode01.Read_Queue_Soundbank()){
|
||||
// processing soundbank, skip selanjutnya
|
||||
delay(2000)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,4 +18,39 @@ class AudioFileInfo {
|
||||
fun isValid() : Boolean {
|
||||
return fileName.isNotBlank() && fileSize > 0 && duration > 0.0 && bytes.isNotEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the duration to a human-readable string format.
|
||||
* @return Duration as a string in HH:MM:SS or MM:SS format or SS if less than a minute.
|
||||
*/
|
||||
fun DurationToString() : String {
|
||||
val totalSeconds = duration.toInt()
|
||||
val hours = totalSeconds / 3600
|
||||
val minutes = (totalSeconds % 3600) / 60
|
||||
val seconds = totalSeconds % 60
|
||||
|
||||
return when {
|
||||
hours > 0 -> String.format("%02d h:%02d m:%02d s", hours, minutes, seconds)
|
||||
minutes > 0 -> String.format("%02d m:%02d s", minutes, seconds)
|
||||
else -> String.format("00:%02d s", seconds)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the file size to a human-readable string format.
|
||||
* @return File size as a string in Bytes, KB, MB, or GB.
|
||||
*/
|
||||
fun FileSizeToString() : String {
|
||||
val kb = 1024
|
||||
val mb = kb * 1024
|
||||
val gb = mb * 1024
|
||||
|
||||
return when {
|
||||
fileSize >= gb -> String.format("%.2f GB", fileSize.toDouble() / gb)
|
||||
fileSize >= mb -> String.format("%.2f MB", fileSize.toDouble() / mb)
|
||||
fileSize >= kb -> String.format("%.2f KB", fileSize.toDouble() / kb)
|
||||
else -> "$fileSize Bytes"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package barix
|
||||
|
||||
import audio.AudioFileInfo
|
||||
import audio.Mp3Encoder
|
||||
import codes.Somecodes
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -16,7 +15,6 @@ import java.nio.ByteBuffer
|
||||
import java.util.function.Consumer
|
||||
import kotlin.experimental.or
|
||||
|
||||
@Suppress("unused")
|
||||
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
|
||||
@@ -29,6 +27,89 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
|
||||
private val mp3Consumer = mutableMapOf<String, Consumer<ByteArray>>()
|
||||
private val udp = DatagramSocket()
|
||||
private var _barixmode: Boolean = false
|
||||
private var _usedbybroadcastzone = mutableSetOf<String>()
|
||||
private var afi : AudioFileInfo? = null
|
||||
private var starttick: Long? = null
|
||||
|
||||
/**
|
||||
* Set audio file information used for playback
|
||||
* @param value The AudioFileInfo object containing audio file details
|
||||
*/
|
||||
fun SetAudioFileInfo(value: AudioFileInfo?){
|
||||
afi = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audio file information used for playback
|
||||
* @return The AudioFileInfo object containing audio file details, or null if not set
|
||||
*/
|
||||
fun GetAudioFileInfo() : AudioFileInfo?{
|
||||
return afi
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the start tick for playback timing
|
||||
* @param tick The start tick in milliseconds
|
||||
*/
|
||||
fun SetStartTick(tick: Long?){
|
||||
starttick = tick
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the elapsed time since the start tick
|
||||
* @return Elapsed time as a formatted string or "N/A" if start tick is not set
|
||||
*/
|
||||
fun GetElapsed() : String {
|
||||
if (starttick != null){
|
||||
val elapsedMs = System.currentTimeMillis() - starttick!!
|
||||
val totalSeconds = elapsedMs / 1000
|
||||
val hours = totalSeconds / 3600
|
||||
val minutes = (totalSeconds % 3600) / 60
|
||||
val seconds = totalSeconds % 60
|
||||
|
||||
return when {
|
||||
hours > 0 -> String.format("%02d h:%02d m:%02d s", hours, minutes, seconds)
|
||||
minutes > 0 -> String.format("%02d m:%02d s", minutes, seconds)
|
||||
else -> String.format("00:%02d s", seconds)
|
||||
}
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a broadcast zone that uses this Barix device
|
||||
* @param zoneName The name of the broadcast zone
|
||||
*/
|
||||
fun AddUsedByBroadcastZone(zoneName: String){
|
||||
_usedbybroadcastzone.add(zoneName)
|
||||
println("Added used by broadcast zone: $zoneName to Barix device $ipaddress")
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple broadcast zones that use this Barix device
|
||||
* @param zoneNames The list of broadcast zone names
|
||||
*/
|
||||
fun AddUsedByBroadcastZone(zoneNames: List<String>){
|
||||
for (zoneName in zoneNames){
|
||||
AddUsedByBroadcastZone(zoneName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all broadcast zones that use this Barix device
|
||||
*/
|
||||
fun ClearUsedByBroadcastZones(){
|
||||
_usedbybroadcastzone.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a comma-separated string of broadcast zones that use this Barix device
|
||||
* @return Comma-separated string of broadcast zones
|
||||
*/
|
||||
fun GetUsedByBroadcastZones() : String{
|
||||
return _usedbybroadcastzone.joinToString(", ")
|
||||
}
|
||||
|
||||
/**
|
||||
* Barix mode flag
|
||||
@@ -53,6 +134,7 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
|
||||
mp3Consumer.remove(key)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun Exists_Mp3_Consumer(key: String) : Boolean{
|
||||
return mp3Consumer.containsKey(key)
|
||||
}
|
||||
@@ -129,14 +211,59 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
|
||||
return statusData == 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if buffer has enough space (more than 10,000 bytes)
|
||||
* @return true if buffer has enough space
|
||||
*/
|
||||
fun bufferEnough(): Boolean{
|
||||
return isOnline() && (bufferRemain > 10000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data to Barix device via UDP
|
||||
* @param data The data to send
|
||||
* @param cbOK Callback function if sending is successful
|
||||
* @param cbFail Callback function if sending fails
|
||||
* @param cbPlaying Callback function to indicate if device is playing
|
||||
*/
|
||||
fun SendData(data: ByteArray, cbOK: Consumer<String>, cbFail: Consumer<String>) {
|
||||
fun SendData(data: ByteArray, cbOK: Consumer<String>, cbFail: Consumer<String>, cbPlaying: Consumer<Boolean>) {
|
||||
if (data.isNotEmpty()) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val bb = ByteBuffer.wrap(data)
|
||||
while(!bufferEnough()){
|
||||
delay(20)
|
||||
Logger.info{"Waiting for StreamerOutput $ipaddress buffer to have enough space: $bufferRemain bytes available, need more than 10000 bytes"}
|
||||
}
|
||||
val bufmax = bufferRemain
|
||||
val bufkosong = 0.2 * bufmax
|
||||
val bufpenuh = 0.8 * bufmax
|
||||
Logger.info{"Starting to send data to StreamerOutput $ipaddress on channel $channel, total data size: ${data.size} bytes, bufferRemain: $bufferRemain bytes, bufkosong: $bufkosong bytes, bufpenuh: $bufpenuh bytes"}
|
||||
// Ide 07/02/2026, kasih buffer dummy 10x1000 byte pertama biar barix kebacanya stabil
|
||||
for (i in 1..10){
|
||||
val chunk = ByteArray(1000){0}
|
||||
udp.send(DatagramPacket(chunk, chunk.size, inet))
|
||||
delay(5)
|
||||
println("Sending dummy buffer $i to $ipaddress")
|
||||
}
|
||||
|
||||
// delay interval awal = 5 ms untuk streamer output dan 10 ms untuk barix
|
||||
// slow down interval = 5 ms untuk streamer output dan 10 ms untuk barix
|
||||
// speed up interval = 2 ms untuk streamer output dan 5 ms untuk barix
|
||||
val slowdowninterval = if (BarixMode) 8L else 5L
|
||||
val speedupinterval = if (BarixMode) 5L else 2L
|
||||
var delayinterval = if (BarixMode) 8L else 5L
|
||||
|
||||
// buat hitung elapsed
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
SetStartTick(System.currentTimeMillis())
|
||||
cbPlaying.accept(true)
|
||||
do{
|
||||
delay(1000)
|
||||
} while (isPlaying())
|
||||
cbPlaying.accept(false)
|
||||
SetStartTick(null)
|
||||
}
|
||||
|
||||
|
||||
while(bb.hasRemaining()){
|
||||
try {
|
||||
@@ -144,51 +271,40 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
|
||||
bb.get(chunk)
|
||||
while(bufferRemain<chunk.size){
|
||||
delay(10)
|
||||
// gas-rem pengiriman
|
||||
when{
|
||||
bufferRemain <= bufkosong -> {
|
||||
if (delayinterval!=slowdowninterval){
|
||||
delayinterval = slowdowninterval
|
||||
Logger.info{"Sending to $ipaddress on channel $channel, bufferRemain low: $bufferRemain bytes, slowing down to $delayinterval ms"}
|
||||
}
|
||||
}
|
||||
bufferRemain >= bufpenuh -> {
|
||||
if (delayinterval!=speedupinterval){
|
||||
delayinterval = speedupinterval
|
||||
Logger.info{"Sending to $ipaddress on channel $channel, bufferRemain high: $bufferRemain bytes, speeding up to $delayinterval ms"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
udp.send(DatagramPacket(chunk, chunk.size, inet))
|
||||
|
||||
mp3encoder.PushData(chunk)
|
||||
if (_barixmode) delay(10) else delay(1)
|
||||
|
||||
|
||||
delay(delayinterval)
|
||||
} catch (e: Exception) {
|
||||
Logger.error{"SendData to $ipaddress failed, message: ${e.message}"}
|
||||
cbFail.accept("SendData to $ipaddress failed, message: ${e.message}")
|
||||
cbPlaying.accept(false)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
cbOK.accept("SendData to $channel ($ipaddress) succeeded, ${data.size} bytes sent")
|
||||
Logger.info{"SendData to $channel ($ipaddress) ended, ${data.size} bytes sent"}
|
||||
cbOK.accept("SendData to $channel ($ipaddress) ended, ${data.size} bytes sent")
|
||||
}
|
||||
|
||||
} else cbFail.accept("SendData to $ipaddress failed, data is empty")
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert BarixConnection to JsonNode
|
||||
* @return JsonNode representation of BarixConnection
|
||||
*/
|
||||
fun toJsonNode(): JsonNode {
|
||||
// make json node from index, channel, ipaddress, port, bufferRemain, statusData, vu
|
||||
return Somecodes.objectmapper.createObjectNode().apply {
|
||||
put("index", index.toInt())
|
||||
put("channel", channel)
|
||||
put("ipaddress", ipaddress)
|
||||
put("port", port)
|
||||
put("bufferRemain", bufferRemain)
|
||||
put("statusData", statusData)
|
||||
put("vu", vu)
|
||||
put("isOnline", isOnline())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert BarixConnection to JSON string
|
||||
* @return JSON string representation of BarixConnection
|
||||
*/
|
||||
fun toJsonString(): String {
|
||||
return Somecodes.toJsonString(toJsonNode())
|
||||
}
|
||||
|
||||
fun ActivateRelay(relays: List<Int>){
|
||||
if (relays.isNotEmpty()){
|
||||
var value : Byte = 0
|
||||
@@ -201,22 +317,6 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate relay on Barix device
|
||||
* @param relays The relay numbers to activate (1-8)
|
||||
*/
|
||||
fun ActivateRelay(vararg relays: Int){
|
||||
if (relays.isNotEmpty()){
|
||||
var value : Byte = 0
|
||||
for (r in relays){
|
||||
if (r in 1..8){
|
||||
value = value or (1 shl (r - 1)).toByte()
|
||||
}
|
||||
}
|
||||
SendSimpleCommand(byteArrayOf(0x1A, value, 0x61))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate relay on Barix device
|
||||
*/
|
||||
@@ -247,41 +347,7 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send command to Barix device
|
||||
* @param command The command to send
|
||||
* @return true if successful
|
||||
*/
|
||||
fun SendCommand(command: String): Boolean {
|
||||
try {
|
||||
if (_tcp!=null){
|
||||
if (!_tcp!!.isClosed){
|
||||
val bb = command.toByteArray()
|
||||
val size = bb.size + 4
|
||||
val b4 = byteArrayOf(
|
||||
(size shr 24 and 0xFF).toByte(),
|
||||
(size shr 16 and 0xFF).toByte(),
|
||||
(size shr 8 and 0xFF).toByte(),
|
||||
(size and 0xFF).toByte()
|
||||
)
|
||||
val out = _tcp!!.getOutputStream()
|
||||
out.write(b4)
|
||||
out.write(bb)
|
||||
out.flush()
|
||||
Logger.info { "SendCommand to $ipaddress : $command" }
|
||||
return true
|
||||
}else {
|
||||
Logger.error { "Socket to $ipaddress is not connected" }
|
||||
}
|
||||
} else {
|
||||
Logger.error { "Socket to $ipaddress is null" }
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error { "Failed to SendCommand to $ipaddress, Message : ${e.message}" }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
try{
|
||||
|
||||
@@ -7,17 +7,15 @@ import org.tinylog.Logger
|
||||
import java.io.DataInputStream
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.function.Consumer
|
||||
|
||||
@Suppress("unused")
|
||||
class TCP_Barix_Command_Server {
|
||||
lateinit var tcpserver: ServerSocket
|
||||
lateinit var job: Job
|
||||
private val socketMap = mutableMapOf<String, Socket>()
|
||||
|
||||
private val regex = """STATUSBARIX;(\d+);(\d+)(;(\d+))?"""
|
||||
private val pattern = Regex(regex)
|
||||
//private val regex = """STATUSBARIX;(\d+);(\d+)(;(\d+))?"""
|
||||
//private val pattern = Regex(regex)
|
||||
|
||||
/**
|
||||
* Start TCP Command Server
|
||||
@@ -43,6 +41,7 @@ class TCP_Barix_Command_Server {
|
||||
try{
|
||||
|
||||
val din = DataInputStream(socket.getInputStream())
|
||||
var VuZeroCounter = 0L
|
||||
while (isActive) {
|
||||
|
||||
val bb = ByteArray(128)
|
||||
@@ -60,25 +59,56 @@ class TCP_Barix_Command_Server {
|
||||
Logger.error { "Error reading length from Streamer Output with IP $key, Message : ${ex.message}" }
|
||||
continue
|
||||
}
|
||||
val str = String(bb,4, stringlength).trim()
|
||||
var str = String(bb,4, stringlength).trim()
|
||||
if (str.isBlank()) continue
|
||||
if (!str.startsWith("STATUSBARIX")) continue
|
||||
if (str.endsWith("@")) str = str.removeSuffix("@")
|
||||
if (ValidString(str)) {
|
||||
// Valid command from Barix is in format $"STATUSBARIX;VU;BuffRemain;StatusData"$
|
||||
pattern.find(str)?.let { matchResult ->
|
||||
val (vu, buffremain, statusdata) = matchResult.destructured
|
||||
val status = BarixStatus(
|
||||
socket.inetAddress.hostAddress,
|
||||
vu.toInt(),
|
||||
buffremain.toInt(),
|
||||
statusdata.toIntOrNull() ?: 0,
|
||||
statusdata.isNullOrEmpty() // barix tidak ada statusdata , Q-AG1 ada
|
||||
)
|
||||
//Logger.info { "Received valid command from $key : $status" }
|
||||
cb.accept(status)
|
||||
} ?: run {
|
||||
Logger.warn { "Invalid command format from $key : $str" }
|
||||
// Valid command from StreamerOutput is in format $"STATUSBARIX;VU;BuffRemain;StatusData"$
|
||||
// Valid command from Barix is in format $"STATUSBARIX;VU;BuffRemain"$
|
||||
val values = str.split(";")
|
||||
if (values.size<3) continue
|
||||
if ("STATUSBARIX" != values[0]) continue
|
||||
val vu = values[1].toIntOrNull() ?: continue
|
||||
|
||||
val buffremain = values[2].toIntOrNull() ?: continue
|
||||
var status: BarixStatus
|
||||
when(values.size){
|
||||
3 ->{
|
||||
// mode barix
|
||||
// kadang vu stuck tidak di 0 saat idle,
|
||||
// jadi kalau vu <512 selama 10 kali berturut2
|
||||
// dan buffer lebih dari 16000, anggap idle
|
||||
if ((vu < 512) && (buffremain>=16000)){
|
||||
VuZeroCounter++
|
||||
} else {
|
||||
VuZeroCounter = 0
|
||||
}
|
||||
// statusdata = isplaying = , if VuZeroCounter >=10 then idle (0) else playing (1)
|
||||
val statusdata = if (VuZeroCounter>=10) 0 else 1
|
||||
status = BarixStatus(
|
||||
socket.inetAddress.hostAddress,
|
||||
vu,
|
||||
buffremain,
|
||||
statusdata,
|
||||
true
|
||||
)
|
||||
}
|
||||
4 ->{
|
||||
// mode Q-AG1
|
||||
val statusdata = values[3].toIntOrNull() ?: 0
|
||||
status = BarixStatus(
|
||||
socket.inetAddress.hostAddress,
|
||||
vu,
|
||||
buffremain,
|
||||
statusdata,
|
||||
false
|
||||
)
|
||||
}
|
||||
else -> continue
|
||||
}
|
||||
cb.accept(status)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -109,6 +109,14 @@ class Somecodes {
|
||||
} else cb.accept(-1,-1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string of numbers separated by commas or semicolons into a list of integers.
|
||||
*/
|
||||
fun StringToListInt(value : String?) : List<Int>{
|
||||
if (value.isNullOrBlank()) return listOf()
|
||||
return value.split(",",";").mapNotNull { it.trim().toIntOrNull() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all files in PagingResult directory.
|
||||
* If PagingResult directory does not exist, callback with -1,-1.
|
||||
|
||||
@@ -72,14 +72,13 @@ class TCP_Android_Command_Server {
|
||||
//println("Received command from $key : $str")
|
||||
str.split("@").map { it.trim() }.filter { ValidString(it) }
|
||||
.forEach {
|
||||
Logger.info{"Receive command from $key : $it"}
|
||||
process_command(key,it) { reply ->
|
||||
try {
|
||||
val cc = String_to_Byte_Android(reply)
|
||||
if (cc.isNotEmpty()){
|
||||
dout.write(cc)
|
||||
dout.flush()
|
||||
Logger.info{"Sent reply ${cc.size} bytes to $key : $reply"}
|
||||
//Logger.info{"Sent reply ${cc.size} bytes to $key : $reply"}
|
||||
} else Logger.error { "Empty reply to send to $key" }
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -149,7 +148,7 @@ class TCP_Android_Command_Server {
|
||||
* @param cb Callback to send reply string
|
||||
*/
|
||||
private fun process_command(key: String, cmd: String, cb: Consumer<String>) {
|
||||
Logger.info { "Command from $key : $cmd" }
|
||||
if ("PING" != cmd) Logger.info { "Command from $key : $cmd" }
|
||||
val parts = cmd.split(";").map { it.trim() }.filter { it.isNotBlank() }
|
||||
when (parts[0]) {
|
||||
"GETLOGIN" -> {
|
||||
|
||||
@@ -25,4 +25,28 @@ data class BroadcastZones(var index: UInt, var description: String, var SoundCha
|
||||
override fun toString(): String {
|
||||
return "BroadcastZones(index=$index, description='$description', SoundChannel='$SoundChannel', id='$id', bp='$bp')"
|
||||
}
|
||||
|
||||
companion object{
|
||||
|
||||
/**
|
||||
* Get a list of relay numbers from the broadcast zone's bp field.
|
||||
* Currently, supports relays 1 to 8.
|
||||
* @param bz The BroadcastZones object
|
||||
* @return List of relay numbers (Int) extracted from the bp field
|
||||
*/
|
||||
fun getRelaysFromBroadcastZone(bz : BroadcastZones) : List<Int>{
|
||||
val result = ArrayList<Int>()
|
||||
// delimiters either comma or semicolon
|
||||
val parts = bz.bp.split(",", ";")
|
||||
for (part in parts){
|
||||
val relay = part.trim().toIntOrNull()
|
||||
if (relay != null){
|
||||
if (relay in 1..8){
|
||||
result.add(relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package web
|
||||
|
||||
class StreamerOutputData(val index: UInt, val channel: String, val ipaddress: String, val vu: Int, val bufferRemain: Int, var isPlaying: Boolean) {
|
||||
data class StreamerOutputData(val index: UInt, val channel: String, val ipaddress: String, val vu: Int, val bufferRemain: Int, val isPlaying: Boolean, val filename: String, val duration: String, val elapsed: String, val broadcastzones: String) {
|
||||
companion object{
|
||||
fun fromBarixConnection(bc: barix.BarixConnection): StreamerOutputData {
|
||||
return StreamerOutputData(bc.index, bc.channel, bc.ipaddress, bc.vu, bc.bufferRemain, bc.isPlaying())
|
||||
return StreamerOutputData(bc.index, bc.channel, bc.ipaddress, bc.vu, bc.bufferRemain, bc.isPlaying(), bc.GetAudioFileInfo()?.fileName ?: "", bc.GetAudioFileInfo()?.DurationToString() ?: "", bc.GetElapsed(), bc.GetUsedByBroadcastZones())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user