Compare commits

...

2 Commits

Author SHA1 Message Date
eba4f7852e Commit 07/08/2025 2025-08-07 15:04:53 +07:00
446f031535 Commit 06/08/2025 2025-08-06 16:35:20 +07:00
17 changed files with 659 additions and 385 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
### IntelliJ IDEA ###
out/
audiofile/
!**/src/main/**/out/
!**/src/test/**/out/

View File

@@ -9,6 +9,7 @@
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/html" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/audiofile" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@@ -1,16 +0,0 @@
{
"ZelloUsername": "gtcdevice01",
"ZelloPassword": "GtcDev2025",
"ZelloChannel": "GtcDev2025",
"ZelloServer": "community",
"ZelloWorkNetworkName": "",
"ZelloEnterpriseServerDomain": "",
"M1": "",
"M2": "",
"M3": "",
"M4": "",
"M5": "",
"M6": "",
"M7": "",
"M8": ""
}

View File

@@ -1,20 +1,20 @@
$(document).ready(function() {
// Your code here
console.log('pocreceiver.js is ready!');
//console.log('pocreceiver.js is ready!');
const path = window.location.pathname;
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
ws.onopen = function() {
console.log('WebSocket connection opened');
//console.log('WebSocket connection opened');
$('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden');
setInterval(function() {
sendCommand({ command: "getZelloStatus" });
}, 5000);
}, 1000);
};
ws.onmessage = function(event) {
console.log('WebSocket message received:', event.data);
//console.log('WebSocket message received:', event.data);
let msg = {};
try {
msg = JSON.parse(event.data);
@@ -25,13 +25,13 @@ $(document).ready(function() {
}
if (msg.reply === "getZelloStatus" && msg.data !== undefined && msg.data.length > 0) {
const zelloData = msg.data;
console.log('Zello Status Data:', zelloData);
$('#zelloStatus').text(zelloData.status);
//console.log('Zello Status Data:', zelloData);
$('#zelloStatus').text(zelloData);
}
};
ws.onclose = function() {
console.log('WebSocket connection closed');
//console.log('WebSocket connection closed');
$('#indicatorDisconnected').removeClass('visually-hidden');
$('#indicatorConnected').addClass('visually-hidden');
};

View File

@@ -1,6 +1,6 @@
$(document).ready(function () {
// Your code here
console.log('precordedbroadcast.js is ready!');
//console.log('precordedbroadcast.js is ready!');
const path = window.location.pathname;
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
for (let i = 1; i <= 8; i++) {
@@ -10,36 +10,55 @@ $(document).ready(function() {
}
ws.onopen = function () {
console.log('WebSocket connection opened');
//console.log('WebSocket connection opened');
$('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden');
sendCommand({ command: "getMessageConfig" });
setInterval(function () {
sendCommand({ command: "getPlaybackStatus" });
}, 5000);
}, 1000); // every second
};
ws.onmessage = function (event) {
console.log('WebSocket message received:', event.data);
//console.log('WebSocket message received:', event.data);
let msg = {};
try {
msg = JSON.parse(event.data);
} catch (e) {
return;
}
if (msg.reply === "getMessageConfig" && msg.data !== undefined) {
const messageConfigdata = msg.data;
console.log('Message Config Data:', messageConfigdata);
if (msg.reply && msg.reply.length > 0 && msg.data && msg.data.length > 0) {
switch (msg.reply) {
case "playMessage":
if (msg.data !== "success")
{alert(msg.data);}
else{
$('#playbackStatus').text('Playback started');
}
break;
case "stopMessage":
if (msg.data !== "success")
{alert(msg.data);}
else{
$('#playbackStatus').text('Playback stopped');
}
break;
case "getMessageConfig":
const messageConfigdata = JSON.parse(msg.data);
//console.log('Message Config Data:', messageConfigdata);
for (let i = 1; i <= 8; i++) {
let filetitle = $(`#fileM${i}`);
let playButton = $(`#playM${i}`);
let stopButton = $(`#stopM${i}`);
let fileInput = messageConfigdata[`M${i}`] || '';
filetitle.val(fileInput);
if (fileInput.length>0){
let fileInput = messageConfigdata[`M${i}`];
if (fileInput && fileInput.length > 0) {
filetitle.text(fileInput);
playButton.prop('disabled', false);
stopButton.prop('disabled', false);
playButton.removeClass('invisible');
stopButton.removeClass('invisible');
playButton.on('click', function () {
let cmd = {
command: "playMessage",
@@ -56,19 +75,27 @@ $(document).ready(function() {
sendCommand(cmd);
});
} else {
playButton.prop('disabled', true);
stopButton.prop('disabled', true);
filetitle.text('Not configured');
playButton.addClass('invisible');
stopButton.addClass('invisible');
}
}
break;
case "getPlaybackStatus":
$('#playbackStatus').text(msg.data);
break;
}
}
} else if (msg.reply === "getPlaybackStatus" && msg.data !== undefined && msg.data.length > 0) {
const playbackData = msg.data;
$('#playbackStatus').text(playbackData.status);
}
};
ws.onclose = function () {
console.log('WebSocket connection closed');
//console.log('WebSocket connection closed');
$('#indicatorDisconnected').removeClass('visually-hidden');
$('#indicatorConnected').addClass('visually-hidden');
};

View File

@@ -1,6 +1,6 @@
$(document).ready(function () {
// Your initialization code here
console.log('setting.js is ready!');
//console.log('setting.js is ready!');
$('#dropdownM1 button').text('');
$('#dropdownM2 button').text('');
$('#dropdownM3 button').text('');
@@ -24,7 +24,7 @@ $(document).ready(function() {
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
ws.onopen = function () {
console.log('WebSocket connection opened');
//console.log('WebSocket connection opened');
$('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden');
sendCommand({ command: "getConfig" });
@@ -32,16 +32,18 @@ $(document).ready(function() {
};
ws.onmessage = function (event) {
console.log('WebSocket message received:', event.data);
//console.log('WebSocket message received:', event.data);
let msg = {};
try {
msg = JSON.parse(event.data);
} catch (e) {
return;
}
if (msg.reply === "getConfig" && msg.data !== undefined ) {
if (msg.reply && msg.reply.length > 0) {
switch (msg.reply) {
case "getConfig":
const configData = JSON.parse(msg.data);
console.log('Config Data:', configData);
//console.log('Config Data:', configData);
$('#zelloUsername').val(configData.zelloUsername || '');
$('#zelloPassword').val(configData.zelloPassword || '');
$('#zelloChannel').val(configData.zelloChannel || '');
@@ -63,7 +65,7 @@ $(document).ready(function() {
const dropdownMenu = $(`#dropdownM${i} .dropdown-menu`);
const dropdownButton = $(`#dropdownM${i} button`);
dropdownMenu.empty();
const messages = configData[`MessageList`] || [];
const messages = configData[`messageList`] || [];
messages.forEach((msg, idx) => {
const item = $('<a class="dropdown-item" href="#"></a>').text(msg);
item.on('click', function () {
@@ -78,12 +80,31 @@ $(document).ready(function() {
dropdownButton.text('');
}
}
break;
case "setZelloConfig":
if (msg.data === "success") {
alert('Zello configuration updated successfully.');
} else {
alert('Failed to update Zello configuration: ' + msg.data);
}
sendCommand({ command: "getConfig" }); // Refresh config after update
break;
case "setMessageConfig":
if (msg.data === "success") {
alert('Message configuration updated successfully.');
} else {
alert('Failed to update Message configuration: ' + msg.data);
}
sendCommand({ command: "getConfig" }); // Refresh config after update
break;
}
}
};
ws.onclose = function () {
console.log('WebSocket connection closed');
//console.log('WebSocket connection closed');
$('#indicatorDisconnected').removeClass('visually-hidden');
$('#indicatorConnected').addClass('visually-hidden');
};
@@ -108,22 +129,25 @@ $(document).ready(function() {
$('#btnApplyZello').on('click', function () {
if (ws.readyState === WebSocket.OPEN) {
let xx = {
command: "setZelloConfig"
};
let data = {
command: "setZelloConfig",
ZelloUsername: $('#zelloUsername').val(),
ZelloPassword: $('#zelloPassword').val(),
ZelloChannel: $('#zelloChannel').val(),
ZelloServer: $('#zellocommunity').is(':checked') ? 'community' :
$('#zellowork').is(':checked') ? 'work' :
$('#zelloenterprise').is(':checked') ? 'enterprise' : ''
};
}
if ($('#zellowork').is(':checked')) {
data.ZelloWorkNetworkName = $('#zelloWorkNetworkName').val();
}
if ($('#zelloenterprise').is(':checked')) {
data.ZelloEnterpriseServerDomain = $('#zelloEnterpriseServerDomain').val();
}
sendCommand(data);
xx.data = JSON.stringify(data);
sendCommand(xx);
} else {
console.warn('WebSocket is not open.');
}
@@ -132,6 +156,7 @@ $(document).ready(function() {
if (ws.readyState === WebSocket.OPEN) {
let data = {
command: "setMessageConfig",
data: JSON.stringify({
M1: $('#dropdownM1 button').text(),
M2: $('#dropdownM2 button').text(),
M3: $('#dropdownM3 button').text(),
@@ -140,6 +165,8 @@ $(document).ready(function() {
M6: $('#dropdownM6 button').text(),
M7: $('#dropdownM7 button').text(),
M8: $('#dropdownM8 button').text()
})
};
sendCommand(data);
} else {
@@ -164,6 +191,7 @@ $(document).ready(function() {
.then(response => {
if (response.ok) {
alert('File uploaded successfully.');
sendCommand({ command: "getConfig" }); // Refresh config after upload
} else {
alert('File upload failed.');
}

View File

@@ -35,7 +35,7 @@
<h1 class="text-center">Zello Status</h1>
</div>
<div class="row">
<p id="zelloStatus">Paragraph</p>
<p class="d-flex justify-content-center" id="zelloStatus">Paragraph</p>
</div>
</div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>

View File

@@ -43,7 +43,7 @@
<div class="card">
<div class="card-body" id="cardM1">
<h4 class="d-flex justify-content-center card-title">Message1</h4>
<p class="text-start card-text" id="fileM1">File 01</p>
<p class="d-flex justify-content-center card-text" id="fileM1">File 01</p>
<div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM1" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM1" type="button">Stop</button></div>
@@ -55,7 +55,7 @@
<div class="card">
<div class="card-body" id="cardM2">
<h4 class="d-flex justify-content-center card-title">Message2</h4>
<p class="text-start card-text" id="fileM2">File 02</p>
<p class="d-flex justify-content-center card-text" id="fileM2">File 02</p>
<div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM2" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM2" type="button">Stop</button></div>
@@ -69,7 +69,7 @@
<div class="card">
<div class="card-body" id="cardM3">
<h4 class="d-flex justify-content-center card-title">Message3</h4>
<p class="text-start card-text" id="fileM3">File 03</p>
<p class="d-flex justify-content-center card-text" id="fileM3">File 03</p>
<div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM3" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM3" type="button">Stop</button></div>
@@ -81,7 +81,7 @@
<div class="card">
<div class="card-body" id="cardM4">
<h4 class="d-flex justify-content-center card-title">Message4</h4>
<p class="text-start card-text" id="fileM4">File 04</p>
<p class="d-flex justify-content-center card-text" id="fileM4">File 04</p>
<div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM4" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM4" type="button">Stop</button></div>
@@ -95,7 +95,7 @@
<div class="card">
<div class="card-body" id="cardM5">
<h4 class="d-flex justify-content-center card-title">Message5</h4>
<p class="text-start card-text" id="fileM5">File 05</p>
<p class="d-flex justify-content-center card-text" id="fileM5">File 05</p>
<div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM5" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM5" type="button">Stop</button></div>
@@ -107,7 +107,7 @@
<div class="card">
<div class="card-body" id="cardM6">
<h4 class="d-flex justify-content-center card-title">Message6</h4>
<p class="text-start card-text" id="fileM6">File 06</p>
<p class="d-flex justify-content-center card-text" id="fileM6">File 06</p>
<div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM6" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM6" type="button">Stop</button></div>
@@ -121,7 +121,7 @@
<div class="card">
<div class="card-body" id="cardM7">
<h4 class="d-flex justify-content-center card-title">Message7</h4>
<p class="text-start card-text" id="fileM7">File 07</p>
<p class="d-flex justify-content-center card-text" id="fileM7">File 07</p>
<div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM7" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM7" type="button">Stop</button></div>
@@ -133,7 +133,7 @@
<div class="card">
<div class="card-body" id="cardM8">
<h4 class="d-flex justify-content-center card-title">Message8</h4>
<p class="text-start card-text" id="fileM8">File 08</p>
<p class="d-flex justify-content-center card-text" id="fileM8">File 08</p>
<div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM8" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM8" type="button">Stop</button></div>

View File

@@ -2,7 +2,7 @@ import audio.AudioFilePlayer
import audio.AudioUtility
import audio.OpusStreamReceiver
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import somecodes.Codes.Companion.ValidString
import somecodes.configFile
import web.WsReply
@@ -10,8 +10,12 @@ import web.webApp
import zello.ZelloClient
import zello.ZelloEvent
import javafx.util.Pair
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import somecodes.Codes.Companion.ValidFile
import somecodes.Codes
import web.WsCommand
import java.util.function.BiFunction
@@ -19,46 +23,205 @@ import java.util.function.BiFunction
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
fun main() {
val logger = LoggerFactory.getLogger("Main")
val objectMapper = ObjectMapper()
val objectMapper = jacksonObjectMapper()
val cfg = configFile()
cfg.Load()
val au = AudioUtility()
var audioID = 0
val preferedAudioDevice = "Speakers"
au.DetectPlaybackDevices().forEach { pair ->
println("Device ID: ${pair.first}, Name: ${pair.second}")
AudioUtility.DetectPlaybackDevices().forEach { pair ->
logger.info("Device ID: ${pair.first}, Name: ${pair.second}")
if (pair.second.contains(preferedAudioDevice)) {
audioID = pair.first
}
}
if (audioID!=0){
val initsuccess = au.InitDevice(audioID)
println("Audio Device $audioID initialized: $initsuccess")
val initsuccess = AudioUtility.InitDevice(audioID,44100)
logger.info("Audio Device $audioID initialized: $initsuccess")
}
// for Zello Client
val o = OpusStreamReceiver(audioID)
// for AudioFilePlayer
var afp: AudioFilePlayer? = null
/**
* Creates a ZelloClient instance based on the configuration.
*/
fun CreateZelloFromConfig(): ZelloClient {
return when (cfg.ZelloServer) {
"work" -> {
ZelloClient.fromZelloWork(
cfg.ZelloUsername ?: "",
cfg.ZelloPassword ?: "",
cfg.ZelloChannel ?: "",
cfg.ZelloWorkNetworkName ?: ""
)
}
"enterprise" -> {
ZelloClient.fromZelloEnterpriseServer(
cfg.ZelloUsername ?: "",
cfg.ZelloPassword ?: "",
cfg.ZelloChannel ?: "",
cfg.ZelloEnterpriseServerDomain ?: ""
)
}
else -> {
ZelloClient.fromConsumerZello(cfg.ZelloUsername ?: "", cfg.ZelloPassword ?: "", cfg.ZelloChannel ?: "")
}
}
}
// Create Zello client from configuration and start it
var z = CreateZelloFromConfig()
// Create ZelloEvent implementation to handle events
val z_event = object : ZelloEvent{
override fun onChannelStatus(
channel: String,
status: String,
userOnline: Int,
error: String?,
errorType: String?
) {
logger.info("Channel Status: $channel is $status with $userOnline users online.")
if (ValidString(error) && ValidString(errorType)) {
logger.info("Error: $error, Type: $errorType")
}
}
override fun onAudioData(streamID: Int, from: String, For: String, channel: String, data: ByteArray) {
logger.info("Audio Data received from $from for $For on channel $channel with streamID $streamID ")
}
override fun onThumbnailImage(imageID: Int, from: String, For: String, channel: String, data: ByteArray, timestamp: Long) {
logger.info("Thumbnail Image received from $from for $For on channel $channel with imageID $imageID at timestamp $timestamp")
}
override fun onFullImage(imageID: Int, from: String, For: String, channel: String, data: ByteArray, timestamp: Long) {
logger.info("Full Image received from $from for $For on channel $channel with imageID $imageID at timestamp $timestamp")
}
override fun onTextMessage(messageID: Int, from: String, For: String, channel: String, text: String, timestamp: Long) {
logger.info("Text Message received from $from for $For on channel $channel with messageID $messageID at timestamp $timestamp. Text: $text")
}
override fun onLocation(messageID: Int, from: String, For: String, channel: String, latitude: Double, longitude: Double, address: String, accuracy: Double, timestamp: Long) {
logger.info("Location received from $from for $For on channel $channel with messageID $messageID at timestamp $timestamp. Location($latitude,$longitude), Address:$address, Accuracy:$accuracy")
}
override fun onConnected() {
logger.info("Connected to Zello server.")
}
override fun onDisconnected(reason: String) {
logger.info("Disconnected from Zello Server, reason: $reason")
logger.info("Reconnecting after 10 seconds...")
z.Stop()
val e = this
CoroutineScope(Dispatchers.Default).launch {
delay(10000) // Wait for 10 seconds before trying to reconnect
z = CreateZelloFromConfig()
z.Start(e)
}
}
override fun onError(errorMessage: String) {
logger.info("Error occurred in Zello client: $errorMessage")
}
override fun onStartStreaming(from: String, For: String, channel: String) {
// stop any previous playback
afp?.Stop()
afp = null
if (o.Start()){
logger.info("Opus Receiver ready for streaming from $from for $For on channel $channel")
} else {
logger.info("Failed to start Opus Receiver for streaming from $from for $For on channel $channel")
}
}
override fun onStopStreaming(from: String, For: String, channel: String) {
o.Stop()
logger.info("Opus Receiver stopped streaming from $from for $For on channel $channel")
}
override fun onStreamingData(
from: String,
For: String,
channel: String,
data: ByteArray
) {
if (o.isPlaying) o.PushData(data)
}
}
z.Start(z_event)
// Start the web application with WebSocket support
val w = webApp("0.0.0.0",3030, BiFunction {
source: String, cmd: WsCommand ->
when (source) {
"setting" -> when(cmd.command){
"getConfig" ->{
logger.info("Get Config")
WsReply(cmd.command,objectMapper.writeValueAsString(cfg) )
val data = mapOf(
"zelloUsername" to cfg.ZelloUsername,
"zelloPassword" to cfg.ZelloPassword,
"zelloChannel" to cfg.ZelloChannel,
"zelloServer" to cfg.ZelloServer,
"zelloWorkNetworkName" to cfg.ZelloWorkNetworkName,
"zelloEnterpriseServerDomain" to cfg.ZelloEnterpriseServerDomain,
"m1" to cfg.M1,
"m2" to cfg.M2,
"m3" to cfg.M3,
"m4" to cfg.M4,
"m5" to cfg.M5,
"m6" to cfg.M6,
"m7" to cfg.M7,
"m8" to cfg.M8,
"messageList" to Codes.getAudioFiles()
)
WsReply(cmd.command, objectMapper.writeValueAsString(data).trim())
}
"setZelloConfig" -> {
try{
val xx = objectMapper.readValue(cmd.data, object: TypeReference<Map<String, String>>() {})
cfg.ZelloUsername = xx["ZelloUsername"]
cfg.ZelloPassword = xx["ZelloPassword"]
cfg.ZelloChannel = xx["ZelloChannel"]
cfg.ZelloServer = xx["ZelloServer"]
cfg.ZelloWorkNetworkName = xx["ZelloWorkNetworkName"]
cfg.ZelloEnterpriseServerDomain = xx["ZelloEnterpriseServerDomain"]
var changed = false
if (cfg.ZelloUsername != xx["ZelloUsername"]) {
cfg.ZelloUsername = xx["ZelloUsername"] ?: ""
changed = true
}
if (cfg.ZelloPassword != xx["ZelloPassword"]) {
cfg.ZelloPassword = xx["ZelloPassword"] ?: ""
changed = true
}
if (cfg.ZelloChannel != xx["ZelloChannel"]) {
cfg.ZelloChannel = xx["ZelloChannel"] ?: ""
changed = true
}
if (cfg.ZelloServer != xx["ZelloServer"]) {
cfg.ZelloServer = xx["ZelloServer"] ?: ""
changed = true
}
if (cfg.ZelloWorkNetworkName != xx["ZelloWorkNetworkName"]) {
cfg.ZelloWorkNetworkName = xx["ZelloWorkNetworkName"] ?: ""
changed = true
}
if (cfg.ZelloEnterpriseServerDomain != xx["ZelloEnterpriseServerDomain"]) {
cfg.ZelloEnterpriseServerDomain = xx["ZelloEnterpriseServerDomain"] ?: ""
changed = true
}
if (changed){
cfg.Save()
z.Stop()
z = CreateZelloFromConfig()
z.Start(z_event)
WsReply(cmd.command,"success")
} else WsReply(cmd.command,"No changes made")
} catch (e: Exception){
WsReply(cmd.command,"failed: ${e.message}")
}
@@ -68,15 +231,43 @@ fun main() {
try{
val xx = objectMapper.readValue(cmd.data, object : TypeReference<Map<String, String>>() {})
cfg.M1 = xx["M1"]
cfg.M2 = xx["M2"]
cfg.M3 = xx["M3"]
cfg.M4 = xx["M4"]
cfg.M5 = xx["M5"]
cfg.M6 = xx["M6"]
cfg.M7 = xx["M7"]
cfg.M8 = xx["M8"]
var changed = false
if (cfg.M1 != xx["M1"]) {
cfg.M1 = xx["M1"] ?: ""
changed = true
}
if (cfg.M2 != xx["M2"]) {
cfg.M2 = xx["M2"] ?: ""
changed = true
}
if (cfg.M3 != xx["M3"]) {
cfg.M3 = xx["M3"] ?: ""
changed = true
}
if (cfg.M4 != xx["M4"]) {
cfg.M4 = xx["M4"] ?: ""
changed = true
}
if (cfg.M5 != xx["M5"]) {
cfg.M5 = xx["M5"] ?: ""
changed = true
}
if (cfg.M6 != xx["M6"]) {
cfg.M6 = xx["M6"] ?: ""
changed = true
}
if (cfg.M7 != xx["M7"]) {
cfg.M7 = xx["M7"] ?: ""
changed = true
}
if (cfg.M8 != xx["M8"]) {
cfg.M8 = xx["M8"] ?: ""
changed = true
}
if (changed){
cfg.Save()
WsReply(cmd.command,"success")
} else WsReply(cmd.command,"No changes made")
} catch (e: Exception){
WsReply(cmd.command,"failed: ${e.message}")
}
@@ -101,12 +292,18 @@ fun main() {
}
"getPlaybackStatus" ->{
if (afp!=null && true==afp?.isPlaying){
WsReply(cmd.command, "Playing: ${afp?.filename}")
WsReply(cmd.command, "Playing: ${afp?.filename}, Duration: ${afp?.duration?.toInt()}, Elapsed: ${afp?.elapsed?.toInt()} seconds")
} else {
WsReply(cmd.command, "Idle")
}
}
"playMessage" ->{
afp?.Stop()
afp = null
// stop Opus Receiver if it is running
o.Stop()
val filename = when(cmd.data){
"M1" -> cfg.M1
"M2" -> cfg.M2
@@ -120,19 +317,22 @@ fun main() {
null
}
}
if (ValidFile(filename)){
if (filename!=null){
try{
val player= AudioFilePlayer(audioID, filename)
player.Play { _ -> afp = null}
afp = player
afp= AudioFilePlayer(audioID, filename)
afp?.Play { _ -> afp = null}
WsReply(cmd.command,"success")
} catch (e: Exception){
afp?.Stop()
afp = null
WsReply(cmd.command, "failed: ${e.message}")
}
} else WsReply(cmd.command,"Invalid file : $filename")
} else {
afp?.Stop()
afp = null
WsReply(cmd.command,"Invalid message name: ${cmd.data}")
}
}
"stopMessage" ->{
@@ -143,6 +343,16 @@ fun main() {
else -> WsReply(cmd.command,"Invalid command: ${cmd.command}")
}
"pocreceiver" -> when(cmd.command){
"getZelloStatus" -> {
var status = "Disconnected"
if (z.currentChannel?.isNotBlank() == true){
status = "Channel: ${z.currentChannel}, Online: ${z.isOnline}, Username: ${z.username}"
if (z.isReceivingStreaming){
status += ", Streaming From: ${z.receivingFrom}, Bytes Received: ${Codes.SizeToString(z.bytesReceived)}"
}
}
WsReply(cmd.command, status)
}
else -> WsReply(cmd.command,"Invalid command: ${cmd.command}")
}
else -> WsReply(cmd.command,"Invalid source: $source")
@@ -151,76 +361,11 @@ fun main() {
} , Pair("admin","admin1234"))
w.Start()
val z = ZelloClient.fromConsumerZello("gtcdevice01","GtcDev2025")
z.Start(object : ZelloEvent {
override fun onChannelStatus(
channel: String,
status: String,
userOnline: Int,
error: String?,
errorType: String?
) {
println("Channel Status: $channel is $status with $userOnline users online.")
if (ValidString(error) && ValidString(errorType)) {
println("Error: $error, Type: $errorType")
}
}
override fun onAudioData(streamID: Int, from: String, For: String, channel: String, data: ByteArray) {
println("Audio Data received from $from for $For on channel $channel with streamID $streamID ")
}
override fun onThumbnailImage(imageID: Int, from: String, For: String, channel: String, data: ByteArray, timestamp: Long) {
println("Thumbnail Image received from $from for $For on channel $channel with imageID $imageID at timestamp $timestamp")
}
override fun onFullImage(imageID: Int, from: String, For: String, channel: String, data: ByteArray, timestamp: Long) {
println("Full Image received from $from for $For on channel $channel with imageID $imageID at timestamp $timestamp")
}
override fun onTextMessage(messageID: Int, from: String, For: String, channel: String, text: String, timestamp: Long) {
println("Text Message received from $from for $For on channel $channel with messageID $messageID at timestamp $timestamp. Text: $text")
}
override fun onLocation(messageID: Int, from: String, For: String, channel: String, latitude: Double, longitude: Double, address: String, accuracy: Double, timestamp: Long) {
println("Location received from $from for $For on channel $channel with messageID $messageID at timestamp $timestamp. Location($latitude,$longitude), Address:$address, Accuracy:$accuracy")
}
override fun onConnected() {
println("Connected to Zello server.")
}
override fun onDisconnected() {
println("Disconnected from Zello server.")
}
override fun onError(errorMessage: String) {
println("Error occurred in Zello client: $errorMessage")
}
override fun onStartStreaming(from: String, For: String, channel: String) {
if (o.Start()){
println("Opus Receiver ready for streaming from $from for $For on channel $channel")
} else {
println("Failed to start Opus Receiver for streaming from $from for $For on channel $channel")
}
}
override fun onStopStreaming(from: String, For: String, channel: String) {
o.Stop()
println("Opus Receiver stopped streaming from $from for $For on channel $channel")
}
override fun onStreamingData(
from: String,
For: String,
channel: String,
data: ByteArray
) {
if (o.isPlaying) o.PushData(data)
}
})
}

View File

@@ -4,71 +4,67 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import somecodes.Codes
import java.util.function.Consumer
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists
/**
* Audio Player for playing audio files.
* Supported extensions : .wav, .mp3
*/
@Suppress("unused")
class AudioFilePlayer(deviceID: Int, val filename: String?, device_samplingrate: Int = 48000) {
val bass: Bass = Bass.Instance
var filehandle = 0
class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate: Int = 48000) {
private val bass: Bass = Bass.Instance
private var filehandle = 0
var isPlaying = false
val fileSize: Long
val duration: Double
var elapsed: Double = 0.0
init{
if (bass.BASS_SetDevice(deviceID)){
filehandle = bass.BASS_StreamCreateFile(false, filename, 0, 0, 0)
if (filehandle == 0) {
throw Exception("Failed to create stream for file $filename: ${bass.BASS_ErrorGetCode()}")
}
} else throw Exception("Failed to set device $deviceID")
val fullpath = Codes.audioFilePath.resolve(filename)
if (fullpath.exists()){
if (AudioUtility.InitDevice(deviceID, device_samplingrate)) {
filehandle = bass.BASS_StreamCreateFile(false, fullpath.absolutePathString(), 0, 0, 0)
if (filehandle!=0){
fileSize = bass.BASS_ChannelGetLength(filehandle, Bass.BASS_POS_BYTE)
duration = bass.BASS_ChannelBytes2Seconds(filehandle, fileSize)
} else throw Exception("Failed to create stream for file $filename: ${bass.BASS_ErrorGetCode()}")
} else throw Exception("Error initializing device $deviceID with sampling rate $device_samplingrate, code ${bass.BASS_ErrorGetCode()}")
} else throw Exception("File $filename does not exists")
}
fun Stop(){
if (filehandle!=0){
bass.BASS_ChannelStop(filehandle)
bass.BASS_StreamFree(filehandle)
bass.BASS_ChannelFree(filehandle)
filehandle = 0
}
isPlaying = false
AudioUtility.Free()
}
fun Play(finished: Consumer<Any> ) : Boolean{
if (bass.BASS_ChannelPlay(filehandle, false)){
elapsed = 0.0
CoroutineScope(Dispatchers.Default).launch {
isPlaying = true
while(true){
delay(1000)
delay(50)
if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){
// finished playing
break
}
elapsed = bass.BASS_ChannelBytes2Seconds(filehandle, bass.BASS_ChannelGetPosition(filehandle, Bass.BASS_POS_BYTE))
}
isPlaying = false
bass.BASS_StreamFree(filehandle)
bass.BASS_ChannelFree(filehandle)
filehandle = 0
finished.accept(true)
}
// Revisi 06/08/2025 Ganti thread dengan Coroutine
// val thread = Thread{
// isPlaying = true
// while(true){
// Thread.sleep(1000)
//
// if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){
// // finished playing
// break
// }
// }
// isPlaying = false
// bass.BASS_StreamFree(filehandle)
// filehandle = 0
// finished.accept(true)
// }
// thread.name = "AudioFilePlayer $filename"
// thread.isDaemon = true
// thread.start()
return true
}
return false

View File

@@ -3,12 +3,13 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
class AudioUtility {
private val bass = Bass.Instance
private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java)
init{
logger.info("Bass Version = ${bass.BASS_GetVersion().toHexString()}")
}
companion object{
private val bass = Bass.Instance
fun DetectPlaybackDevices() : List<Pair<Int, String>> {
val result = ArrayList<Pair<Int, String>>()
@@ -38,4 +39,25 @@ class AudioUtility {
return false // gagal GetDeviceInfo
}
/**
* Check if the device is having some handles opened.
* @param deviceID the device ID to check
* @return true if the device is opening something, false otherwise
*/
fun DeviceIsOpeningSomething(deviceID: Int) : Boolean {
return bass.BASS_GetConfig(Bass.BASS_CONFIG_HANDLES) > 0
}
fun Free(){
bass.BASS_Free()
}
}
init{
logger.info("Bass Version = ${bass.BASS_GetVersion().toHexString()}")
}
}

View File

@@ -12,7 +12,7 @@ import org.slf4j.LoggerFactory
* @throws Exception if the device cannot be set.
*/
@Suppress("unused")
class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
class OpusStreamReceiver(val deviceID: Int, val samplingrate: Int = 16000) {
private val bass = Bass.Instance
private val bassopus = BASSOPUS.Instance
private var filehandle = 0
@@ -20,17 +20,15 @@ class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
var isPlaying = false
init{
if (!bass.BASS_SetDevice(deviceID)){
throw Exception("Failed to set device $deviceID")
}
}
/**
* Starts the Opus stream playback.
* @return true if the stream started successfully, false otherwise.
*/
fun Start() : Boolean{
if (AudioUtility.InitDevice(deviceID, samplingrate)){
val opushead = BASSOPUS.BASS_OPUS_HEAD()
opushead.version = 1
opushead.channels = 1
@@ -43,6 +41,8 @@ class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
return true
} else logger.error("BASS_ChannelPlay failed for filehandle $filehandle, code ${bass.BASS_ErrorGetCode()}")
} else logger.error("BASS_OPUS_StreamCreate failed, code ${bass.BASS_ErrorGetCode()}")
} else logger.error("Error initializing device $deviceID with sampling rate $samplingrate, code ${bass.BASS_ErrorGetCode()}")
return false
}
@@ -51,11 +51,11 @@ class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
*/
fun Stop(){
if (filehandle!=0){
bass.BASS_ChannelStop(filehandle)
bass.BASS_StreamFree(filehandle)
bass.BASS_ChannelFree(filehandle)
filehandle = 0
isPlaying = false
}
isPlaying = false
AudioUtility.Free()
}
/**

View File

@@ -1,12 +1,42 @@
package somecodes
import com.fasterxml.jackson.databind.ObjectMapper
import java.io.File
import kotlin.io.path.Path
@Suppress("unused")
class Codes {
private val objectMapper = ObjectMapper()
companion object{
val audioFilePath = Path(System.getProperty("user.dir"), "audiofile")
private val validAudioExtensions = setOf("wav", "mp3")
private val KB_size = 1024 // 1 KB = 1024 bytes
private val MB_size = 1024 * KB_size // 1 MB = 1024 KB
private val GB_size = 1024 * MB_size // 1 GB = 1024 MB
fun SizeToString(size: Long) : String {
return when {
size >= GB_size -> String.format("%.2f GB", size.toDouble() / GB_size)
size >= MB_size -> String.format("%.2f MB", size.toDouble() / MB_size)
size >= KB_size -> String.format("%.2f KB", size.toDouble() / KB_size)
else -> "$size bytes"
}
}
fun getAudioFiles() : Array<String> {
val audioDir = audioFilePath.toFile()
if (!audioDir.exists()) {
audioDir.mkdirs() // Create directory if it doesn't exist
}
val ll = audioDir.listFiles()?.filter { it.isFile && validAudioExtensions.contains(it.extension) }?.map { it.name } ?: emptyList()
return ll.toTypedArray()
}
fun getaudioFileFullPath(filename: String) : String {
if (ValidString(filename)) {
val file = audioFilePath.resolve(filename)
return file.toAbsolutePath().toString()
}
return ""
}
fun ValidFile(s: String?) : Boolean {
if (s!=null){

View File

@@ -1,5 +1,6 @@
package somecodes
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.nio.file.Path
import kotlin.io.path.Path
@@ -21,15 +22,14 @@ class configFile {
private val filepath : Path = Path(System.getProperty("user.dir"), "config.json")
@Suppress("UNCHECKED_CAST")
fun Load(){
if (filepath.toFile().exists()){
// file found, then load the configuration to configFile object
try{
val json = filepath.toFile().readText()
val configMap = json.split(",").associate { it ->
val (key, value) = it.split(":").map { it.trim().removeSurrounding("\"") }
key to value
}
val objectMapper = jacksonObjectMapper()
val configMap = objectMapper.readValue(json, Map::class.java) as Map<String, String>
ZelloUsername = configMap["ZelloUsername"]
ZelloPassword = configMap["ZelloPassword"]
@@ -72,23 +72,25 @@ class configFile {
fun Save(){
try {
// Convert the configFile object to JSON and write it to the file
val json = """{
"ZelloUsername": "$ZelloUsername",
"ZelloPassword": "$ZelloPassword",
"ZelloChannel": "$ZelloChannel",
"ZelloServer": "$ZelloServer",
"ZelloWorkNetworkName": "$ZelloWorkNetworkName",
"ZelloEnterpriseServerDomain": "$ZelloEnterpriseServerDomain",
"M1": "$M1",
"M2": "$M2",
"M3": "$M3",
"M4": "$M4",
"M5": "$M5",
"M6": "$M6",
"M7": "$M7",
"M8": "$M8"
}"""
filepath.toFile().writeText(json)
val js = mapOf(
"ZelloUsername" to ZelloUsername,
"ZelloPassword" to ZelloPassword,
"ZelloChannel" to ZelloChannel,
"ZelloServer" to ZelloServer,
"ZelloWorkNetworkName" to ZelloWorkNetworkName,
"ZelloEnterpriseServerDomain" to ZelloEnterpriseServerDomain,
"M1" to M1,
"M2" to M2,
"M3" to M3,
"M4" to M4,
"M5" to M5,
"M6" to M6,
"M7" to M7,
"M8" to M8
)
val mapper = jacksonObjectMapper()
mapper.writerWithDefaultPrettyPrinter().writeValue(filepath.toFile(), js)
} catch (e: Exception) {
println("Error saving configuration: ${e.message}")
}

View File

@@ -2,10 +2,15 @@ package web;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.javalin.Javalin;
import io.javalin.http.HttpStatus;
import javafx.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import somecodes.Codes;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
@@ -66,7 +71,7 @@ public class webApp {
String message = wsMessageContext.message();
try{
var command = objectMapper.readValue(message, WsCommand.class);
logger.info("Received command from pocreceiver.html/ws : {}", command);
//logger.info("Received command from pocreceiver.html/ws : {}", command);
var reply = callback.apply("pocreceiver", command);
wsMessageContext.send(reply);
} catch (Exception e){
@@ -85,7 +90,7 @@ public class webApp {
String message = wsMessageContext.message();
try{
var command = objectMapper.readValue(message, WsCommand.class);
logger.info("Received command from prerecordedbroadcast.html/ws : {}", command);
//logger.info("Received command from prerecordedbroadcast.html/ws : {}", command);
var reply = callback.apply("prerecordedbroadcast", command);
wsMessageContext.send(reply);
} catch (Exception e){
@@ -99,14 +104,33 @@ public class webApp {
ctx.redirect("/index.html");
}
});
post("/upload", ctx -> {
// Handle file upload
var file = ctx.uploadedFile("file");
if (file != null ) {
// Process the uploaded file
try(InputStream in = file.content()) {
var savetarget = Codes.Companion.getAudioFilePath().resolve(file.filename());
Files.copy(in, savetarget, StandardCopyOption.REPLACE_EXISTING);
logger.info("File uploaded: {}", file.filename());
ctx.status(HttpStatus.OK).result("File uploaded successfully: " + file.filename());
} catch (Exception e){
ctx.status(HttpStatus.INTERNAL_SERVER_ERROR).result("File upload failed: " + file.filename());
}
} else {
ctx.status(HttpStatus.BAD_REQUEST).result("No file uploaded");
}
});
ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> {
// Handle incoming WebSocket messages
String message = wsMessageContext.message();
//logger.info("Received message from setting.html/ws: {}", message);
try{
var command = objectMapper.readValue(message, WsCommand.class);
logger.info("Received command from setting.html/ws : {}", command);
//logger.info("Received command from setting.html/ws : {}", command);
var reply = callback.apply("setting", command);
logger.info("Replying to setting.html/ws : {}", reply);
//logger.info("Replying to setting.html/ws : {}", reply);
wsMessageContext.send(reply);
} catch (Exception e){
logger.error("Error processing {} from setting.html/ws: {}", message, e.getMessage());

View File

@@ -2,10 +2,6 @@ package zello
import com.fasterxml.jackson.module.kotlin.contains
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.java_websocket.client.WebSocketClient
import org.java_websocket.handshake.ServerHandshake
import org.slf4j.Logger
@@ -20,23 +16,27 @@ import java.util.function.BiConsumer
/**
* ZelloClient is a WebSocket client for connecting to Zello services.
* [Source](https://github.com/zelloptt/zello-channel-api/blob/master/API.md)
* @param address the WebSocket address of the Zello server, e.g. "wss://zello.io/ws"
* @param username the username for Zello authentication
* @param password the password for Zello authentication
* @param channel the default channel to join after connecting
*
*/
class ZelloClient(val address : URI, val username: String, val password: String) {
class ZelloClient(val address : URI, val username: String, val password: String, val channel: String="GtcDev2025") {
private val streamJob = HashMap<Int, ZelloAudioJob>()
private val imageJob = HashMap<Int, ZelloImageJob>()
private val commandJob = HashMap<Int, Any>()
companion object {
fun fromConsumerZello(username : String, password: String) : ZelloClient {
return ZelloClient(URI.create("wss://zello.io/ws"), username, password)
fun fromConsumerZello(username : String, password: String, channel: String) : ZelloClient {
return ZelloClient(URI.create("wss://zello.io/ws"), username, password, channel)
}
fun fromZelloWork(username: String, password: String, networkName : String) : ZelloClient{
return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"), username, password)
fun fromZelloWork(username: String, password: String, channel: String, networkName : String) : ZelloClient{
return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"), username, password, channel)
}
fun fromZelloEnterpriseServer(username: String, password: String, serverDomain: String) : ZelloClient{
return ZelloClient(URI.create("wss://$serverDomain/ws/mesh"), username, password)
fun fromZelloEnterpriseServer(username: String, password: String, channel: String, serverDomain: String) : ZelloClient{
return ZelloClient(URI.create("wss://$serverDomain/ws/mesh"), username, password, channel)
}
}
@@ -45,21 +45,29 @@ class ZelloClient(val address : URI, val username: String, val password: String)
// this key is temporary, valid only 30 days from 2025-07-29
// if need to create, from https://developers.zello.com/keys
private val developerKey : String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJXa002Y21ScllYSjBiMjV2T2pFLi1yYjJ2THFRbUhYV3dKY2I2azl2TDdUMEtzRWZMRjcxZm5jcktTZ0s2ZE0iLCJleHAiOjE3NTY0MzIyMTIsImF6cCI6ImRldiJ9.ANK7BIS6WVVWsQRjcZXyGWrV2RodCUQD4WXWaA6E4Dlyy8bBCMFdbiKN2D7B_x729HQULailnfRhbXF4Avfg14qONdc1XE_0iGiPUO1kfUSgdd11QylOzjxy6FTKSeZmHOh65JZq2dIWxobCcva-RPvbR8TA656upHh32xrWv9zlU0N707FTca04kze0Iq-q-uC5EL82yK10FEvOPDX88MYy71QRYi8Qh_KbSyMcYAhe2bTsiyjm51ZH9ntkRHd0HNiaijNZI6-qXkkp5Soqmzh-bTtbbgmbX4BT3Qpz_IP3epaX3jl_Aq5DHxXwCsJ9FThif9um5D0TWVGQteR0cQ"
// default channel to join
private var channels = arrayOf("GtcDev2025")
// refresh token for the session
// this is set after the first LogonReply
private var refresh_token: String? = null
private val logger : Logger = LoggerFactory.getLogger(ZelloClient::class.java)
private val mapper = jacksonObjectMapper()
var currentChannel: String? = null ; private set
var isOnline: Boolean = false ; private set
var isReceivingStreaming: Boolean = false
var receivingFrom: String? = null; private set
var bytesReceived: Long = 0; private set
fun Start(event: ZelloEvent){
client = object : WebSocketClient(address) {
override fun onOpen(handshakedata: ServerHandshake?) {
//logger.info("Connected to $address")
isOnline = false
currentChannel = null
isReceivingStreaming = false
seqID = 0
inc_seqID()
val lg = LogonCommand.create(seqID,channels, developerKey, username, password)
val lg = LogonCommand.create(seqID,arrayOf(channel), developerKey, username, password)
val value = mapper.writeValueAsString(lg)
send(value)
}
@@ -82,6 +90,7 @@ class ZelloClient(val address : URI, val username: String, val password: String)
job.pushAudioData(data)
event.onStreamingData(job.from, job.For?:"", job.channel, data)
}
bytesReceived += data.size
}
0x02.toByte() ->{
@@ -130,7 +139,7 @@ class ZelloClient(val address : URI, val username: String, val password: String)
refresh_token = lgreply.refresh_token
event.onConnected()
} else {
logger.error("Failed to logon: ${lgreply.error ?: "Unknown error"}")
event.onError("Failed to logon: ${lgreply.error ?: "Unknown error"}")
}
}
else ->{
@@ -163,6 +172,8 @@ class ZelloClient(val address : URI, val username: String, val password: String)
"on_channel_status" -> {
val channelstatus = mapper.treeToValue(jsnode, Event_OnChannelStatus::class.java)
event.onChannelStatus(channelstatus.channel, channelstatus.status, channelstatus.users_online, channelstatus.error, channelstatus.error_type)
currentChannel = channelstatus.channel
isOnline = channelstatus.status== "online"
}
"on_error" -> {
val error = mapper.treeToValue(jsnode, Event_OnError::class.java)
@@ -173,6 +184,9 @@ class ZelloClient(val address : URI, val username: String, val password: String)
logger.info("Stream started on channel ${streamstart.channel} from ${streamstart.from} for ${streamstart.For}")
streamJob[streamstart.stream_id] = ZelloAudioJob.fromEventOnStreamStart(streamstart)
event.onStartStreaming(streamstart.from, streamstart.For, streamstart.channel)
isReceivingStreaming = true
receivingFrom = streamstart.from
bytesReceived = 0 // reset bytes received
}
"on_stream_stop" -> {
val streamstop = mapper.treeToValue(jsnode, Event_OnStreamStop::class.java)
@@ -184,6 +198,8 @@ class ZelloClient(val address : URI, val username: String, val password: String)
event.onStopStreaming(job.from, job.For?:"", job.channel)
event.onAudioData(job.streamID, job.from, job.For?:"", job.channel, job.getAudioData())
}
isReceivingStreaming = false
receivingFrom = null
}
"on_image" ->{
val image = mapper.treeToValue(jsnode, Event_OnImage::class.java)
@@ -209,12 +225,10 @@ class ZelloClient(val address : URI, val username: String, val password: String)
override fun onClose(code: Int, reason: String?, remote: Boolean) {
logger.info("Closed from ${if (remote) "Remote side" else "Local side"}, Code=$code, Reason=$reason")
event.onDisconnected()
// try reconnecting after 10 seconds
CoroutineScope(Dispatchers.Default).launch {
delay(10000)
connect()
}
event.onDisconnected(reason?: "Unknown reason")
isOnline = false
currentChannel = null
//Revisi 06/08/2025 : Change to Coroutines
// val thread = Thread {

View File

@@ -9,7 +9,7 @@ interface ZelloEvent {
fun onTextMessage(messageID: Int, from: String, For: String, channel: String, text: String, timestamp: Long)
fun onLocation(messageID: Int, from: String, For: String, channel: String, latitude: Double, longitude: Double, address:String, accuracy: Double, timestamp: Long )
fun onConnected()
fun onDisconnected()
fun onDisconnected(reason: String)
fun onError(errorMessage: String)
fun onStartStreaming(from: String, For: String, channel: String)
fun onStopStreaming(from: String, For: String, channel: String)