diff --git a/WebPageHtml.pas b/WebPageHtml.pas
index 18183eb..8a45130 100644
--- a/WebPageHtml.pas
+++ b/WebPageHtml.pas
@@ -44,6 +44,9 @@ unit WebPageHtml;
из server state: выпадающий список и кнопка отражают десктоп.
- (Mode fix) modGrpSel.onchange при смене группы сразу отправляет
команду mode с дефолтным режимом группы (LSB, CWL, FM, AM).
+ - (Opus) По флагу state.opus=false (на сервере нет libopus) показывается
+ баннер "No audio — libopus is not installed on the server", а оверлей
+ "Click to start audio" не показывается вовсе — звука всё равно не будет.
}
{$IFDEF FPC}
@@ -755,6 +758,8 @@ begin
'function applyState(msg){' +
'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' +
+ // На сервере нет libopus — аудио и микрофон мертвы молча, показываем баннер.
+ 'if(msg.opus!=null){srvOpus=!!msg.opus;setOpusWarn(!srvOpus);}' +
// Перестраиваем bandSel если изменился список XVTR или текущий индекс
'if(msg.xvtr_bands!=null||msg.xvtr_current!=null){' +
'const newBands=msg.xvtr_bands||XVTR_BANDS;' +
@@ -999,6 +1004,7 @@ begin
// Opus: async decodeFrame, очередь, оверлей для user gesture
'let ws=null,opDec=null,opRdy=false,opQueue=[];' +
+ 'let srvOpus=true,opusWarnEl=null,opusWarnHidden=false,audioOv=null;' +
'let txMicStream=null,txMicACtx=null,txMicProc=null,txMicEnc=null,txMicGain=null,prevTx=false,txMicStarting=false;' +
'async function initOpus(){if(opRdy)return;' +
'try{const m=await import("https://cdn.jsdelivr.net/npm/opus-decoder@0.7.7/+esm");' +
@@ -1108,16 +1114,34 @@ begin
'else if(t===0x57){drawWf(f32(ab));}' +
'else if(t===0x41){if(audioCtx&&audioCtx.state==="suspended")audioCtx.resume();playOpus(new Uint8Array(ab,1));}};}' +
- // Оверлей для user gesture (AudioContext требует клика)
- 'function showAudioOverlay(){' +
+ // Баннер «на сервере нет libopus»: без него отказ выглядит как немой звук.
+ 'function setOpusWarn(on){' +
+ 'if(!on){if(opusWarnEl){opusWarnEl.remove();opusWarnEl=null;}opusWarnHidden=false;return;}' +
+ 'if(audioOv){audioOv.remove();audioOv=null;}' +
+ // Состояние прилетает по нескольку раз в секунду, а setOpusWarn зовётся с
+ // каждым — без opusWarnHidden закрытый баннер тут же возвращался бы.
+ 'if(opusWarnEl||opusWarnHidden)return;' +
+ 'const w=document.createElement("div");' +
+ 'w.style.cssText="position:fixed;top:8px;left:50%;transform:translateX(-50%);z-index:9998;max-width:92vw;padding:8px 12px;border:1px solid #7a3d3d;border-radius:6px;background:#1a0c0c;color:#ffb0b0;font-family:monospace;font-size:12px;line-height:1.5;text-align:center;cursor:pointer;";' +
+ 'w.title="Click to dismiss";' +
+ 'const t1=document.createElement("b");t1.textContent="No audio \u2014 libopus is not installed on the server";' +
+ 'const t2=document.createElement("div");t2.style.color="#c98f8f";' +
+ 't2.textContent="RX audio and the web microphone stay silent until it is installed.";' +
+ 'w.appendChild(t1);w.appendChild(t2);' +
+ 'w.addEventListener("click",function(){w.remove();opusWarnHidden=true;if(opusWarnEl===w)opusWarnEl=null;},{once:true});' +
+ 'document.body.appendChild(w);opusWarnEl=w;}' +
+
+ // Оверлей для user gesture (AudioContext требует клика).
+ // Без libopus звука не будет вовсе — «Click to start audio» только врёт, не показываем.
+ 'function showAudioOverlay(){if(!srvOpus)return;' +
'const ov=document.createElement("div");' +
'ov.style.cssText="position:fixed;inset:0;background:rgba(0,0,0,.72);display:flex;align-items:center;justify-content:center;z-index:9999;cursor:pointer;";' +
'const inner=document.createElement("div");' +
'inner.textContent="\u25BA Click to start audio";' +
'inner.style.cssText="color:#7fff7f;font-size:22px;font-family:monospace;text-align:center;padding:20px;border:1px solid #3d7a3d;border-radius:6px;background:#0c1a0c;";' +
'ov.appendChild(inner);' +
- 'ov.addEventListener("click",function(){audioKick();ov.remove();},{once:true});' +
- 'document.body.appendChild(ov);}' +
+ 'ov.addEventListener("click",function(){audioKick();ov.remove();audioOv=null;},{once:true});' +
+ 'document.body.appendChild(ov);audioOv=ov;}' +
'window.addEventListener("resize",resize);resize();renderVfos();connect();' +
'document.addEventListener("click",function(){if(audioCtx&&audioCtx.state==="suspended")audioCtx.resume();});' +
diff --git a/WebServer.pas b/WebServer.pas
index 3bc2e03..30536fd 100644
--- a/WebServer.pas
+++ b/WebServer.pas
@@ -77,6 +77,10 @@ unit WebServer;
гарантирует создание AudioContext при первом кликe пользователя.
- (Audio fix 5) Буферизация Opus-пакетов пока WASM не инициализирован —
первые пакеты больше не теряются при медленной загрузке CDN.
+ - (Audio fix 6) В state-JSON добавлено поле "opus" (= FOpusReady). Если
+ libopus в системе нет, LoadOpus молча возвращает False, кадры 'A' не
+ уходят и микрофон не декодируется — раньше это выглядело как немой
+ звук без объяснений. Теперь страница показывает баннер.
}
{$IFDEF FPC}
@@ -1838,7 +1842,7 @@ begin
'"beacon_visible":%s,"beacon_on":%s,"beacon_text":"%s",' +
'"beacon_ref_hz":%.0f,"beacon_track_hz":%.0f,' +
'"rf_agc_visible":%s,"rx_gain_mode":%d,"rx_gain_db":%d,' +
- '"rx_mute_visible":%s,"rx_mute_on":%s}',
+ '"rx_mute_visible":%s,"rx_mute_on":%s,"opus":%s}',
[FFreq, FVfoB, FActiveVfo,
FMode, MODE_N[EnsureRange(FMode, MODE_MIN, MODE_MAX)],
FFilterIdx, FFilterBW,
@@ -1874,7 +1878,10 @@ begin
JsonEscape(FBeaconText), FBeaconRefHz, FBeaconTrackHz,
BoolToStr(FRfAgcVisible, 'true', 'false'), FRxGainMode, FRxGainDb,
BoolToStr(FRxMuteVisible, 'true', 'false'),
- BoolToStr(FRxMuteOn, 'true', 'false')
+ BoolToStr(FRxMuteOn, 'true', 'false'),
+ // Без libopus сервер молча не шлёт кадры 'A' и не принимает микрофон —
+ // страница показывает предупреждение, иначе отказ невидим.
+ BoolToStr(FOpusReady, 'true', 'false')
], FS);
finally
FStateLock.Leave;