call: P2P аудиозвонки — сигналинг, релей медиа, аудио-контур, headless/desktop/android

- src/call/: сигналинг (call.c, call_proto.h, ETCP_RT_ID_CALL 0x36) + релей медиа
  + headless аудио-сокет (call_headless.c, call_audio_bind) + Opus/jitter/тоны
- chat: события CHAT_EVT_CALL_* (34–40), CHAT_EVT_DM_MESSAGES (41), DM-трамплины
- chatgui: call window, miniaudio I/O, audio_frame_ring, test_audio_jitter
- chatgui-android: CallScreen/IncomingCallDialog, аудио-роутинг, JNI, рингтон
- tests: test_call, test_call_headless
This commit is contained in:
evgeny committed 2026-09-16 10:17:57 +03:00
1 parent 0157f7f96b
commit b11e77e4c0
78 files changed
+7053 -70

No files matched your search

+146
View File
@@ -17,6 +17,8 @@
#include "voiceplayback.h"
#include "audiorecorder.h"
#include "voicemessageencoder.h"
#include "callwindow.h"
#include "call_audio_engine.h"
extern "C" {
#include "../../lib/audio_compressor.h"
@@ -39,10 +41,13 @@ extern "C" {
#include <cmath>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRandomGenerator>
#include <csignal>
extern "C" {
#include "chat/chat_core.h"
#include "call/call.h"
#include "dm/dm_core.h"
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
void chat_core_attachment_download_trampoline(void* arg);
@@ -417,6 +422,38 @@ void MainWindow::setupBridgeCallbacks() {
if (s_mainWindow->m_currentChannelId == chId)
s_mainWindow->m_accountList->setChannel(chId);
});
/* ── звонки ── */
gui_bridge_set_call_incoming_cb([](const char* ch_id, int ch_id_len, uint64_t caller, uint64_t call_id) {
if (!s_mainWindow) return;
s_mainWindow->onCallIncoming(QString::fromUtf8(ch_id, ch_id_len), caller, call_id);
});
gui_bridge_set_call_ringing_cb([](uint64_t call_id) {
if (s_mainWindow) s_mainWindow->onCallRinging(call_id);
});
gui_bridge_set_call_accepted_cb([](uint64_t call_id) {
if (s_mainWindow) s_mainWindow->onCallAccepted(call_id);
});
gui_bridge_set_call_ended_cb([](uint64_t call_id, uint8_t reason) {
if (s_mainWindow) s_mainWindow->onCallEnded(call_id, reason);
});
gui_bridge_set_call_declined_cb([](uint64_t call_id, uint8_t reason) {
if (s_mainWindow) s_mainWindow->onCallEnded(call_id, reason);
});
/* ── DM (p2p чат) ── */
gui_bridge_set_dm_conv_updated_cb([](const char* conv_id, int conv_id_len) {
if (s_mainWindow)
s_mainWindow->onDmConvUpdated(QString::fromUtf8(conv_id, conv_id_len));
});
gui_bridge_set_dm_msg_received_cb([](const char* conv_id, int conv_id_len, uint64_t author) {
if (s_mainWindow)
s_mainWindow->onDmMsgReceived(QString::fromUtf8(conv_id, conv_id_len), author);
});
gui_bridge_set_dm_messages_cb([](const uint8_t* data, int len) {
if (s_mainWindow)
s_mainWindow->m_messageList->onDmMessages(data, len);
});
}
void MainWindow::setupConnects() {
@@ -425,6 +462,12 @@ void MainWindow::setupConnects() {
saveLastChannelId(channelId);
m_currentChannelId = channelId;
m_messageList->loadChannel(channelId);
if (channelId.startsWith(QStringLiteral("dm:"))) {
/* DM: беседа уже создана/обслуживается dm_core */
m_accountList->setChannel(QString());
m_channelList->setChannelUnreadCount(channelId, 0);
return;
}
m_accountList->setChannel(channelId);
struct chat_create_auto_arg* arg = (struct chat_create_auto_arg*)u_calloc(1, sizeof(*arg));
if (arg) {
@@ -442,6 +485,8 @@ void MainWindow::setupConnects() {
connect(m_channelList, &ChannelList::channelSettingsRequested, this, &MainWindow::showChannelSettings);
connect(m_accountList, &AccountList::memberRenamed,
m_messageList, &MessageList::reloadLocalNicks);
connect(m_accountList, &AccountList::callRequested, this, &MainWindow::onCallRequested);
connect(m_accountList, &AccountList::pmRequested, this, &MainWindow::onPmRequested);
connect(m_messageList, &MessageList::readPositionChanged,
this, [this](const QString& channelId, qint64 lastReadMsgId) {
Q_UNUSED(lastReadMsgId);
@@ -453,6 +498,107 @@ void MainWindow::setupConnects() {
connect(m_messageList, &MessageList::openImageRequested, this, &MainWindow::openImage);
}
/* ── звонки ── */
void MainWindow::onCallRequested(const QString& chId, quint64 nodeId, const QString& name) {
quint64 callId = QRandomGenerator::global()->generate64();
if (!callId) callId = 1;
struct call_start_arg* a = (struct call_start_arg*)u_malloc(sizeof(*a));
if (!a) return;
memset(a, 0, sizeof(*a));
a->inst = gui_bridge_get_inst();
strncpy(a->ch_id, chId.toUtf8().constData(), sizeof(a->ch_id) - 1);
a->peer_node_id = nodeId;
a->call_id = callId;
gui_bridge_post_uasync_fn(call_start_trampoline, a);
CallWindow* w = new CallWindow(this);
w->setCallId(callId);
w->setPeerName(name);
w->setState(CallWindow::Outgoing);
connect(w, &CallWindow::closed, this, &MainWindow::closeCallWindow);
m_activeCalls.insert(callId, w);
w->show();
}
void MainWindow::onCallIncoming(const QString& chId, quint64 caller, quint64 callId) {
Q_UNUSED(chId);
if (m_activeCalls.contains(callId)) return;
QString name = m_db->getDisplayName(caller);
if (name.isEmpty()) name = QStringLiteral("0x%1").arg(caller, 16, 16, QChar('0'));
CallWindow* w = new CallWindow(this);
w->setCallId(callId);
w->setPeerName(name);
w->setState(CallWindow::Incoming);
connect(w, &CallWindow::closed, this, &MainWindow::closeCallWindow);
m_activeCalls.insert(callId, w);
w->show();
w->raise();
w->activateWindow();
}
void MainWindow::onCallRinging(quint64 callId) {
/* caller слышит гудок — окно уже показано в Outgoing */
Q_UNUSED(callId);
}
void MainWindow::onCallAccepted(quint64 callId) {
CallWindow* w = m_activeCalls.value(callId, nullptr);
if (w) w->setState(CallWindow::Active);
if (m_db)
CallAudioEngine::instance()->setPlaybackDeviceIndex(m_db->getUiStateInt("audio_output_device", -1));
CallAudioEngine::instance()->start(callId);
}
void MainWindow::onCallEnded(quint64 callId, uint8_t reason) {
Q_UNUSED(reason);
/* тон завершения (~450мс) → авто-teardown аудио-движка */
if (CallAudioEngine::instance()->callId() == callId)
CallAudioEngine::instance()->beginEnd(callId);
closeCallWindow(callId);
}
void MainWindow::closeCallWindow(quint64 callId) {
CallWindow* w = m_activeCalls.take(callId);
if (!w) return;
w->disconnect(this);
w->deleteLater();
}
/* ── DM (p2p чат) ── */
void MainWindow::onPmRequested(const QString& chId, quint64 nodeId, const QString& name) {
struct dm_start_req* r = (struct dm_start_req*)u_malloc(sizeof(*r));
if (!r) return;
memset(r, 0, sizeof(*r));
r->inst = gui_bridge_get_inst();
r->peer_node_id = nodeId;
strncpy(r->source_ch_id, chId.toUtf8().constData(), sizeof(r->source_ch_id) - 1);
strncpy(r->peer_name, name.toUtf8().constData(), sizeof(r->peer_name) - 1);
gui_bridge_post_uasync_fn(dm_start_trampoline, r);
/* открытие беседы произойдёт по CHAT_EVT_DM_CONV_UPDATED (onDmConvUpdated) */
}
void MainWindow::onDmConvUpdated(const QString& convId) {
if (!m_channelList) return;
QString key = QStringLiteral("dm:") + convId;
m_channelList->loadChannels();
m_channelList->selectChannel(key);
}
void MainWindow::onDmMsgReceived(const QString& convId, quint64 author) {
QString key = QStringLiteral("dm:") + convId;
if (m_currentChannelId == key) {
m_messageList->refresh();
return;
}
if (author == m_db->myNodeId()) return;
if (m_channelList)
m_channelList->setChannelUnreadCount(key, m_channelList->getChannelUnreadCount(key) + 1);
}
void MainWindow::setupTray() {
m_trayIcon = new QSystemTrayIcon(this);
m_trayIcon->setIcon(style()->standardIcon(QStyle::SP_ComputerIcon));