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:
2026-08-26 09:17:42 +03:00
parent 98d90e1e5a
commit 957c3c1761
2 changed files with 37 additions and 6 deletions
+9 -2
View File
@@ -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;