commit 12/11/2025

This commit is contained in:
2025-11-12 10:15:03 +07:00
parent bf554b4f15
commit 46be98363a
11 changed files with 134 additions and 89 deletions

View File

@@ -251,95 +251,62 @@ function GetListeningZones() {
} }
/** /**
* Starts live audio for the selected broadcast zone. * Open or Close Live Audio for the selected Broadcast Zone.
* @param {String} bz Broadcast Zone * @param {string} command either 'Open' or 'Close'
* @param {string} bz Broadcast Zone
* @param {Function} cbOK callback function on success
* @param {Function} cbFail callback function on failure
*/ */
function StartLiveAudio(bz, cbOK = null, cbFail = null) { function LiveAudioCommand(command, bz, cbOK = null, cbFail = null) {
if (bz && bz.length > 0) { function raise_cbOK(value=null){
let playurl = `/api/LiveAudio/Open/${bz}`; if (cbOK) cbOK(value);
const listenaudio = document.getElementById('listenaudio');
if (listenaudio) {
fetch(playurl, { method: 'GET' })
.then(response => {
console.log("Fetch response for Live Audio:", JSON.stringify(response));
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} }
return response.blob(); function raise_cbFail(value){
if (cbFail) cbFail(value);
}
if (command && command.length>0){
if (bz && bz.length>0){
if (command === 'Open' || command === 'Close'){
let url = `/api/LiveAudio`;
let payload = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
command: command,
broadcastzone: bz
}) })
.then(blob => { };
console.log(`Received audio stream for Broadcast Zone: ${bz}`); fetch(url, payload)
const url = window.URL.createObjectURL(blob);
if (listenaudio) {
listenaudio.pause();
listenaudio.src = url;
listenaudio.load();
listenaudio.play();
console.log(`Started Live Audio for Broadcast Zone: ${bz}`);
listenaudio.setAttribute('visibility', 'visible');
if (cbOK) cbOK();
} else new Error("Listening audio element not found.");
})
.catch(error => {
alert(`Error starting Live Audio for Broadcast Zone: ${bz}. ${error}`);
if (cbFail) cbFail();
});
} else {
alert("Listening audio element not found.");
if (cbFail) cbFail();
}
} else {
alert("Please select a Broadcast Zone to start Live Audio.");
if (cbFail) cbFail();
}
}
/**
* Stops live audio for the selected broadcast zone.
* @param {String} bz Broadcast Zone
* @param {Function} cbOK Callback function on success
* @param {Function} cbFail Callback function on failure
*/
function StopLiveAudio(bz, cbOK = null, cbFail = null) {
if (bz && bz.length > 0) {
const listenaudio = document.getElementById('listenaudio');
if (listenaudio) {
listenaudio.pause();
listenaudio.src = "";
console.log("Stopped Live Audio.");
let url = `/api/LiveAudio/Close/${bz}`;
fetch(url, { method: 'GET' })
.then(response => { .then(response => {
console.log("Fetch response for closing Live Audio:", JSON.stringify(response)); console.log(`Fetch response for Live Audio Command ${command}:`, JSON.stringify(response));
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
console.log(`Live Audio for Broadcast Zone: ${bz} closed on server.`, data); console.log(`Live Audio Command ${command} for Broadcast Zone: ${bz} executed on server.`, data);
listenaudio.setAttribute('visibility', 'hidden'); raise_cbOK(data);
if (cbOK) cbOK();
}) })
.catch(error => { .catch(error => {
console.log(`Error closing Live Audio for Broadcast Zone: ${bz} on server. ${error}`); console.log(`Error executing Live Audio Command ${command} for Broadcast Zone: ${bz} on server. ${error}`);
if (cbFail) cbFail(); raise_cbFail(error);
}); });
} else raise_cbFail("LiveAudioCommand: Unknown command "+command);
} else raise_cbFail("LiveAudioCommand: Broadcast Zone is empty");
} else { } else raise_cbFail("LiveAudioCommand: command is empty");
alert("Listening audio element not found.");
if (cbFail) cbFail();
}
} else {
alert("Please select a Broadcast Zone to stop Live Audio.");
if (cbFail) cbFail();
}
} }
/**
* Websocket for streaming
*/
let streamws = null;
let mediasource = null;
$(document).ready(function () { $(document).ready(function () {
console.log("overview.js loaded"); console.log("overview.js loaded");
@@ -350,12 +317,48 @@ $(document).ready(function () {
let $icon = $(this).find('svg'); let $icon = $(this).find('svg');
if ($icon.hasClass('fa-stop')) { if ($icon.hasClass('fa-stop')) {
console.log("Stopping Live Audio for Broadcast Zone:", bz); console.log("Stopping Live Audio for Broadcast Zone:", bz);
StopLiveAudio(bz); LiveAudioCommand('Close', bz, (okdata) =>{
$icon.toggleClass('fa-stop fa-play');
$("#listenzone").prop('disabled', false);
if (streamws) {
streamws.close();
streamws = null;
}
if (mediasource) {
mediasource.endOfStream();
mediasource = null;
}
let audio = document.getElementById('listenaudio');
audio.src = "";
}, (errdata) =>{
alert("Error stopping Live Audio: " + errdata);
});
} else { } else {
console.log("Starting Live Audio for Broadcast Zone:", bz); console.log("Starting Live Audio for Broadcast Zone:", bz);
StartLiveAudio(bz); LiveAudioCommand('Open', bz, (okdata) =>{
}
$icon.toggleClass('fa-stop fa-play'); $icon.toggleClass('fa-stop fa-play');
$("#listenzone").prop('disabled', true);
streamws = new WebSocket(`ws://${window.location.host}/LiveAudio/ws`);
mediasource = new MediaSource();
let audio = document.getElementById('listenaudio');
audio.src = URL.createObjectURL(mediasource);
mediasource.addEventListener('sourceopen', () => {
const sourceBuffer = mediasource.addSourceBuffer('audio/mpeg; codecs="mp3"');
streamws.binaryType = 'arraybuffer';
streamws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const chunk = new Uint8Array(event.data);
sourceBuffer.appendBuffer(chunk);
}
};
});
}, (errdata) =>{
alert("Error starting Live Audio: " + errdata);
});
}
}); });
$('#clearpagingqueue').off('click').on('click', function () { $('#clearpagingqueue').off('click').on('click', function () {
DoClear("QueuePaging/", "Paging Queue", (okdata) => { DoClear("QueuePaging/", "Paging Queue", (okdata) => {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
libs/win32-x86/bassopus.dll Normal file

Binary file not shown.

View File

@@ -901,22 +901,18 @@ class MainExtension01 {
val remark = variables?.get("REMARK").orEmpty() val remark = variables?.get("REMARK").orEmpty()
when(remark){ when(remark){
"GOP" -> { "GOP" -> {
//TODO Combobox First_Call_Message_Chooser.
val remarkMsg = config.Get(configKeys.REMARK_GOP.key) val remarkMsg = config.Get(configKeys.REMARK_GOP.key)
ann_id = Regex("\\[(\\d+)]").find(remarkMsg)?.value?.toIntOrNull() ?: 0 ann_id = Regex("\\[(\\d+)]").find(remarkMsg)?.value?.toIntOrNull() ?: 0
} }
"GBD" ->{ "GBD" ->{
// TODO Combobox Second_Call_Message_Chooser
val remarkMsg = config.Get(configKeys.REMARK_GBD.key) val remarkMsg = config.Get(configKeys.REMARK_GBD.key)
ann_id = Regex("\\[(\\d+)]").find(remarkMsg)?.value?.toIntOrNull() ?: 0 ann_id = Regex("\\[(\\d+)]").find(remarkMsg)?.value?.toIntOrNull() ?: 0
} }
"GFC" ->{ "GFC" ->{
// TODO Combobox Final_Call_Message_Chooser
val remarkMsg = config.Get(configKeys.REMARK_GFC.key) val remarkMsg = config.Get(configKeys.REMARK_GFC.key)
ann_id = Regex("\\[(\\d+)]").find(remarkMsg)?.value?.toIntOrNull() ?: 0 ann_id = Regex("\\[(\\d+)]").find(remarkMsg)?.value?.toIntOrNull() ?: 0
} }
"FLD" ->{ "FLD" ->{
// TODO Combobox Landed_Message_Chooser
val remarkMsg = config.Get(configKeys.REMARK_FLD.key) val remarkMsg = config.Get(configKeys.REMARK_FLD.key)
ann_id = Regex("\\[(\\d+)]").find(remarkMsg)?.value?.toIntOrNull() ?: 0 ann_id = Regex("\\[(\\d+)]").find(remarkMsg)?.value?.toIntOrNull() ?: 0
} }

View File

@@ -22,6 +22,7 @@ class AudioPlayer (var samplingrate: Int = 44100) {
val bassencmp3: BassEncMP3 = BassEncMP3.Instance val bassencmp3: BassEncMP3 = BassEncMP3.Instance
val bassencopus: BassEncOpus = BassEncOpus.Instance val bassencopus: BassEncOpus = BassEncOpus.Instance
val bassencogg : BassEncOGG = BassEncOGG.Instance val bassencogg : BassEncOGG = BassEncOGG.Instance
val bassmix : BassMix = BassMix.Instance
var initedDevice = -1 var initedDevice = -1
@@ -34,6 +35,7 @@ class AudioPlayer (var samplingrate: Int = 44100) {
Logger.info { "BassEncMP3 version ${Integer.toHexString(bassencmp3.BASS_Encode_MP3_GetVersion())}" } Logger.info { "BassEncMP3 version ${Integer.toHexString(bassencmp3.BASS_Encode_MP3_GetVersion())}" }
Logger.info { "BassEncOpus version ${Integer.toHexString(bassencopus.BASS_Encode_OPUS_GetVersion())}" } Logger.info { "BassEncOpus version ${Integer.toHexString(bassencopus.BASS_Encode_OPUS_GetVersion())}" }
Logger.info {" BassEncOGG version ${Integer.toHexString(bassencogg.BASS_Encode_OGG_GetVersion())}"} Logger.info {" BassEncOGG version ${Integer.toHexString(bassencogg.BASS_Encode_OGG_GetVersion())}"}
Logger.info {" BassMix version ${Integer.toHexString(bassmix.BASS_Mixer_GetVersion())}"}
InitAudio(0) // Audio 0 is No Sound, use for reading and writing wav silently InitAudio(0) // Audio 0 is No Sound, use for reading and writing wav silently
} }

37
src/audio/BassOpus.java Normal file
View File

@@ -0,0 +1,37 @@
package audio;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
@SuppressWarnings("unused")
public interface BassOpus extends Library {
BassOpus Instance = (BassOpus) Native.load("bassopus", BassOpus.class);
// BASS_CHANNELINFO type
int BASS_CTYPE_STREAM_OPUS = 0x11200;
// Additional attributes
int BASS_ATTRIB_OPUS_ORIGFREQ = 0x13000;
int BASS_ATTRIB_OPUS_GAIN = 0x13001;
class BASS_OPUS_HEAD {
public byte version;
public byte channels;
public short preskip;
public int inputrate;
public short gain;
public byte mapping;
public byte streams;
public byte coupled;
public byte[] chanmap;
}
int BASS_STREAMPROC_OPUS_LOSS = 0x40000000;
int BASS_OPUS_StreamCreate(BASS_OPUS_HEAD head, int flags, Bass.STREAMPROC proc, Object user);
int BASS_OPUS_StreamCreate(BASS_OPUS_HEAD head, int flags, int proc, Object user);
int BASS_OPUS_StreamCreateFile(String file, long offset, long length, int flags);
int BASS_OPUS_StreamCreateFile(Pointer file, long offset, long length, int flags);
int BASS_OPUS_StreamCreateURL(String url, int offset, int flags, Bass.DOWNLOADPROC proc, Object user);
int BASS_OPUS_StreamCreateFileUser(int system, int flags, Bass.BASS_FILEPROCS procs, Object user);
int BASS_OPUS_StreamPutData(int handle, Pointer buffer, int length);
}

View File

@@ -247,6 +247,13 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
path("api") { path("api") {
//TODO https://stackoverflow.com/questions/70002015/streaming-into-audio-element //TODO https://stackoverflow.com/questions/70002015/streaming-into-audio-element
path("LiveAudio") { path("LiveAudio") {
post{
val json : JsonNode = objectmapper.readTree(it.body())
val broadcastzone = json.get("broadcastzone")?.asText("") ?: ""
val command = json.get("command")?.asText("") ?: ""
println("LiveAudio command=$command for zone $broadcastzone from ${it.host()}" )
}
ws("/ws/{uuid}"){ws -> ws("/ws/{uuid}"){ws ->
ws.onConnect { ws.onConnect {
ctx -> ctx ->