Commit 07/08/2025
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
### IntelliJ IDEA ###
|
### IntelliJ IDEA ###
|
||||||
out/
|
out/
|
||||||
|
audiofile/
|
||||||
!**/src/main/**/out/
|
!**/src/main/**/out/
|
||||||
!**/src/test/**/out/
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/html" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/html" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/audiofile" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Your code here
|
// Your code here
|
||||||
console.log('pocreceiver.js is ready!');
|
//console.log('pocreceiver.js is ready!');
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
|
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
|
||||||
|
|
||||||
ws.onopen = function() {
|
ws.onopen = function() {
|
||||||
console.log('WebSocket connection opened');
|
//console.log('WebSocket connection opened');
|
||||||
$('#indicatorDisconnected').addClass('visually-hidden');
|
$('#indicatorDisconnected').addClass('visually-hidden');
|
||||||
$('#indicatorConnected').removeClass('visually-hidden');
|
$('#indicatorConnected').removeClass('visually-hidden');
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
sendCommand({ command: "getZelloStatus" });
|
sendCommand({ command: "getZelloStatus" });
|
||||||
}, 5000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = function(event) {
|
ws.onmessage = function(event) {
|
||||||
console.log('WebSocket message received:', event.data);
|
//console.log('WebSocket message received:', event.data);
|
||||||
let msg = {};
|
let msg = {};
|
||||||
try {
|
try {
|
||||||
msg = JSON.parse(event.data);
|
msg = JSON.parse(event.data);
|
||||||
@@ -25,13 +25,13 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
if (msg.reply === "getZelloStatus" && msg.data !== undefined && msg.data.length > 0) {
|
if (msg.reply === "getZelloStatus" && msg.data !== undefined && msg.data.length > 0) {
|
||||||
const zelloData = msg.data;
|
const zelloData = msg.data;
|
||||||
console.log('Zello Status Data:', zelloData);
|
//console.log('Zello Status Data:', zelloData);
|
||||||
$('#zelloStatus').text(zelloData.status);
|
$('#zelloStatus').text(zelloData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = function() {
|
ws.onclose = function() {
|
||||||
console.log('WebSocket connection closed');
|
//console.log('WebSocket connection closed');
|
||||||
$('#indicatorDisconnected').removeClass('visually-hidden');
|
$('#indicatorDisconnected').removeClass('visually-hidden');
|
||||||
$('#indicatorConnected').addClass('visually-hidden');
|
$('#indicatorConnected').addClass('visually-hidden');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<h1 class="text-center">Zello Status</h1>
|
<h1 class="text-center">Zello Status</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p id="zelloStatus">Paragraph</p>
|
<p class="d-flex justify-content-center" id="zelloStatus">Paragraph</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
|||||||
339
src/Main.kt
339
src/Main.kt
@@ -10,12 +10,14 @@ import web.webApp
|
|||||||
import zello.ZelloClient
|
import zello.ZelloClient
|
||||||
import zello.ZelloEvent
|
import zello.ZelloEvent
|
||||||
import javafx.util.Pair
|
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 org.slf4j.LoggerFactory
|
||||||
import somecodes.Codes
|
import somecodes.Codes
|
||||||
import web.WsCommand
|
import web.WsCommand
|
||||||
import java.util.function.BiFunction
|
import java.util.function.BiFunction
|
||||||
import kotlin.io.path.isRegularFile
|
|
||||||
import kotlin.io.path.pathString
|
|
||||||
|
|
||||||
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
|
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
|
||||||
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
|
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
|
||||||
@@ -25,23 +27,140 @@ fun main() {
|
|||||||
|
|
||||||
val cfg = configFile()
|
val cfg = configFile()
|
||||||
cfg.Load()
|
cfg.Load()
|
||||||
val au = AudioUtility()
|
|
||||||
var audioID = 0
|
var audioID = 0
|
||||||
val preferedAudioDevice = "Speakers"
|
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)) {
|
if (pair.second.contains(preferedAudioDevice)) {
|
||||||
audioID = pair.first
|
audioID = pair.first
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (audioID!=0){
|
if (audioID!=0){
|
||||||
val initsuccess = au.InitDevice(audioID,44100)
|
val initsuccess = AudioUtility.InitDevice(audioID,44100)
|
||||||
println("Audio Device $audioID initialized: $initsuccess")
|
logger.info("Audio Device $audioID initialized: $initsuccess")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for Zello Client
|
||||||
val o = OpusStreamReceiver(audioID)
|
val o = OpusStreamReceiver(audioID)
|
||||||
|
// for AudioFilePlayer
|
||||||
var afp: AudioFilePlayer? = null
|
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 {
|
val w = webApp("0.0.0.0",3030, BiFunction {
|
||||||
source: String, cmd: WsCommand ->
|
source: String, cmd: WsCommand ->
|
||||||
when (source) {
|
when (source) {
|
||||||
@@ -70,14 +189,39 @@ fun main() {
|
|||||||
"setZelloConfig" -> {
|
"setZelloConfig" -> {
|
||||||
try{
|
try{
|
||||||
val xx = objectMapper.readValue(cmd.data, object: TypeReference<Map<String, String>>() {})
|
val xx = objectMapper.readValue(cmd.data, object: TypeReference<Map<String, String>>() {})
|
||||||
cfg.ZelloUsername = xx["ZelloUsername"]
|
var changed = false
|
||||||
cfg.ZelloPassword = xx["ZelloPassword"]
|
if (cfg.ZelloUsername != xx["ZelloUsername"]) {
|
||||||
cfg.ZelloChannel = xx["ZelloChannel"]
|
cfg.ZelloUsername = xx["ZelloUsername"] ?: ""
|
||||||
cfg.ZelloServer = xx["ZelloServer"]
|
changed = true
|
||||||
cfg.ZelloWorkNetworkName = xx["ZelloWorkNetworkName"]
|
}
|
||||||
cfg.ZelloEnterpriseServerDomain = xx["ZelloEnterpriseServerDomain"]
|
if (cfg.ZelloPassword != xx["ZelloPassword"]) {
|
||||||
cfg.Save()
|
cfg.ZelloPassword = xx["ZelloPassword"] ?: ""
|
||||||
WsReply(cmd.command,"success")
|
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){
|
} catch (e: Exception){
|
||||||
WsReply(cmd.command,"failed: ${e.message}")
|
WsReply(cmd.command,"failed: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -87,16 +231,43 @@ fun main() {
|
|||||||
try{
|
try{
|
||||||
|
|
||||||
val xx = objectMapper.readValue(cmd.data, object : TypeReference<Map<String, String>>() {})
|
val xx = objectMapper.readValue(cmd.data, object : TypeReference<Map<String, String>>() {})
|
||||||
cfg.M1 = xx["M1"]
|
var changed = false
|
||||||
cfg.M2 = xx["M2"]
|
if (cfg.M1 != xx["M1"]) {
|
||||||
cfg.M3 = xx["M3"]
|
cfg.M1 = xx["M1"] ?: ""
|
||||||
cfg.M4 = xx["M4"]
|
changed = true
|
||||||
cfg.M5 = xx["M5"]
|
}
|
||||||
cfg.M6 = xx["M6"]
|
if (cfg.M2 != xx["M2"]) {
|
||||||
cfg.M7 = xx["M7"]
|
cfg.M2 = xx["M2"] ?: ""
|
||||||
cfg.M8 = xx["M8"]
|
changed = true
|
||||||
cfg.Save()
|
}
|
||||||
WsReply(cmd.command,"success")
|
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){
|
} catch (e: Exception){
|
||||||
WsReply(cmd.command,"failed: ${e.message}")
|
WsReply(cmd.command,"failed: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -121,12 +292,18 @@ fun main() {
|
|||||||
}
|
}
|
||||||
"getPlaybackStatus" ->{
|
"getPlaybackStatus" ->{
|
||||||
if (afp!=null && true==afp?.isPlaying){
|
if (afp!=null && true==afp?.isPlaying){
|
||||||
WsReply(cmd.command, "Playing: ${afp?.ShortFileName()}")
|
WsReply(cmd.command, "Playing: ${afp?.filename}, Duration: ${afp?.duration?.toInt()}, Elapsed: ${afp?.elapsed?.toInt()} seconds")
|
||||||
} else {
|
} else {
|
||||||
WsReply(cmd.command, "Idle")
|
WsReply(cmd.command, "Idle")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"playMessage" ->{
|
"playMessage" ->{
|
||||||
|
|
||||||
|
afp?.Stop()
|
||||||
|
afp = null
|
||||||
|
// stop Opus Receiver if it is running
|
||||||
|
o.Stop()
|
||||||
|
|
||||||
val filename = when(cmd.data){
|
val filename = when(cmd.data){
|
||||||
"M1" -> cfg.M1
|
"M1" -> cfg.M1
|
||||||
"M2" -> cfg.M2
|
"M2" -> cfg.M2
|
||||||
@@ -141,24 +318,21 @@ fun main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filename!=null){
|
if (filename!=null){
|
||||||
val completefilename = Codes.audioFilePath.resolve(filename)
|
try{
|
||||||
if (completefilename.isRegularFile()){
|
afp= AudioFilePlayer(audioID, filename)
|
||||||
try{
|
afp?.Play { _ -> afp = null}
|
||||||
|
WsReply(cmd.command,"success")
|
||||||
val player= AudioFilePlayer(audioID, completefilename.pathString)
|
|
||||||
player.Play { _ -> afp = null}
|
|
||||||
afp = player
|
|
||||||
WsReply(cmd.command,"success")
|
|
||||||
|
|
||||||
} catch (e: Exception){
|
|
||||||
WsReply(cmd.command, "failed: ${e.message}")
|
|
||||||
}
|
|
||||||
} else WsReply(cmd.command,"File Not Found : $filename")
|
|
||||||
} else WsReply(cmd.command,"Invalid message name: ${cmd.data}")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
} catch (e: Exception){
|
||||||
|
afp?.Stop()
|
||||||
|
afp = null
|
||||||
|
WsReply(cmd.command, "failed: ${e.message}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
afp?.Stop()
|
||||||
|
afp = null
|
||||||
|
WsReply(cmd.command,"Invalid message name: ${cmd.data}")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
"stopMessage" ->{
|
"stopMessage" ->{
|
||||||
@@ -169,6 +343,16 @@ fun main() {
|
|||||||
else -> WsReply(cmd.command,"Invalid command: ${cmd.command}")
|
else -> WsReply(cmd.command,"Invalid command: ${cmd.command}")
|
||||||
}
|
}
|
||||||
"pocreceiver" -> when(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 command: ${cmd.command}")
|
||||||
}
|
}
|
||||||
else -> WsReply(cmd.command,"Invalid source: $source")
|
else -> WsReply(cmd.command,"Invalid source: $source")
|
||||||
@@ -177,76 +361,11 @@ fun main() {
|
|||||||
} , Pair("admin","admin1234"))
|
} , Pair("admin","admin1234"))
|
||||||
w.Start()
|
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,77 +4,67 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import somecodes.Codes
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audio Player for playing audio files.
|
* Audio Player for playing audio files.
|
||||||
* Supported extensions : .wav, .mp3
|
* Supported extensions : .wav, .mp3
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class AudioFilePlayer(deviceID: Int, filename: String, device_samplingrate: Int = 48000) {
|
class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate: Int = 48000) {
|
||||||
val bass: Bass = Bass.Instance
|
private val bass: Bass = Bass.Instance
|
||||||
var filehandle = 0
|
private var filehandle = 0
|
||||||
var isPlaying = false
|
var isPlaying = false
|
||||||
private val filepath = Path(filename)
|
val fileSize: Long
|
||||||
|
val duration: Double
|
||||||
|
var elapsed: Double = 0.0
|
||||||
init{
|
init{
|
||||||
if (bass.BASS_SetDevice(deviceID)){
|
val fullpath = Codes.audioFilePath.resolve(filename)
|
||||||
filehandle = bass.BASS_StreamCreateFile(false, filename, 0, 0, 0)
|
if (fullpath.exists()){
|
||||||
if (filehandle == 0) {
|
if (AudioUtility.InitDevice(deviceID, device_samplingrate)) {
|
||||||
throw Exception("Failed to create stream for file $filename: ${bass.BASS_ErrorGetCode()}")
|
filehandle = bass.BASS_StreamCreateFile(false, fullpath.absolutePathString(), 0, 0, 0)
|
||||||
}
|
if (filehandle!=0){
|
||||||
} else throw Exception("Failed to set device $deviceID")
|
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(){
|
fun Stop(){
|
||||||
if (filehandle!=0){
|
if (filehandle!=0){
|
||||||
bass.BASS_ChannelStop(filehandle)
|
bass.BASS_ChannelFree(filehandle)
|
||||||
bass.BASS_StreamFree(filehandle)
|
|
||||||
filehandle = 0
|
filehandle = 0
|
||||||
}
|
}
|
||||||
}
|
isPlaying = false
|
||||||
|
AudioUtility.Free()
|
||||||
fun ShortFileName() : String {
|
|
||||||
return filepath.fileName.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Play(finished: Consumer<Any> ) : Boolean{
|
fun Play(finished: Consumer<Any> ) : Boolean{
|
||||||
if (bass.BASS_ChannelPlay(filehandle, false)){
|
if (bass.BASS_ChannelPlay(filehandle, false)){
|
||||||
|
elapsed = 0.0
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
while(true){
|
while(true){
|
||||||
delay(1000)
|
delay(50)
|
||||||
|
|
||||||
if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){
|
if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){
|
||||||
// finished playing
|
// finished playing
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
elapsed = bass.BASS_ChannelBytes2Seconds(filehandle, bass.BASS_ChannelGetPosition(filehandle, Bass.BASS_POS_BYTE))
|
||||||
}
|
}
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
bass.BASS_StreamFree(filehandle)
|
bass.BASS_ChannelFree(filehandle)
|
||||||
filehandle = 0
|
filehandle = 0
|
||||||
finished.accept(true)
|
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 true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -3,39 +3,61 @@ import org.slf4j.Logger
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
class AudioUtility {
|
class AudioUtility {
|
||||||
private val bass = Bass.Instance
|
|
||||||
private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java)
|
private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
companion object{
|
||||||
|
private val bass = Bass.Instance
|
||||||
|
|
||||||
|
fun DetectPlaybackDevices() : List<Pair<Int, String>> {
|
||||||
|
val result = ArrayList<Pair<Int, String>>()
|
||||||
|
for(i in 0..10){
|
||||||
|
val dev = Bass.BASS_DEVICEINFO()
|
||||||
|
if (bass.BASS_GetDeviceInfo(i, dev)){
|
||||||
|
if (dev.flags and Bass.BASS_DEVICE_ENABLED != 0){
|
||||||
|
result.add(Pair(i, dev.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun InitDevice(deviceID: Int, device_samplingrate: Int = 48000) : Boolean {
|
||||||
|
val dev = Bass.BASS_DEVICEINFO()
|
||||||
|
if (bass.BASS_GetDeviceInfo(deviceID, dev)) {
|
||||||
|
if (dev.flags and Bass.BASS_DEVICE_ENABLED > 0){
|
||||||
|
if (dev.flags and Bass.BASS_DEVICE_INIT > 0){
|
||||||
|
return true // sudah init
|
||||||
|
} else {
|
||||||
|
val initflag = Bass.BASS_DEVICE_16BITS or Bass.BASS_DEVICE_MONO
|
||||||
|
return bass.BASS_Init(deviceID, device_samplingrate,initflag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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{
|
init{
|
||||||
logger.info("Bass Version = ${bass.BASS_GetVersion().toHexString()}")
|
logger.info("Bass Version = ${bass.BASS_GetVersion().toHexString()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DetectPlaybackDevices() : List<Pair<Int, String>> {
|
|
||||||
val result = ArrayList<Pair<Int, String>>()
|
|
||||||
for(i in 0..10){
|
|
||||||
val dev = Bass.BASS_DEVICEINFO()
|
|
||||||
if (bass.BASS_GetDeviceInfo(i, dev)){
|
|
||||||
if (dev.flags and Bass.BASS_DEVICE_ENABLED != 0){
|
|
||||||
result.add(Pair(i, dev.name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun InitDevice(deviceID: Int, device_samplingrate: Int = 48000) : Boolean {
|
|
||||||
val dev = Bass.BASS_DEVICEINFO()
|
|
||||||
if (bass.BASS_GetDeviceInfo(deviceID, dev)) {
|
|
||||||
if (dev.flags and Bass.BASS_DEVICE_ENABLED > 0){
|
|
||||||
if (dev.flags and Bass.BASS_DEVICE_INIT > 0){
|
|
||||||
return true // sudah init
|
|
||||||
} else {
|
|
||||||
val initflag = Bass.BASS_DEVICE_16BITS or Bass.BASS_DEVICE_MONO
|
|
||||||
return bass.BASS_Init(deviceID, device_samplingrate,initflag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false // gagal GetDeviceInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ import org.slf4j.LoggerFactory
|
|||||||
* @throws Exception if the device cannot be set.
|
* @throws Exception if the device cannot be set.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@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 bass = Bass.Instance
|
||||||
private val bassopus = BASSOPUS.Instance
|
private val bassopus = BASSOPUS.Instance
|
||||||
private var filehandle = 0
|
private var filehandle = 0
|
||||||
@@ -20,29 +20,29 @@ class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
|
|||||||
var isPlaying = false
|
var isPlaying = false
|
||||||
|
|
||||||
|
|
||||||
init{
|
|
||||||
if (!bass.BASS_SetDevice(deviceID)){
|
|
||||||
throw Exception("Failed to set device $deviceID")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the Opus stream playback.
|
* Starts the Opus stream playback.
|
||||||
* @return true if the stream started successfully, false otherwise.
|
* @return true if the stream started successfully, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun Start() : Boolean{
|
fun Start() : Boolean{
|
||||||
val opushead = BASSOPUS.BASS_OPUS_HEAD()
|
|
||||||
opushead.version = 1
|
if (AudioUtility.InitDevice(deviceID, samplingrate)){
|
||||||
opushead.channels = 1
|
val opushead = BASSOPUS.BASS_OPUS_HEAD()
|
||||||
opushead.inputrate = samplingrate
|
opushead.version = 1
|
||||||
val procpush = Pointer(-1)
|
opushead.channels = 1
|
||||||
filehandle = bassopus.BASS_OPUS_StreamCreate(opushead,0, procpush, null)
|
opushead.inputrate = samplingrate
|
||||||
if (filehandle != 0){
|
val procpush = Pointer(-1)
|
||||||
if (bass.BASS_ChannelPlay(filehandle,false)){
|
filehandle = bassopus.BASS_OPUS_StreamCreate(opushead,0, procpush, null)
|
||||||
isPlaying = true
|
if (filehandle != 0){
|
||||||
return true
|
if (bass.BASS_ChannelPlay(filehandle,false)){
|
||||||
} else logger.error("BASS_ChannelPlay failed for filehandle $filehandle, code ${bass.BASS_ErrorGetCode()}")
|
isPlaying = true
|
||||||
} else logger.error("BASS_OPUS_StreamCreate failed, code ${bass.BASS_ErrorGetCode()}")
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,11 +51,11 @@ class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
|
|||||||
*/
|
*/
|
||||||
fun Stop(){
|
fun Stop(){
|
||||||
if (filehandle!=0){
|
if (filehandle!=0){
|
||||||
bass.BASS_ChannelStop(filehandle)
|
bass.BASS_ChannelFree(filehandle)
|
||||||
bass.BASS_StreamFree(filehandle)
|
|
||||||
filehandle = 0
|
filehandle = 0
|
||||||
isPlaying = false
|
|
||||||
}
|
}
|
||||||
|
isPlaying = false
|
||||||
|
AudioUtility.Free()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,18 @@ class Codes {
|
|||||||
companion object{
|
companion object{
|
||||||
val audioFilePath = Path(System.getProperty("user.dir"), "audiofile")
|
val audioFilePath = Path(System.getProperty("user.dir"), "audiofile")
|
||||||
private val validAudioExtensions = setOf("wav", "mp3")
|
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> {
|
fun getAudioFiles() : Array<String> {
|
||||||
val audioDir = audioFilePath.toFile()
|
val audioDir = audioFilePath.toFile()
|
||||||
if (!audioDir.exists()) {
|
if (!audioDir.exists()) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import somecodes.Codes;
|
|||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -72,7 +71,7 @@ public class webApp {
|
|||||||
String message = wsMessageContext.message();
|
String message = wsMessageContext.message();
|
||||||
try{
|
try{
|
||||||
var command = objectMapper.readValue(message, WsCommand.class);
|
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);
|
var reply = callback.apply("pocreceiver", command);
|
||||||
wsMessageContext.send(reply);
|
wsMessageContext.send(reply);
|
||||||
} catch (Exception e){
|
} catch (Exception e){
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ package zello
|
|||||||
|
|
||||||
import com.fasterxml.jackson.module.kotlin.contains
|
import com.fasterxml.jackson.module.kotlin.contains
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
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.client.WebSocketClient
|
||||||
import org.java_websocket.handshake.ServerHandshake
|
import org.java_websocket.handshake.ServerHandshake
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
@@ -20,23 +16,27 @@ import java.util.function.BiConsumer
|
|||||||
/**
|
/**
|
||||||
* ZelloClient is a WebSocket client for connecting to Zello services.
|
* ZelloClient is a WebSocket client for connecting to Zello services.
|
||||||
* [Source](https://github.com/zelloptt/zello-channel-api/blob/master/API.md)
|
* [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 streamJob = HashMap<Int, ZelloAudioJob>()
|
||||||
private val imageJob = HashMap<Int, ZelloImageJob>()
|
private val imageJob = HashMap<Int, ZelloImageJob>()
|
||||||
private val commandJob = HashMap<Int, Any>()
|
private val commandJob = HashMap<Int, Any>()
|
||||||
companion object {
|
companion object {
|
||||||
fun fromConsumerZello(username : String, password: String) : ZelloClient {
|
fun fromConsumerZello(username : String, password: String, channel: String) : ZelloClient {
|
||||||
return ZelloClient(URI.create("wss://zello.io/ws"), username, password)
|
return ZelloClient(URI.create("wss://zello.io/ws"), username, password, channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromZelloWork(username: String, password: String, networkName : String) : ZelloClient{
|
fun fromZelloWork(username: String, password: String, channel: String, networkName : String) : ZelloClient{
|
||||||
return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"), username, password)
|
return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"), username, password, channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromZelloEnterpriseServer(username: String, password: String, serverDomain: String) : ZelloClient{
|
fun fromZelloEnterpriseServer(username: String, password: String, channel: String, serverDomain: String) : ZelloClient{
|
||||||
return ZelloClient(URI.create("wss://$serverDomain/ws/mesh"), username, password)
|
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
|
// this key is temporary, valid only 30 days from 2025-07-29
|
||||||
// if need to create, from https://developers.zello.com/keys
|
// 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"
|
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
|
// refresh token for the session
|
||||||
// this is set after the first LogonReply
|
// this is set after the first LogonReply
|
||||||
private var refresh_token: String? = null
|
private var refresh_token: String? = null
|
||||||
|
|
||||||
private val logger : Logger = LoggerFactory.getLogger(ZelloClient::class.java)
|
private val logger : Logger = LoggerFactory.getLogger(ZelloClient::class.java)
|
||||||
private val mapper = jacksonObjectMapper()
|
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){
|
fun Start(event: ZelloEvent){
|
||||||
client = object : WebSocketClient(address) {
|
client = object : WebSocketClient(address) {
|
||||||
override fun onOpen(handshakedata: ServerHandshake?) {
|
override fun onOpen(handshakedata: ServerHandshake?) {
|
||||||
//logger.info("Connected to $address")
|
//logger.info("Connected to $address")
|
||||||
|
isOnline = false
|
||||||
|
currentChannel = null
|
||||||
|
isReceivingStreaming = false
|
||||||
seqID = 0
|
seqID = 0
|
||||||
inc_seqID()
|
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)
|
val value = mapper.writeValueAsString(lg)
|
||||||
send(value)
|
send(value)
|
||||||
}
|
}
|
||||||
@@ -82,6 +90,7 @@ class ZelloClient(val address : URI, val username: String, val password: String)
|
|||||||
job.pushAudioData(data)
|
job.pushAudioData(data)
|
||||||
event.onStreamingData(job.from, job.For?:"", job.channel, data)
|
event.onStreamingData(job.from, job.For?:"", job.channel, data)
|
||||||
}
|
}
|
||||||
|
bytesReceived += data.size
|
||||||
|
|
||||||
}
|
}
|
||||||
0x02.toByte() ->{
|
0x02.toByte() ->{
|
||||||
@@ -130,7 +139,7 @@ class ZelloClient(val address : URI, val username: String, val password: String)
|
|||||||
refresh_token = lgreply.refresh_token
|
refresh_token = lgreply.refresh_token
|
||||||
event.onConnected()
|
event.onConnected()
|
||||||
} else {
|
} else {
|
||||||
logger.error("Failed to logon: ${lgreply.error ?: "Unknown error"}")
|
event.onError("Failed to logon: ${lgreply.error ?: "Unknown error"}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else ->{
|
else ->{
|
||||||
@@ -163,6 +172,8 @@ class ZelloClient(val address : URI, val username: String, val password: String)
|
|||||||
"on_channel_status" -> {
|
"on_channel_status" -> {
|
||||||
val channelstatus = mapper.treeToValue(jsnode, Event_OnChannelStatus::class.java)
|
val channelstatus = mapper.treeToValue(jsnode, Event_OnChannelStatus::class.java)
|
||||||
event.onChannelStatus(channelstatus.channel, channelstatus.status, channelstatus.users_online, channelstatus.error, channelstatus.error_type)
|
event.onChannelStatus(channelstatus.channel, channelstatus.status, channelstatus.users_online, channelstatus.error, channelstatus.error_type)
|
||||||
|
currentChannel = channelstatus.channel
|
||||||
|
isOnline = channelstatus.status== "online"
|
||||||
}
|
}
|
||||||
"on_error" -> {
|
"on_error" -> {
|
||||||
val error = mapper.treeToValue(jsnode, Event_OnError::class.java)
|
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}")
|
logger.info("Stream started on channel ${streamstart.channel} from ${streamstart.from} for ${streamstart.For}")
|
||||||
streamJob[streamstart.stream_id] = ZelloAudioJob.fromEventOnStreamStart(streamstart)
|
streamJob[streamstart.stream_id] = ZelloAudioJob.fromEventOnStreamStart(streamstart)
|
||||||
event.onStartStreaming(streamstart.from, streamstart.For, streamstart.channel)
|
event.onStartStreaming(streamstart.from, streamstart.For, streamstart.channel)
|
||||||
|
isReceivingStreaming = true
|
||||||
|
receivingFrom = streamstart.from
|
||||||
|
bytesReceived = 0 // reset bytes received
|
||||||
}
|
}
|
||||||
"on_stream_stop" -> {
|
"on_stream_stop" -> {
|
||||||
val streamstop = mapper.treeToValue(jsnode, Event_OnStreamStop::class.java)
|
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.onStopStreaming(job.from, job.For?:"", job.channel)
|
||||||
event.onAudioData(job.streamID, job.from, job.For?:"", job.channel, job.getAudioData())
|
event.onAudioData(job.streamID, job.from, job.For?:"", job.channel, job.getAudioData())
|
||||||
}
|
}
|
||||||
|
isReceivingStreaming = false
|
||||||
|
receivingFrom = null
|
||||||
}
|
}
|
||||||
"on_image" ->{
|
"on_image" ->{
|
||||||
val image = mapper.treeToValue(jsnode, Event_OnImage::class.java)
|
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) {
|
override fun onClose(code: Int, reason: String?, remote: Boolean) {
|
||||||
logger.info("Closed from ${if (remote) "Remote side" else "Local side"}, Code=$code, Reason=$reason")
|
logger.info("Closed from ${if (remote) "Remote side" else "Local side"}, Code=$code, Reason=$reason")
|
||||||
event.onDisconnected()
|
event.onDisconnected(reason?: "Unknown reason")
|
||||||
// try reconnecting after 10 seconds
|
isOnline = false
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
currentChannel = null
|
||||||
delay(10000)
|
|
||||||
connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
//Revisi 06/08/2025 : Change to Coroutines
|
//Revisi 06/08/2025 : Change to Coroutines
|
||||||
// val thread = Thread {
|
// val thread = Thread {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface ZelloEvent {
|
|||||||
fun onTextMessage(messageID: Int, from: String, For: String, channel: String, text: String, timestamp: Long)
|
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 onLocation(messageID: Int, from: String, For: String, channel: String, latitude: Double, longitude: Double, address:String, accuracy: Double, timestamp: Long )
|
||||||
fun onConnected()
|
fun onConnected()
|
||||||
fun onDisconnected()
|
fun onDisconnected(reason: String)
|
||||||
fun onError(errorMessage: String)
|
fun onError(errorMessage: String)
|
||||||
fun onStartStreaming(from: String, For: String, channel: String)
|
fun onStartStreaming(from: String, For: String, channel: String)
|
||||||
fun onStopStreaming(from: String, For: String, channel: String)
|
fun onStopStreaming(from: String, For: String, channel: String)
|
||||||
|
|||||||
Reference in New Issue
Block a user