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 00000000..25998074
Binary files /dev/null and b/tools/chatgui-android/app/src/main/res/raw/call_connecting.mp3 differ
diff --git a/tools/chatgui-android/app/src/main/res/raw/call_ringing.mp3 b/tools/chatgui-android/app/src/main/res/raw/call_ringing.mp3
new file mode 100644
index 00000000..8f752e69
Binary files /dev/null and b/tools/chatgui-android/app/src/main/res/raw/call_ringing.mp3 differ
diff --git a/tools/chatgui/resources/chatgui.qrc b/tools/chatgui/resources/chatgui.qrc
index 7f8ffec5..0f5120e3 100644
--- a/tools/chatgui/resources/chatgui.qrc
+++ b/tools/chatgui/resources/chatgui.qrc
@@ -11,6 +11,8 @@
icons/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 00000000..25998074
Binary files /dev/null and b/tools/chatgui/resources/sounds/call_connecting.mp3 differ
diff --git a/tools/chatgui/resources/sounds/call_ringing.mp3 b/tools/chatgui/resources/sounds/call_ringing.mp3
new file mode 100644
index 00000000..8f752e69
Binary files /dev/null and b/tools/chatgui/resources/sounds/call_ringing.mp3 differ
diff --git a/tools/chatgui/src/callwindow.cpp b/tools/chatgui/src/callwindow.cpp
index bf482795..0d66c78f 100644
--- a/tools/chatgui/src/callwindow.cpp
+++ b/tools/chatgui/src/callwindow.cpp
@@ -229,6 +229,10 @@ void CallWindow::setState(State s) {
m_state = s;
switch (s) {
case Outgoing:
+ m_statusLabel->setText(QStringLiteral("Пытаемся связаться…"));
+ 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;
}