Commit 01/08/2025
This commit is contained in:
41
src/Main.kt
41
src/Main.kt
@@ -1,3 +1,5 @@
|
|||||||
|
import audio.AudioUtility
|
||||||
|
import audio.OpusStreamReceiver
|
||||||
import somecodes.Codes.Companion.ValidString
|
import somecodes.Codes.Companion.ValidString
|
||||||
import zello.ZelloClient
|
import zello.ZelloClient
|
||||||
import zello.ZelloEvent
|
import zello.ZelloEvent
|
||||||
@@ -5,8 +7,23 @@ import zello.ZelloEvent
|
|||||||
//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.
|
||||||
fun main() {
|
fun main() {
|
||||||
|
val au = AudioUtility()
|
||||||
|
var audioID = 0
|
||||||
|
val preferedAudioDevice = "Speakers"
|
||||||
|
au.DetectPlaybackDevices().forEach { pair ->
|
||||||
|
println("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 z = ZelloClient.fromConsumerZello()
|
val o = OpusStreamReceiver(audioID)
|
||||||
|
|
||||||
|
val z = ZelloClient.fromConsumerZello("gtcdevice01","GtcDev2025")
|
||||||
z.Start(object : ZelloEvent {
|
z.Start(object : ZelloEvent {
|
||||||
override fun onChannelStatus(
|
override fun onChannelStatus(
|
||||||
channel: String,
|
channel: String,
|
||||||
@@ -52,6 +69,28 @@ fun main() {
|
|||||||
override fun onError(errorMessage: String) {
|
override fun onError(errorMessage: String) {
|
||||||
println("Error occurred in Zello client: $errorMessage")
|
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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
42
src/audio/AudioFilePlayer.kt
Normal file
42
src/audio/AudioFilePlayer.kt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package audio
|
||||||
|
|
||||||
|
import java.util.function.Consumer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Play(finished: Consumer<Any> ) : Boolean{
|
||||||
|
if (bass.BASS_ChannelPlay(filehandle, false)){
|
||||||
|
|
||||||
|
val thread = Thread{
|
||||||
|
while(true){
|
||||||
|
Thread.sleep(1000)
|
||||||
|
if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){
|
||||||
|
// finished playing
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finished.accept(true)
|
||||||
|
}
|
||||||
|
thread.name = "AudioFilePlayer $filename"
|
||||||
|
thread.isDaemon = true
|
||||||
|
thread.start()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/audio/AudioUtility.kt
Normal file
41
src/audio/AudioUtility.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package audio
|
||||||
|
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()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -18,18 +18,20 @@ public interface BASSOPUS extends Library {
|
|||||||
|
|
||||||
@Structure.FieldOrder({ "version", "channels", "preskip", "inputrate", "gain", "mapping", "streams", "coupled", "chanmap" })
|
@Structure.FieldOrder({ "version", "channels", "preskip", "inputrate", "gain", "mapping", "streams", "coupled", "chanmap" })
|
||||||
class BASS_OPUS_HEAD extends Structure{
|
class BASS_OPUS_HEAD extends Structure{
|
||||||
byte version;
|
public byte version;
|
||||||
byte channels;
|
public byte channels;
|
||||||
short preskip;
|
public short preskip;
|
||||||
int inputrate;
|
public int inputrate;
|
||||||
short gain;
|
public short gain;
|
||||||
byte mapping;
|
public byte mapping;
|
||||||
byte streams;
|
public byte streams;
|
||||||
byte coupled;
|
public byte coupled;
|
||||||
byte[] chanmap = new byte[255];
|
public byte[] chanmap = new byte[255];
|
||||||
}
|
}
|
||||||
|
|
||||||
int BASS_OPUS_StreamCreate(BASS_OPUS_HEAD head, int flags, Bass.STREAMPROC proc, Pointer user);int BASS_OPUS_StreamCreateFile(boolean mem, String file, long offset, long length, int flags);
|
int BASS_OPUS_StreamCreate(BASS_OPUS_HEAD head, int flags, Bass.STREAMPROC proc, Pointer user);
|
||||||
|
int BASS_OPUS_StreamCreate(BASS_OPUS_HEAD head, int flags,Pointer proc, Pointer user);
|
||||||
|
int BASS_OPUS_StreamCreateFile(boolean mem, String file, long offset, long length, int flags);
|
||||||
int BASS_OPUS_StreamCreateURL(String url, int offset, int flags, Bass.DOWNLOADPROC proc, Pointer user);
|
int BASS_OPUS_StreamCreateURL(String url, int offset, int flags, Bass.DOWNLOADPROC proc, Pointer user);
|
||||||
int BASS_OPUS_StreamCreateFileUser(int system, int flags, Bass.BASS_FILEPROCS procs, Pointer user);
|
int BASS_OPUS_StreamCreateFileUser(int system, int flags, Bass.BASS_FILEPROCS procs, Pointer user);
|
||||||
int BASS_OPUS_StreamPutData(int handle, Pointer buffer, int length);
|
int BASS_OPUS_StreamPutData(int handle, Pointer buffer, int length);
|
||||||
|
|||||||
@@ -716,6 +716,7 @@ public interface Bass extends Library {
|
|||||||
boolean BASS_SampleStop(int handle);
|
boolean BASS_SampleStop(int handle);
|
||||||
|
|
||||||
int BASS_StreamCreate(int freq, int chans, int flags, STREAMPROC proc, Pointer user);
|
int BASS_StreamCreate(int freq, int chans, int flags, STREAMPROC proc, Pointer user);
|
||||||
|
int BASS_StreamCreate(int freq, int chans, int flags, Pointer proc, Pointer user);
|
||||||
int BASS_StreamCreateFile(boolean mem, String file, long offset, long length, int flags);
|
int BASS_StreamCreateFile(boolean mem, String file, long offset, long length, int flags);
|
||||||
int BASS_StreamCreateFile(Pointer file, long offset, long length, int flags);
|
int BASS_StreamCreateFile(Pointer file, long offset, long length, int flags);
|
||||||
int BASS_StreamCreateURL(String url, int offset, int flags, DOWNLOADPROC proc, Pointer user);
|
int BASS_StreamCreateURL(String url, int offset, int flags, DOWNLOADPROC proc, Pointer user);
|
||||||
@@ -785,8 +786,7 @@ public interface Bass extends Library {
|
|||||||
boolean BASS_FXGetParameters(int handle, Object params);
|
boolean BASS_FXGetParameters(int handle, Object params);
|
||||||
boolean BASS_FXSetPriority(int handle, int priority);
|
boolean BASS_FXSetPriority(int handle, int priority);
|
||||||
boolean BASS_FXReset(int handle);
|
boolean BASS_FXReset(int handle);
|
||||||
// gak bisa
|
|
||||||
int BASS_StreamCreate(int freq, int chans, int flags, int proc, Pointer user);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
77
src/audio/OpusStreamReceiver.kt
Normal file
77
src/audio/OpusStreamReceiver.kt
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package audio
|
||||||
|
|
||||||
|
import com.sun.jna.Memory
|
||||||
|
import com.sun.jna.Pointer
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpusStreamReceiver is a class that handles receiving Opus audio streams.
|
||||||
|
* @param deviceID The ID of the audio device to use for playback.
|
||||||
|
* @param samplingrate The sampling rate for the audio stream, default is 16000 Hz.
|
||||||
|
* @throws Exception if the device cannot be set.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
|
||||||
|
private val bass = Bass.Instance
|
||||||
|
private val bassopus = BASSOPUS.Instance
|
||||||
|
private var filehandle = 0
|
||||||
|
private val logger : Logger = LoggerFactory.getLogger(OpusStreamReceiver::class.java)
|
||||||
|
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{
|
||||||
|
val opushead = BASSOPUS.BASS_OPUS_HEAD()
|
||||||
|
opushead.version = 1
|
||||||
|
opushead.channels = 1
|
||||||
|
opushead.inputrate = samplingrate
|
||||||
|
val procpush = Pointer(-1)
|
||||||
|
filehandle = bassopus.BASS_OPUS_StreamCreate(opushead,0, procpush, null)
|
||||||
|
if (filehandle != 0){
|
||||||
|
if (bass.BASS_ChannelPlay(filehandle,false)){
|
||||||
|
isPlaying = true
|
||||||
|
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()}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the Opus stream playback and frees the resources.
|
||||||
|
*/
|
||||||
|
fun Stop(){
|
||||||
|
if (filehandle!=0){
|
||||||
|
bass.BASS_ChannelStop(filehandle)
|
||||||
|
bass.BASS_StreamFree(filehandle)
|
||||||
|
filehandle = 0
|
||||||
|
isPlaying = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pushes audio data to the Opus stream for playback.
|
||||||
|
*/
|
||||||
|
fun PushData(data: ByteArray): Boolean {
|
||||||
|
if (filehandle == 0 || !isPlaying) return false
|
||||||
|
if (data.isEmpty()) return false
|
||||||
|
val buffer = Memory(data.size.toLong())
|
||||||
|
buffer.write(0, data, 0, data.size)
|
||||||
|
val result = bassopus.BASS_OPUS_StreamPutData(filehandle, buffer, data.size)
|
||||||
|
if (result>=0){
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
logger.error("PushData failed, error code: ${bass.BASS_ErrorGetCode()}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,8 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder
|
|||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@JsonPropertyOrder(value=["command","seq","channels","type","codec","codec_header","packet_duration","For"])
|
@JsonPropertyOrder(value=["command","seq","channels","type","codec","codec_header","packet_duration","For"])
|
||||||
class StartStreamCommand(val seq: Int, val channels: String, val packet_duration: Int=20, val For:String ) {
|
class StartStreamCommand(val seq: Int, val channels: String, val packet_duration: Int=20, val codec_header: String, val For:String?=null ) {
|
||||||
val command: String = "start_stream"
|
val command: String = "start_stream"
|
||||||
val type: String = "audio"
|
val type: String = "audio"
|
||||||
val codec: String = "opus"
|
val codec: String = "opus"
|
||||||
val codec_header: String ="gD4BPA==" // base64 encoded header for [opus codec header](https://github.com/zelloptt/zello-channel-api/blob/master/API.md#codec_header-attribute)
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
package zello
|
package zello
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.module.kotlin.contains
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
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
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import somecodes.Codes
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.function.BiConsumer
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
/**
|
/**
|
||||||
@@ -15,21 +18,21 @@ import java.nio.ByteBuffer
|
|||||||
* [Source](https://github.com/zelloptt/zello-channel-api/blob/master/API.md)
|
* [Source](https://github.com/zelloptt/zello-channel-api/blob/master/API.md)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
class ZelloClient(val address : URI) {
|
class ZelloClient(val address : URI, val username: String, val password: String) {
|
||||||
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() : ZelloClient {
|
fun fromConsumerZello(username : String, password: String) : ZelloClient {
|
||||||
return ZelloClient(URI.create("wss://zello.io/ws"))
|
return ZelloClient(URI.create("wss://zello.io/ws"), username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromZelloWork(networkName : String) : ZelloClient{
|
fun fromZelloWork(username: String, password: String, networkName : String) : ZelloClient{
|
||||||
return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"))
|
return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"), username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromZelloEnterpriseServer(serverDomain: String) : ZelloClient{
|
fun fromZelloEnterpriseServer(username: String, password: String, serverDomain: String) : ZelloClient{
|
||||||
return ZelloClient(URI.create("wss://$serverDomain/ws/mesh"))
|
return ZelloClient(URI.create("wss://$serverDomain/ws/mesh"), username, password)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,19 +52,18 @@ class ZelloClient(val address : URI) {
|
|||||||
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")
|
||||||
seqID = 0
|
seqID = 0
|
||||||
inc_seqID()
|
inc_seqID()
|
||||||
val lg = LogonCommand.create(seqID,channels, developerKey)
|
val lg = LogonCommand.create(seqID,channels, developerKey, username, password)
|
||||||
val value = mapper.writeValueAsString(lg)
|
val value = mapper.writeValueAsString(lg)
|
||||||
logger.info("Sending LogonCommand: $value")
|
|
||||||
send(value)
|
send(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onMessage(bytes: ByteBuffer?) {
|
override fun onMessage(bytes: ByteBuffer?) {
|
||||||
val receivedbytes = bytes?.capacity() ?: 0
|
val receivedbytes = bytes?.capacity() ?: 0
|
||||||
logger.info("Binary message received, length: $receivedbytes")
|
//logger.info("Binary message received, length: $receivedbytes")
|
||||||
if (receivedbytes>0){
|
if (receivedbytes>0){
|
||||||
when(bytes?.get(0)){
|
when(bytes?.get(0)){
|
||||||
0x01.toByte() -> {
|
0x01.toByte() -> {
|
||||||
@@ -70,10 +72,13 @@ class ZelloClient(val address : URI) {
|
|||||||
val str_id = bytes.getInt(1)
|
val str_id = bytes.getInt(1)
|
||||||
val packet_id = bytes.getInt(5)
|
val packet_id = bytes.getInt(5)
|
||||||
val data = ByteArray(receivedbytes - 9)
|
val data = ByteArray(receivedbytes - 9)
|
||||||
bytes.get(data, 9, receivedbytes - 9)
|
bytes.get(9,data )
|
||||||
logger.info("Audio stream received, stream_id=$str_id, packet_id=$packet_id, data length=${data.size}")
|
val job = streamJob[str_id]
|
||||||
|
if (job!=null){
|
||||||
|
job.pushAudioData(data)
|
||||||
|
event.onStreamingData(job.from, job.For?:"", job.channel, data)
|
||||||
|
}
|
||||||
|
|
||||||
streamJob[str_id]?.pushAudioData(data)
|
|
||||||
}
|
}
|
||||||
0x02.toByte() ->{
|
0x02.toByte() ->{
|
||||||
// image stream
|
// image stream
|
||||||
@@ -82,7 +87,7 @@ class ZelloClient(val address : URI) {
|
|||||||
val image_id = bytes.getInt(1)
|
val image_id = bytes.getInt(1)
|
||||||
val image_type = bytes.getInt(5)
|
val image_type = bytes.getInt(5)
|
||||||
val data = ByteArray(receivedbytes - 9)
|
val data = ByteArray(receivedbytes - 9)
|
||||||
bytes.get(data, 9, receivedbytes - 9)
|
bytes.get(9, data)
|
||||||
logger.info("Image stream received, image_id=$image_id, image_type=$image_type, data length=${data.size}")
|
logger.info("Image stream received, image_id=$image_id, image_type=$image_type, data length=${data.size}")
|
||||||
// Here you can process the image data
|
// Here you can process the image data
|
||||||
// image_type 0x01 is full image, 0x02 is thumbnail
|
// image_type 0x01 is full image, 0x02 is thumbnail
|
||||||
@@ -113,19 +118,44 @@ class ZelloClient(val address : URI) {
|
|||||||
logger.info("Message received: $message")
|
logger.info("Message received: $message")
|
||||||
val jsnode = mapper.readTree(message)
|
val jsnode = mapper.readTree(message)
|
||||||
if (jsnode["seq"] != null) {
|
if (jsnode["seq"] != null) {
|
||||||
val seq = jsnode["seq"].asInt()
|
val seq = jsnode.get("seq").asInt()
|
||||||
if (seq == 1){
|
when(seq){
|
||||||
|
1 ->{
|
||||||
//logon reply
|
//logon reply
|
||||||
val lgreply = mapper.treeToValue(jsnode, LogonReply::class.java)
|
val lgreply = mapper.treeToValue(jsnode, LogonReply::class.java)
|
||||||
if (lgreply.success==true){
|
if (lgreply.success==true){
|
||||||
logger.info("successfully logged on, refresh_token=${lgreply.refresh_token}")
|
|
||||||
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"}")
|
logger.error("Failed to logon: ${lgreply.error ?: "Unknown error"}")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else ->{
|
||||||
|
// other sequence reply
|
||||||
|
val j = commandJob.remove(seq)
|
||||||
|
if (j is BiConsumer<*, *>) {
|
||||||
|
val sucess = jsnode["success"]?.asBoolean() ?: false
|
||||||
|
if (jsnode.contains("stream_id")){
|
||||||
|
// start stream reply
|
||||||
|
val job = j as BiConsumer<Boolean, Int>
|
||||||
|
job.accept(sucess, jsnode["stream_id"].asInt())
|
||||||
|
} else if (jsnode.contains("image_id")){
|
||||||
|
// send image reply
|
||||||
|
val job = j as BiConsumer<Boolean, Int>
|
||||||
|
job.accept(sucess, jsnode["image_id"].asInt())
|
||||||
} else {
|
} else {
|
||||||
// other commands
|
// normal success / fail reply
|
||||||
|
val job = j as BiConsumer<Boolean, Int>
|
||||||
|
job.accept(sucess,0)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// event, tidak ada seq
|
||||||
when (jsnode["command"]?.asText()){
|
when (jsnode["command"]?.asText()){
|
||||||
"on_channel_status" -> {
|
"on_channel_status" -> {
|
||||||
val channelstatus = mapper.treeToValue(jsnode, Event_OnChannelStatus::class.java)
|
val channelstatus = mapper.treeToValue(jsnode, Event_OnChannelStatus::class.java)
|
||||||
@@ -139,12 +169,16 @@ class ZelloClient(val address : URI) {
|
|||||||
val streamstart = mapper.treeToValue(jsnode, Event_OnStreamStart::class.java)
|
val streamstart = mapper.treeToValue(jsnode, Event_OnStreamStart::class.java)
|
||||||
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.put(streamstart.stream_id, ZelloAudioJob.fromEventOnStreamStart(streamstart))
|
streamJob.put(streamstart.stream_id, ZelloAudioJob.fromEventOnStreamStart(streamstart))
|
||||||
|
event.onStartStreaming(streamstart.from, streamstart.For, streamstart.channel)
|
||||||
}
|
}
|
||||||
"on_stream_stop" -> {
|
"on_stream_stop" -> {
|
||||||
val streamstop = mapper.treeToValue(jsnode, Event_OnStreamStop::class.java)
|
val streamstop = mapper.treeToValue(jsnode, Event_OnStreamStop::class.java)
|
||||||
logger.info("Stream stopped on channel ${streamstop.stream_id}")
|
logger.info("Stream stopped on channel ${streamstop.stream_id}")
|
||||||
|
|
||||||
val job = streamJob.remove(streamstop.stream_id)
|
val job = streamJob.remove(streamstop.stream_id)
|
||||||
|
|
||||||
if (job!=null){
|
if (job!=null){
|
||||||
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +201,6 @@ class ZelloClient(val address : URI) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,10 +228,47 @@ class ZelloClient(val address : URI) {
|
|||||||
client?.connect()
|
client?.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the Zello client and closes the WebSocket connection.
|
||||||
|
*/
|
||||||
fun Stop(){
|
fun Stop(){
|
||||||
client?.close()
|
client?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a stream on the specified channel with the given sampling rate.
|
||||||
|
* will raise a callback <Boolean, Int> where Boolean is true if the stream started successfully, and Int is the stream ID.
|
||||||
|
* Stream ID will be used to stop the stream later.
|
||||||
|
* @param chanel which channel to stream to
|
||||||
|
* @param samplingrate the sampling rate for the audio stream
|
||||||
|
* @param For optional parameter to specify the recipient of the stream
|
||||||
|
* @param callback a callback to handle the result of the stream start operation
|
||||||
|
*/
|
||||||
|
fun StartStream(chanel: String, samplingrate: Int, For: String? = null, callback: BiConsumer<Boolean, Int>){
|
||||||
|
val seqID = inc_seqID()
|
||||||
|
val x = StartStreamCommand(
|
||||||
|
seqID,
|
||||||
|
chanel,
|
||||||
|
20,
|
||||||
|
Codes.toCodecHeader(samplingrate,1,60),
|
||||||
|
For)
|
||||||
|
client?.send(mapper.writeValueAsString(x))
|
||||||
|
// put here, because require a response in onMessage
|
||||||
|
commandJob.put(seqID, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops a stream on the specified channel with the given stream ID.
|
||||||
|
* @param channel which channel to stop the stream on
|
||||||
|
* @param streamID the ID of the stream to stop
|
||||||
|
*/
|
||||||
|
fun StopStream(channel: String, streamID: Int){
|
||||||
|
val seqID = inc_seqID()
|
||||||
|
val x = StopStreamCommand(seqID, streamID, channel)
|
||||||
|
client?.send(mapper.writeValueAsString(x))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private fun inc_seqID(): Int{
|
private fun inc_seqID(): Int{
|
||||||
if (seqID < Int.MAX_VALUE) {
|
if (seqID < Int.MAX_VALUE) {
|
||||||
seqID++
|
seqID++
|
||||||
|
|||||||
@@ -11,4 +11,7 @@ interface ZelloEvent {
|
|||||||
fun onConnected()
|
fun onConnected()
|
||||||
fun onDisconnected()
|
fun onDisconnected()
|
||||||
fun onError(errorMessage: String)
|
fun onError(errorMessage: String)
|
||||||
|
fun onStartStreaming(from: String, For: String, channel: String)
|
||||||
|
fun onStopStreaming(from: String, For: String, channel: String)
|
||||||
|
fun onStreamingData(from: String, For: String, channel: String, data: ByteArray)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user