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;