mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-26 23:37:33 +00:00
feat(web): страница говорит, что звука нет из-за отсутствия libopus
libopus грузится динамически и объявлен необязательным: `LoadOpus` при отсутствии библиотеки молча возвращает False, `FOpusReady` остаётся сброшен, `PushAudio` выходит первой же строкой, а микрофон из браузера не декодируется (гейт по `Assigned(FOpusDec)`). Снаружи это выглядело как немой приёмник без единого намёка на причину — тем более что браузер при этом бодро пишет «Opus ready»: WASM-декодер тянется с CDN и к серверу отношения не имеет. В state-JSON добавлено поле `opus` = FOpusReady. Флаг ставится один раз в Start до запуска потоков и дальше не меняется, поэтому читается без FStateLock. На странице по `opus:false` поднимается баннер «No audio — libopus is not installed on the server» со второй строкой о том, что молчат и приём, и микрофон. Клик закрывает баннер насовсем (`opusWarnHidden`): состояние рассылается по нескольку раз в секунду, и без флага закрытый баннер возвращался бы на следующем же кадре. Заодно оверлей «Click to start audio» в этом случае не показывается вовсе — AudioContext поднимать не для чего, оверлей только вводил бы в заблуждение. Проверено живьём: демон с системной libopus отдаёт `"opus":true`, он же с подложной битой `libopus.so.0` в LD_LIBRARY_PATH — `"opus":false`, JSON целиком парсится. Логику баннера прогнал на заглушке DOM, JS страницы проверен `node --check`. В настоящем браузере баннер ещё не видели.
This commit is contained in:
+28
-4
@@ -44,6 +44,9 @@ unit WebPageHtml;
|
|||||||
из server state: выпадающий список и кнопка отражают десктоп.
|
из server state: выпадающий список и кнопка отражают десктоп.
|
||||||
- (Mode fix) modGrpSel.onchange при смене группы сразу отправляет
|
- (Mode fix) modGrpSel.onchange при смене группы сразу отправляет
|
||||||
команду mode с дефолтным режимом группы (LSB, CWL, FM, AM).
|
команду 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}
|
{$IFDEF FPC}
|
||||||
@@ -755,6 +758,8 @@ begin
|
|||||||
|
|
||||||
'function applyState(msg){' +
|
'function applyState(msg){' +
|
||||||
'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' +
|
'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 или текущий индекс
|
// Перестраиваем bandSel если изменился список XVTR или текущий индекс
|
||||||
'if(msg.xvtr_bands!=null||msg.xvtr_current!=null){' +
|
'if(msg.xvtr_bands!=null||msg.xvtr_current!=null){' +
|
||||||
'const newBands=msg.xvtr_bands||XVTR_BANDS;' +
|
'const newBands=msg.xvtr_bands||XVTR_BANDS;' +
|
||||||
@@ -999,6 +1004,7 @@ begin
|
|||||||
|
|
||||||
// Opus: async decodeFrame, очередь, оверлей для user gesture
|
// Opus: async decodeFrame, очередь, оверлей для user gesture
|
||||||
'let ws=null,opDec=null,opRdy=false,opQueue=[];' +
|
'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;' +
|
'let txMicStream=null,txMicACtx=null,txMicProc=null,txMicEnc=null,txMicGain=null,prevTx=false,txMicStarting=false;' +
|
||||||
'async function initOpus(){if(opRdy)return;' +
|
'async function initOpus(){if(opRdy)return;' +
|
||||||
'try{const m=await import("https://cdn.jsdelivr.net/npm/opus-decoder@0.7.7/+esm");' +
|
'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===0x57){drawWf(f32(ab));}' +
|
||||||
'else if(t===0x41){if(audioCtx&&audioCtx.state==="suspended")audioCtx.resume();playOpus(new Uint8Array(ab,1));}};}' +
|
'else if(t===0x41){if(audioCtx&&audioCtx.state==="suspended")audioCtx.resume();playOpus(new Uint8Array(ab,1));}};}' +
|
||||||
|
|
||||||
// Оверлей для user gesture (AudioContext требует клика)
|
// Баннер «на сервере нет libopus»: без него отказ выглядит как немой звук.
|
||||||
'function showAudioOverlay(){' +
|
'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");' +
|
'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;";' +
|
'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");' +
|
'const inner=document.createElement("div");' +
|
||||||
'inner.textContent="\u25BA Click to start audio";' +
|
'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;";' +
|
'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.appendChild(inner);' +
|
||||||
'ov.addEventListener("click",function(){audioKick();ov.remove();},{once:true});' +
|
'ov.addEventListener("click",function(){audioKick();ov.remove();audioOv=null;},{once:true});' +
|
||||||
'document.body.appendChild(ov);}' +
|
'document.body.appendChild(ov);audioOv=ov;}' +
|
||||||
|
|
||||||
'window.addEventListener("resize",resize);resize();renderVfos();connect();' +
|
'window.addEventListener("resize",resize);resize();renderVfos();connect();' +
|
||||||
'document.addEventListener("click",function(){if(audioCtx&&audioCtx.state==="suspended")audioCtx.resume();});' +
|
'document.addEventListener("click",function(){if(audioCtx&&audioCtx.state==="suspended")audioCtx.resume();});' +
|
||||||
|
|||||||
+9
-2
@@ -77,6 +77,10 @@ unit WebServer;
|
|||||||
гарантирует создание AudioContext при первом кликe пользователя.
|
гарантирует создание AudioContext при первом кликe пользователя.
|
||||||
- (Audio fix 5) Буферизация Opus-пакетов пока WASM не инициализирован —
|
- (Audio fix 5) Буферизация Opus-пакетов пока WASM не инициализирован —
|
||||||
первые пакеты больше не теряются при медленной загрузке CDN.
|
первые пакеты больше не теряются при медленной загрузке CDN.
|
||||||
|
- (Audio fix 6) В state-JSON добавлено поле "opus" (= FOpusReady). Если
|
||||||
|
libopus в системе нет, LoadOpus молча возвращает False, кадры 'A' не
|
||||||
|
уходят и микрофон не декодируется — раньше это выглядело как немой
|
||||||
|
звук без объяснений. Теперь страница показывает баннер.
|
||||||
}
|
}
|
||||||
|
|
||||||
{$IFDEF FPC}
|
{$IFDEF FPC}
|
||||||
@@ -1838,7 +1842,7 @@ begin
|
|||||||
'"beacon_visible":%s,"beacon_on":%s,"beacon_text":"%s",' +
|
'"beacon_visible":%s,"beacon_on":%s,"beacon_text":"%s",' +
|
||||||
'"beacon_ref_hz":%.0f,"beacon_track_hz":%.0f,' +
|
'"beacon_ref_hz":%.0f,"beacon_track_hz":%.0f,' +
|
||||||
'"rf_agc_visible":%s,"rx_gain_mode":%d,"rx_gain_db":%d,' +
|
'"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,
|
[FFreq, FVfoB, FActiveVfo,
|
||||||
FMode, MODE_N[EnsureRange(FMode, MODE_MIN, MODE_MAX)],
|
FMode, MODE_N[EnsureRange(FMode, MODE_MIN, MODE_MAX)],
|
||||||
FFilterIdx, FFilterBW,
|
FFilterIdx, FFilterBW,
|
||||||
@@ -1874,7 +1878,10 @@ begin
|
|||||||
JsonEscape(FBeaconText), FBeaconRefHz, FBeaconTrackHz,
|
JsonEscape(FBeaconText), FBeaconRefHz, FBeaconTrackHz,
|
||||||
BoolToStr(FRfAgcVisible, 'true', 'false'), FRxGainMode, FRxGainDb,
|
BoolToStr(FRfAgcVisible, 'true', 'false'), FRxGainMode, FRxGainDb,
|
||||||
BoolToStr(FRxMuteVisible, 'true', 'false'),
|
BoolToStr(FRxMuteVisible, 'true', 'false'),
|
||||||
BoolToStr(FRxMuteOn, 'true', 'false')
|
BoolToStr(FRxMuteOn, 'true', 'false'),
|
||||||
|
// Без libopus сервер молча не шлёт кадры 'A' и не принимает микрофон —
|
||||||
|
// страница показывает предупреждение, иначе отказ невидим.
|
||||||
|
BoolToStr(FOpusReady, 'true', 'false')
|
||||||
], FS);
|
], FS);
|
||||||
finally
|
finally
|
||||||
FStateLock.Leave;
|
FStateLock.Leave;
|
||||||
|
|||||||
Reference in New Issue
Block a user