From b90dc752d25b8a17ec3724c6bfde33a9e249256f Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 23 Sep 2026 19:16:53 +0300 Subject: [PATCH] =?UTF-8?q?call:=20=D0=B4=D0=B2=D0=B5=20=D1=84=D0=B0=D0=B7?= =?UTF-8?q?=D1=8B=20=D0=B8=D1=81=D1=85=D0=BE=D0=B4=D1=8F=D1=89=D0=B5=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=B2=D1=8B=D0=B7=D0=BE=D0=B2=D0=B0=20=E2=80=94=20?= =?UTF-8?q?=D0=B8=D0=BD=D0=B4=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D1=8F=20+=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=BD=D1=8B=D0=B5=20=D0=B3=D1=83=D0=B4=D0=BA?= =?UTF-8?q?=D0=B8=20(desktop/Android)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/chat_call_arch.md | 8 ++ doc/tasks.md | 13 ++++ .../main/java/com/utun/chat/MainActivity.kt | 4 +- .../java/com/utun/chat/data/CallRingback.kt | 72 ++++++++++++++++++ .../chat/ui/components/ActiveCallBanner.kt | 4 +- .../com/utun/chat/ui/screens/CallScreen.kt | 4 +- .../com/utun/chat/viewmodel/ChatViewModel.kt | 5 ++ .../app/src/main/res/raw/call_connecting.mp3 | Bin 0 -> 32492 bytes .../app/src/main/res/raw/call_ringing.mp3 | Bin 0 -> 40556 bytes tools/chatgui/resources/chatgui.qrc | 2 + .../resources/sounds/call_connecting.mp3 | Bin 0 -> 32492 bytes .../chatgui/resources/sounds/call_ringing.mp3 | Bin 0 -> 40556 bytes tools/chatgui/src/callwindow.cpp | 9 ++- tools/chatgui/src/callwindow.h | 6 +- tools/chatgui/src/mainwindow.cpp | 29 +++++-- tools/chatgui/src/mainwindow.h | 1 + tools/chatgui/src/sound_manager.cpp | 3 + tools/chatgui/src/soundsettingspage.cpp | 10 ++- tools/chatgui/src/soundsettingspage.h | 2 +- tools/chatgui/transport/gui_bridge.h | 5 ++ tools/chatgui/transport/gui_bridge_impl.cpp | 14 ++++ 21 files changed, 170 insertions(+), 21 deletions(-) create mode 100644 tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallRingback.kt create mode 100644 tools/chatgui-android/app/src/main/res/raw/call_connecting.mp3 create mode 100644 tools/chatgui-android/app/src/main/res/raw/call_ringing.mp3 create mode 100644 tools/chatgui/resources/sounds/call_connecting.mp3 create mode 100644 tools/chatgui/resources/sounds/call_ringing.mp3 diff --git a/doc/chat_call_arch.md b/doc/chat_call_arch.md index 7b37692d..f18ee8fe 100644 --- a/doc/chat_call_arch.md +++ b/doc/chat_call_arch.md @@ -127,6 +127,14 @@ API: `ajb_create / ajb_push / ajb_pull / ajb_stats / ajb_destroy`. ## 8. Тоны и диагностика +- **Исходящий звонок — две фазы с разными гудками** (у вызывающего): + - фаза 1 «пытаемся связаться» (`OUTGOING`: INVITE отправлен/ретранслируется, маршрут + устанавливается) — гудок «соединение» (короткие бипы); + - фаза 2 «звоним» (`RINGING`: получен RINGING, у пира идёт вызов) — длинные гудки (ringback). + Гудки генерируются платформенно-нативно: desktop — цикличные MP3 через `SoundManager` + (`call_connecting`/`call_ringing`), Android — `ToneGenerator` + (`TONE_SUP_DIAL`/`TONE_SUP_RINGTONE`). Переключение — по `CHAT_EVT_CALL_RINGING`, + остановка — по `CALL_ACCEPTED`/`CALL_ENDED`/`CALL_ERROR`/hangup. - **Сталл >500мс** (нет входящих медиа) → в `call_pull_pcm` микшируется `CALL_TONE_GLITCH`: `пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс) пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл. diff --git a/doc/tasks.md b/doc/tasks.md index ded5f828..442ba8d4 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -68,6 +68,19 @@ [ ] **DM-звонки** (fallback `group_id==0`) — не сделано: `call_is_online`/`call_start` требуют реальную группу (`topo_groups_find`), для DM (group_id==0) маршрута нет. +## Звонок: две фазы исходящего вызова (индикация + гудки) + +[+] **Две фазы вызова** — сделано. Исходящий звонок разбит на 2 фазы с индикацией + в окне вызова и разными гудками: + - фаза 1 «Пытаемся связаться…» (`OUTGOING` — INVITE/маршрут) — гудок «соединение»; + - фаза 2 «Звоним…» (`RINGING` — у пира идёт вызов) — длинные гудки (ringback). + Desktop: `CallWindow::Ringing` + `SoundManager` loop `call_connecting`/`call_ringing` + (MP3 в qrc), переключение по `onCallRinging`, остановка по accepted/ended/error; + добавлена обработка `GUI_EVT_CALL_ERROR` (40) — раньше при «пир офлайн» окно/гудок + зависали навсегда. Android: `CallRingback` (`ToneGenerator` DIAL/RINGTONE), + подписи `OUTGOING`/`RINGING` в CallScreen + ActiveCallBanner. + Звуковые события добавлены в SoundSettingsPage (call_connecting/call_ringing). + ## Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e) Сделано (глобальные синглтоны переведены на `UTUN_INSTANCE`): diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt index bca9334e..2f95b669 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt @@ -113,7 +113,7 @@ class MainActivity : ComponentActivity() { if (screen != Screen.Call) screenBeforeCall = screen screen = Screen.Call } - null -> if (screen == Screen.Call) screen = Screen.ChannelList + null -> if (screen == Screen.Call) screen = screenBeforeCall ?: Screen.ChannelList else -> {} } } @@ -254,7 +254,7 @@ class MainActivity : ComponentActivity() { Screen.Logs -> LogScreen(onBack = { screen = Screen.ChannelList }) Screen.Call -> CallScreen( viewModel = vm, - onClose = { vm.dismissCall(); screen = Screen.ChannelList }, + onClose = { vm.dismissCall(); screen = screenBeforeCall ?: Screen.ChannelList }, onMinimize = { screen = screenBeforeCall ?: Screen.ChannelList } ) Screen.QrScan -> { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallRingback.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallRingback.kt new file mode 100644 index 00000000..9c5aa713 --- /dev/null +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallRingback.kt @@ -0,0 +1,72 @@ +package com.utun.chat.data + +import android.media.AudioAttributes +import android.media.MediaPlayer +import com.utun.chat.ChatApplication +import com.utun.chat.R + +/** + * Гудки исходящего звонка (ringback) для вызывающего через зацикленный MediaPlayer. + * + * Две фазы с разными гудками: + * - фаза 1 «пытаемся связаться» — call_connecting (короткие бипы 425 Гц); + * - фаза 2 «звоним» (пир принял вызов, у него звонит) — call_ringing (длинные гудки). + * + * ToneGenerator для call-тонов на современном Android (API 31+) молчит, поэтому + * гудки — готовые MP3 из res/raw, как входящая мелодия (RingtonePlayer). + * На ACCEPTED/ENDED/hangup вызывающий останавливает гудок. Идемпотентный stop. + */ +object CallRingback { + + @Volatile private var player: MediaPlayer? = null + + /** Фаза 1: короткие бипы «устанавливаем соединение». Идемпотентно. */ + fun startConnecting() { + start(R.raw.call_connecting, "connecting") + } + + /** Фаза 2: длинные гудки (у пира идёт вызов). Идемпотентно. */ + fun startRinging() { + start(R.raw.call_ringing, "ringing") + } + + /** Остановить текущий гудок. Идемпотентно. */ + fun stop() { + release() + LogManager.addLog("INFO", "Ringback", "stopped") + } + + private fun start(resId: Int, tag: String) { + release() + val ctx = ChatApplication.instance + val mp = try { + MediaPlayer.create(ctx, resId) + } catch (e: Exception) { + LogManager.addLog("ERROR", "Ringback", "MediaPlayer.create failed ($tag): ${e.message}") + null + } ?: return + try { + mp.setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .build() + ) + mp.isLooping = true + mp.start() + } catch (e: Exception) { + LogManager.addLog("ERROR", "Ringback", "MediaPlayer start failed ($tag): ${e.message}") + try { mp.release() } catch (_: Exception) {} + return + } + player = mp + LogManager.addLog("INFO", "Ringback", "started $tag") + } + + private fun release() { + val p = player ?: return + player = null + try { p.stop() } catch (_: Exception) {} + try { p.release() } catch (_: Exception) {} + } +} diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ActiveCallBanner.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ActiveCallBanner.kt index b564ca7c..db7b3568 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ActiveCallBanner.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ActiveCallBanner.kt @@ -44,8 +44,8 @@ fun ActiveCallBanner( val name = st.peerName.ifEmpty { "0x%016x".format(st.peerNodeId) } val status = when (phase) { CallPhase.ACTIVE -> "Разговор " + fmtDuration(st.durationMs) - CallPhase.RINGING -> "Гудки…" - else -> "Звоним…" + CallPhase.RINGING -> "Звоним…" + else -> "Пытаемся связаться…" } Row( diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt index b90b6c68..cafb7894 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CallScreen.kt @@ -40,8 +40,8 @@ private fun fmtDuration(sec: Int): String { } private fun callStatusText(phase: CallPhase): String = when (phase) { - CallPhase.OUTGOING -> "Звоним…" - CallPhase.RINGING -> "Гудки…" + CallPhase.OUTGOING -> "Пытаемся связаться…" + CallPhase.RINGING -> "Звоним…" CallPhase.ACTIVE -> "Разговор" CallPhase.ENDED -> "Завершён" else -> "" diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index 39a38b8f..559086e2 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -574,6 +574,7 @@ class ChatViewModel : ViewModel() { val st = _callState.value if (st != null && st.callId == callId) { _callState.value = st.copy(phase = CallPhase.RINGING) + CallRingback.startRinging() LogManager.addLog("INFO", "VM", "CALL_RINGING id=0x%016x".format(callId)) } } @@ -583,6 +584,7 @@ class ChatViewModel : ViewModel() { val st = _callState.value if (st != null && st.callId == callId) { LogManager.addLog("INFO", "VM", "CALL_ACCEPTED id=0x%016x".format(callId)) + CallRingback.stop() callAudio.router.setPreferredRoute( ChatApplication.instance.configProvider.getCallAudioRoute()) if (!callAudio.start(callId)) { @@ -666,6 +668,7 @@ class ChatViewModel : ViewModel() { LogManager.addLog("INFO", "VM", "CALL_$tag id=0x%016x reason=$reason".format(callId)) if (st == null || st.callId != callId) return callAudio.stop() + CallRingback.stop() IncomingCallManager.stopRinging() callIncomingAtMs = 0L _muted.value = false @@ -691,6 +694,7 @@ class ChatViewModel : ViewModel() { _callState.value = CallState(callId = callId, channelId = channelId, peerNodeId = peerNodeId, peerName = name, phase = CallPhase.OUTGOING) NativeLib.callStart(channelId, peerNodeId, callId) + CallRingback.startConnecting() LogManager.addLog("INFO", "VM", "startCall ch=$channelId peer=0x%016x id=0x%016x".format(peerNodeId, callId)) } @@ -727,6 +731,7 @@ class ChatViewModel : ViewModel() { fun dismissCall() { callAudio.stop() + CallRingback.stop() callIncomingAtMs = 0L _callState.value = null } diff --git a/tools/chatgui-android/app/src/main/res/raw/call_connecting.mp3 b/tools/chatgui-android/app/src/main/res/raw/call_connecting.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..25998074ee83606381839b99392c52eddd8a28d9 GIT binary patch literal 32492 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/send.png sounds/msg_incoming.mp3 sounds/call_incoming.mp3 + sounds/call_connecting.mp3 + sounds/call_ringing.mp3 bg.jpg diff --git a/tools/chatgui/resources/sounds/call_connecting.mp3 b/tools/chatgui/resources/sounds/call_connecting.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..25998074ee83606381839b99392c52eddd8a28d9 GIT binary patch literal 32492 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"Пытаемся связаться…")); + m_durationLabel->hide(); + break; + case Ringing: m_statusLabel->setText(QStringLiteral("Звоним…")); m_durationLabel->hide(); break; @@ -273,7 +277,7 @@ void CallWindow::populateDeviceCombo() { void CallWindow::updateButtons() { m_acceptCell->setVisible(m_state == Incoming); m_declineCell->setVisible(m_state == Incoming); - m_hangupCell->setVisible(m_state == Outgoing || m_state == Active); + m_hangupCell->setVisible(m_state == Outgoing || m_state == Ringing || m_state == Active); m_muteCell->setVisible(m_state == Active); m_deviceCombo->setVisible(m_state == Active); } @@ -314,7 +318,8 @@ void CallWindow::onDeviceChanged(int index) { } void CallWindow::closeEvent(QCloseEvent* event) { - if (m_state == Active) postCallFn(call_hangup_trampoline, m_callId); + if (m_state == Outgoing || m_state == Ringing || m_state == Active) + postCallFn(call_hangup_trampoline, m_callId); emit closed(m_callId); event->accept(); } diff --git a/tools/chatgui/src/callwindow.h b/tools/chatgui/src/callwindow.h index efbb3d66..0568c27f 100644 --- a/tools/chatgui/src/callwindow.h +++ b/tools/chatgui/src/callwindow.h @@ -8,12 +8,12 @@ class QPushButton; class QComboBox; class QTimer; -/* Окно звонка: состояния Outgoing (звоним), Incoming (принять/отклонить), - * Active (идёт разговор, таймер), Ended (завершён). */ +/* Окно звонка: состояния Outgoing (пытаемся связаться), Ringing (звоним), + * Incoming (принять/отклонить), Active (идёт разговор, таймер), Ended (завершён). */ class CallWindow : public QWidget { Q_OBJECT public: - enum State { Outgoing, Incoming, Active, Ended }; + enum State { Outgoing, Ringing, Incoming, Active, Ended }; explicit CallWindow(QWidget* parent = nullptr); ~CallWindow() override; diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 028a0543..358fa4f9 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -328,16 +328,20 @@ void MainWindow::setupSoundFromConfig() { else sm->loadSound("call_incoming", resolvePath(callInFile)); + sm->loadSound("call_connecting", ":/sounds/call_connecting.mp3"); + sm->loadSound("call_ringing", ":/sounds/call_ringing.mp3"); + if (!msgOutFile.isEmpty()) sm->loadSound("msg_outgoing", resolvePath(msgOutFile)); if (!connectFile.isEmpty()) sm->loadSound("connect", resolvePath(connectFile)); if (!disconnectFile.isEmpty()) sm->loadSound("disconnect", resolvePath(disconnectFile)); if (m_db && m_db->isOpen()) { sm->setEnabled(m_db->getUiStateInt("sound_enabled", 1)); sm->setVolume(m_db->getUiStateInt("sound_volume", 80) / 100.0f); - QString se[] = {"msg_incoming", "msg_outgoing", "connect", "disconnect", "call_incoming"}; - for (int i = 0; i < 5; i++) { + QString se[] = {"msg_incoming", "msg_outgoing", "connect", "disconnect", + "call_incoming", "call_connecting", "call_ringing"}; + for (int i = 0; i < 7; i++) { const QString& ek = se[i]; - sm->setEvent(ek, m_db->getUiStateInt(QStringLiteral("sound_%1_on").arg(ek), (i == 0 || i == 4) ? 1 : 0), + sm->setEvent(ek, m_db->getUiStateInt(QStringLiteral("sound_%1_on").arg(ek), (i == 0 || i >= 4) ? 1 : 0), m_db->getUiStateInt(QStringLiteral("sound_%1_vol").arg(ek), 80) / 100.0f); } } @@ -448,6 +452,10 @@ void MainWindow::setupBridgeCallbacks() { gui_bridge_set_call_declined_cb([](uint64_t call_id, uint8_t reason) { if (s_mainWindow) s_mainWindow->onCallEnded(call_id, reason); }); + gui_bridge_set_call_error_cb([](uint64_t call_id, uint8_t err, const char* text, int len) { + Q_UNUSED(text); Q_UNUSED(len); + if (s_mainWindow) s_mainWindow->onCallError(call_id, err); + }); gui_bridge_set_call_path_cb([](uint64_t call_id, const char* text, int len) { if (s_mainWindow) s_mainWindow->onCallPath(call_id, QString::fromUtf8(text, len)); }); @@ -537,6 +545,8 @@ void MainWindow::onCallRequested(const QString& chId, quint64 nodeId, const QStr connect(w, &CallWindow::closed, this, &MainWindow::closeCallWindow); m_activeCalls.insert(callId, w); w->show(); + + SoundManager::instance()->playLoop("call_connecting"); } void MainWindow::onCallIncoming(const QString& chId, quint64 caller, quint64 callId) { @@ -563,8 +573,11 @@ void MainWindow::onCallIncoming(const QString& chId, quint64 caller, quint64 cal } void MainWindow::onCallRinging(quint64 callId) { - /* caller слышит гудок — окно уже показано в Outgoing */ - Q_UNUSED(callId); + /* caller: INVITE доставлен, у пира идёт вызов — фаза 2 (гудки) */ + CallWindow* w = m_activeCalls.value(callId, nullptr); + if (w && w->state() == CallWindow::Outgoing) + w->setState(CallWindow::Ringing); + SoundManager::instance()->playLoop("call_ringing"); } void MainWindow::onCallAccepted(quint64 callId) { @@ -584,6 +597,12 @@ void MainWindow::onCallEnded(quint64 callId, uint8_t reason) { closeCallWindow(callId); } +void MainWindow::onCallError(quint64 callId, uint8_t err) { + /* пир офлайн / нет маршрута — звонок не состоялся */ + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MainWindow: call error id=%016llx err=%u", (unsigned long long)callId, (unsigned)err); + closeCallWindow(callId); +} + void MainWindow::onCallPath(quint64 callId, const QString& path) { CallWindow* w = m_activeCalls.value(callId, nullptr); if (w) w->setBgpPath(path); diff --git a/tools/chatgui/src/mainwindow.h b/tools/chatgui/src/mainwindow.h index 4a20235d..e890418c 100644 --- a/tools/chatgui/src/mainwindow.h +++ b/tools/chatgui/src/mainwindow.h @@ -72,6 +72,7 @@ public: void onCallRinging(quint64 callId); void onCallAccepted(quint64 callId); void onCallEnded(quint64 callId, uint8_t reason); + void onCallError(quint64 callId, uint8_t err); void onCallPath(quint64 callId, const QString& path); void closeCallWindow(quint64 callId); diff --git a/tools/chatgui/src/sound_manager.cpp b/tools/chatgui/src/sound_manager.cpp index 8e593f21..96113581 100644 --- a/tools/chatgui/src/sound_manager.cpp +++ b/tools/chatgui/src/sound_manager.cpp @@ -164,6 +164,9 @@ void SoundManager::playLoop(const QString& name) { if (!m_enabled) return; if (!m_initialized) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "SoundManager::playLoop: not initialized"); return; } + /* остановить предыдущий цикл (иначе call_connecting + call_ringing звучат вместе) */ + stopLoop(); + EventSettings es = m_eventSettings.value(name); if (!es.enabled) return; diff --git a/tools/chatgui/src/soundsettingspage.cpp b/tools/chatgui/src/soundsettingspage.cpp index b6e9e96a..c9099c49 100644 --- a/tools/chatgui/src/soundsettingspage.cpp +++ b/tools/chatgui/src/soundsettingspage.cpp @@ -60,8 +60,10 @@ SoundSettingsPage::SoundSettingsPage(DbManager* db, QWidget* parent) m_events[2] = {"connect", "Peer connected", nullptr, nullptr, nullptr}; m_events[3] = {"disconnect", "Peer disconnected", nullptr, nullptr, nullptr}; m_events[4] = {"call_incoming", "Incoming call", nullptr, nullptr, nullptr}; + m_events[5] = {"call_connecting", "Call: connecting", nullptr, nullptr, nullptr}; + m_events[6] = {"call_ringing", "Call: ringback", nullptr, nullptr, nullptr}; - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 7; i++) { auto& e = m_events[i]; auto* row = new QHBoxLayout(); row->setSpacing(8); @@ -119,11 +121,11 @@ void SoundSettingsPage::loadFromDb() { m_volumeLabel->setEnabled(enabled); m_volumeLabel->setText(QString::number(vol) + "%"); - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 7; i++) { auto& e = m_events[i]; QString onKey = QStringLiteral("sound_%1_on").arg(e.key); QString volKey = QStringLiteral("sound_%1_vol").arg(e.key); - bool on = m_db->getUiStateInt(onKey, (i == 0 || i == 4) ? 1 : 0); // msg_incoming и call_incoming on by default + bool on = m_db->getUiStateInt(onKey, (i == 0 || i >= 4) ? 1 : 0); // msg_incoming и звонковые on by default int ev = m_db->getUiStateInt(volKey, 80); e.check->setChecked(on); e.slider->setValue(ev); @@ -139,7 +141,7 @@ void SoundSettingsPage::applyAndSave() { sm->setEnabled(enabled); sm->setVolume(masterVol); - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 7; i++) { auto& e = m_events[i]; bool on = e.check->isChecked(); float ev = e.slider->value() / 100.0f; diff --git a/tools/chatgui/src/soundsettingspage.h b/tools/chatgui/src/soundsettingspage.h index 9e09c59d..450c92bc 100644 --- a/tools/chatgui/src/soundsettingspage.h +++ b/tools/chatgui/src/soundsettingspage.h @@ -28,5 +28,5 @@ private: QCheckBox* m_enabledCheck; QSlider* m_volumeSlider; QLabel* m_volumeLabel; - EventRow m_events[5]; + EventRow m_events[7]; }; diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h index 60b68164..5098abcf 100644 --- a/tools/chatgui/transport/gui_bridge.h +++ b/tools/chatgui/transport/gui_bridge.h @@ -47,6 +47,7 @@ struct UTUN_INSTANCE; #define GUI_EVT_CALL_ENDED 37 /* data: [call_id:8][reason:1] */ #define GUI_EVT_CALL_DECLINED 38 /* data: [call_id:8][reason:1] */ #define GUI_EVT_CALL_STATS 39 /* data: [call_id:8][rtt_ms:2][buffer_ms:2][tempo_x100:2] */ +#define GUI_EVT_CALL_ERROR 40 /* data: [call_id:8][err:1][text:var] */ #define GUI_EVT_DM_MESSAGES 41 /* data: [conv_id_len:1][conv_id:var][count:2]([dir:1][seq:8][ts:8][author:8][ct_len:1][ct:var][data_len:2][data:var])* */ #define GUI_EVT_CALL_PATH 42 /* data: [call_id:8][text:var] — BGP-маршрут до пира звонка */ #define GUI_EVT_RADIO_TALK 43 /* data: [group_id:8][src_node_id:8][stream_id:2][on:1] — кто-то начал/закончил говорить */ @@ -190,6 +191,10 @@ typedef void (*gui_call_ended_fn)(uint64_t call_id, uint8_t reason); void gui_bridge_set_call_ended_cb(gui_call_ended_fn cb); void gui_bridge_set_call_declined_cb(gui_call_ended_fn cb); +/* Callback для ошибки звонка (пир офлайн/нет маршрута). text может быть пустым. */ +typedef void (*gui_call_error_fn)(uint64_t call_id, uint8_t err, const char* text, int len); +void gui_bridge_set_call_error_cb(gui_call_error_fn cb); + /* Callback для BGP-маршрута звонка (вызывается из GUI-потока) */ typedef void (*gui_call_path_fn)(uint64_t call_id, const char* text, int len); void gui_bridge_set_call_path_cb(gui_call_path_fn cb); diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index 8d1cbf4b..bd7196e0 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -53,6 +53,7 @@ static gui_call_id_fn g_call_ringing_cb = nullptr; static gui_call_id_fn g_call_accepted_cb = nullptr; static gui_call_ended_fn g_call_ended_cb = nullptr; static gui_call_ended_fn g_call_declined_cb = nullptr; +static gui_call_error_fn g_call_error_cb = nullptr; static gui_call_path_fn g_call_path_cb = nullptr; static gui_radio_talk_fn g_radio_talk_cb = nullptr; static gui_dm_msg_received_fn g_dm_msg_received_cb = nullptr; @@ -307,6 +308,15 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { if (g_call_declined_cb) g_call_declined_cb(callId, d[8]); } break; + case GUI_EVT_CALL_ERROR: + if (dlen >= 9) { + uint64_t callId; memcpy(&callId, d, 8); + uint8_t err = d[8]; + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CALL_ERROR id=0x%016llx err=%u text=%.*s", + (unsigned long long)callId, (unsigned)err, dlen - 9, (const char*)d + 9); + if (g_call_error_cb) g_call_error_cb(callId, err, (const char*)d + 9, dlen - 9); + } + break; case GUI_EVT_CALL_PATH: if (dlen >= 9) { uint64_t callId; memcpy(&callId, d, 8); @@ -503,6 +513,10 @@ void gui_bridge_set_call_declined_cb(gui_call_ended_fn cb) { g_call_declined_cb = cb; } +void gui_bridge_set_call_error_cb(gui_call_error_fn cb) { + g_call_error_cb = cb; +} + void gui_bridge_set_call_path_cb(gui_call_path_fn cb) { g_call_path_cb = cb; }