diff --git a/html/webpage/assets/js/overview.js b/html/webpage/assets/js/overview.js index 72dc879..83a9e68 100644 --- a/html/webpage/assets/js/overview.js +++ b/html/webpage/assets/js/overview.js @@ -251,94 +251,61 @@ function GetListeningZones() { } /** - * Starts live audio for the selected broadcast zone. - * @param {String} bz Broadcast Zone + * Open or Close Live Audio for the selected 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) { - if (bz && bz.length > 0) { - let playurl = `/api/LiveAudio/Open/${bz}`; - 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(); - }) - .then(blob => { - console.log(`Received audio stream for Broadcast Zone: ${bz}`); - 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(); +function LiveAudioCommand(command, bz, cbOK = null, cbFail = null) { + function raise_cbOK(value=null){ + if (cbOK) cbOK(value); } + 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 + }) + }; + fetch(url, payload) + .then(response => { + console.log(`Fetch response for Live Audio Command ${command}:`, JSON.stringify(response)); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => { + console.log(`Live Audio Command ${command} for Broadcast Zone: ${bz} executed on server.`, data); + raise_cbOK(data); + }) + .catch(error => { + console.log(`Error executing Live Audio Command ${command} for Broadcast Zone: ${bz} on server. ${error}`); + raise_cbFail(error); + }); + + } else raise_cbFail("LiveAudioCommand: Unknown command "+command); + } else raise_cbFail("LiveAudioCommand: Broadcast Zone is empty"); + } else raise_cbFail("LiveAudioCommand: command is empty"); } /** - * 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 + * Websocket for streaming */ -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 => { - console.log("Fetch response for closing Live Audio:", JSON.stringify(response)); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.json(); - }) - .then(data => { - console.log(`Live Audio for Broadcast Zone: ${bz} closed on server.`, data); - listenaudio.setAttribute('visibility', 'hidden'); - if (cbOK) cbOK(); - }) - .catch(error => { - console.log(`Error closing Live Audio for Broadcast Zone: ${bz} on server. ${error}`); - if (cbFail) cbFail(); - }); - - - - } else { - alert("Listening audio element not found."); - if (cbFail) cbFail(); - } - } else { - alert("Please select a Broadcast Zone to stop Live Audio."); - if (cbFail) cbFail(); - } -} +let streamws = null; +let mediasource = null; $(document).ready(function () { console.log("overview.js loaded"); @@ -350,12 +317,48 @@ $(document).ready(function () { let $icon = $(this).find('svg'); if ($icon.hasClass('fa-stop')) { 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 { console.log("Starting Live Audio for Broadcast Zone:", bz); - StartLiveAudio(bz); + LiveAudioCommand('Open', bz, (okdata) =>{ + $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); + }); } - $icon.toggleClass('fa-stop fa-play'); + }); $('#clearpagingqueue').off('click').on('click', function () { DoClear("QueuePaging/", "Paging Queue", (okdata) => { diff --git a/libs/linux-aarch64/libbassopus.so b/libs/linux-aarch64/libbassopus.so new file mode 100644 index 0000000..6cebe42 Binary files /dev/null and b/libs/linux-aarch64/libbassopus.so differ diff --git a/libs/linux-armhf/libbassopus.so b/libs/linux-armhf/libbassopus.so new file mode 100644 index 0000000..c21b493 Binary files /dev/null and b/libs/linux-armhf/libbassopus.so differ diff --git a/libs/linux-x86-64/libbassopus.so b/libs/linux-x86-64/libbassopus.so new file mode 100644 index 0000000..20c3b9e Binary files /dev/null and b/libs/linux-x86-64/libbassopus.so differ diff --git a/libs/linux-x86/libbassopus.so b/libs/linux-x86/libbassopus.so new file mode 100644 index 0000000..f9906a0 Binary files /dev/null and b/libs/linux-x86/libbassopus.so differ diff --git a/libs/win32-x86-64/bassopus.dll b/libs/win32-x86-64/bassopus.dll new file mode 100644 index 0000000..9743836 Binary files /dev/null and b/libs/win32-x86-64/bassopus.dll differ diff --git a/libs/win32-x86/bassopus.dll b/libs/win32-x86/bassopus.dll new file mode 100644 index 0000000..6297357 Binary files /dev/null and b/libs/win32-x86/bassopus.dll differ diff --git a/src/MainExtension01.kt b/src/MainExtension01.kt index d95bf23..988afed 100644 --- a/src/MainExtension01.kt +++ b/src/MainExtension01.kt @@ -901,22 +901,18 @@ class MainExtension01 { val remark = variables?.get("REMARK").orEmpty() when(remark){ "GOP" -> { - //TODO Combobox First_Call_Message_Chooser. val remarkMsg = config.Get(configKeys.REMARK_GOP.key) ann_id = Regex("\\[(\\d+)]").find(remarkMsg)?.value?.toIntOrNull() ?: 0 } "GBD" ->{ - // TODO Combobox Second_Call_Message_Chooser val remarkMsg = config.Get(configKeys.REMARK_GBD.key) ann_id = Regex("\\[(\\d+)]").find(remarkMsg)?.value?.toIntOrNull() ?: 0 } "GFC" ->{ - // TODO Combobox Final_Call_Message_Chooser val remarkMsg = config.Get(configKeys.REMARK_GFC.key) ann_id = Regex("\\[(\\d+)]").find(remarkMsg)?.value?.toIntOrNull() ?: 0 } "FLD" ->{ - // TODO Combobox Landed_Message_Chooser val remarkMsg = config.Get(configKeys.REMARK_FLD.key) ann_id = Regex("\\[(\\d+)]").find(remarkMsg)?.value?.toIntOrNull() ?: 0 } diff --git a/src/audio/AudioPlayer.kt b/src/audio/AudioPlayer.kt index 17ee231..1a723dc 100644 --- a/src/audio/AudioPlayer.kt +++ b/src/audio/AudioPlayer.kt @@ -22,6 +22,7 @@ class AudioPlayer (var samplingrate: Int = 44100) { val bassencmp3: BassEncMP3 = BassEncMP3.Instance val bassencopus: BassEncOpus = BassEncOpus.Instance val bassencogg : BassEncOGG = BassEncOGG.Instance + val bassmix : BassMix = BassMix.Instance 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 { "BassEncOpus version ${Integer.toHexString(bassencopus.BASS_Encode_OPUS_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 } diff --git a/src/audio/BassOpus.java b/src/audio/BassOpus.java new file mode 100644 index 0000000..1be5d5b --- /dev/null +++ b/src/audio/BassOpus.java @@ -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); +} diff --git a/src/web/WebApp.kt b/src/web/WebApp.kt index e761e0b..073f2df 100644 --- a/src/web/WebApp.kt +++ b/src/web/WebApp.kt @@ -247,6 +247,13 @@ class WebApp(val listenPort: Int, val userlist: List>, val path("api") { //TODO https://stackoverflow.com/questions/70002015/streaming-into-audio-element 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.onConnect { ctx ->