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

+107 -1
View File
@@ -35,6 +35,7 @@ static std::vector<std::unique_ptr<LottieIcon>> s_icons;
extern "C" {
#include "../transport/gui_bridge.h"
#include "chat/chat_core.h"
#include "dm/dm_core.h"
#include "video/video.h"
#include "../../lib/u_async.h"
#include "../../lib/mem.h"
@@ -357,6 +358,21 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
return;
}
if (isDmChannel(m_currentChannelId)) {
QByteArray textData = text.toUtf8();
QString conv = dmConvId(m_currentChannelId);
struct dm_send_req* req = (struct dm_send_req*)u_malloc(sizeof(struct dm_send_req) + textData.size());
if (!req) return;
memset(req, 0, sizeof(*req));
req->inst = gui_bridge_get_inst();
strncpy(req->conv_id, conv.toUtf8().constData(), sizeof(req->conv_id) - 1);
req->data = (uint8_t*)(req + 1);
req->data_len = (uint32_t)textData.size();
memcpy(req->data, textData.constData(), textData.size());
gui_bridge_post_uasync_fn(dm_send_trampoline, req);
return;
}
QByteArray textData = text.toUtf8();
auto ts = QDateTime::currentMSecsSinceEpoch();
@@ -545,6 +561,19 @@ void MessageList::loadChannel(const QString& channelId) {
m_currentChannelId = channelId;
m_inputBar->setRecordingContext(m_db ? m_db->dbPath() + "/media" : QString(), channelId);
m_readDebounce->stop();
/* DM: только текст; сообщения расшифровываются в dm_core (uasync-поток) */
if (isDmChannel(channelId)) {
m_inputBar->setMediaEnabled(false);
requestDmMessages(channelId);
if (prevFocus)
prevFocus->setFocus();
else
m_inputBar->setFocus();
return;
}
m_inputBar->setMediaEnabled(true);
if (!m_db || !m_db->isOpen()) return;
m_localNicks = m_db->getLocalNicknames(channelId);
@@ -606,7 +635,14 @@ void MessageList::loadChannel(const QString& channelId) {
}
void MessageList::refresh() {
if (m_currentChannelId.isEmpty() || !m_db || !m_db->isOpen()) return;
if (m_currentChannelId.isEmpty()) return;
if (isDmChannel(m_currentChannelId)) {
requestDmMessages(m_currentChannelId);
return;
}
if (!m_db || !m_db->isOpen()) return;
qint64 maxId = 0;
for (int r = 0; r < m_model->rowCount(); r++)
@@ -909,3 +945,73 @@ void MessageList::onMessageContextMenu(const QPoint& pos) {
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req);
}
}
/* ── DM (p2p чат) ── */
bool MessageList::isDmChannel(const QString& channelId) {
return channelId.startsWith(QStringLiteral("dm:"));
}
QString MessageList::dmConvId(const QString& channelId) {
return channelId.mid(3);
}
void MessageList::requestDmMessages(const QString& channelId) {
QString conv = dmConvId(channelId);
struct dm_messages_req* req = (struct dm_messages_req*)u_malloc(sizeof(*req));
if (!req) return;
memset(req, 0, sizeof(*req));
req->inst = gui_bridge_get_inst();
strncpy(req->conv_id, conv.toUtf8().constData(), sizeof(req->conv_id) - 1);
gui_bridge_post_uasync_fn(dm_messages_trampoline, req);
}
void MessageList::onDmMessages(const uint8_t* data, int len) {
if (len < 3) return;
uint8_t cl = data[0];
if (len < 1 + (int)cl + 2) return;
QString convId = QString::fromUtf8((const char*)data + 1, cl);
if (m_currentChannelId != QStringLiteral("dm:") + convId) return; /* stale response */
uint16_t count; memcpy(&count, data + 1 + cl, 2);
const uint8_t* p = data + 1 + cl + 2;
const uint8_t* end = data + len;
m_model->clear();
m_avatarCache.clear();
for (int i = 0; i < count; i++) {
if (p + 1 + 8 + 8 + 8 + 1 > end) break;
int dir = p[0]; p += 1;
uint64_t seq; memcpy(&seq, p, 8); p += 8;
uint64_t ts; memcpy(&ts, p, 8); p += 8;
quint64 author; memcpy(&author, p, 8); p += 8;
uint8_t ctl = *p; p += 1;
if (p + ctl > end) break;
p += ctl; /* ct игнорируем: DM v1 — текст */
if (p + 2 > end) break;
uint16_t dlen; memcpy(&dlen, p, 2); p += 2;
if (p + dlen > end) break;
QString text = QString::fromUtf8((const char*)p, dlen);
p += dlen;
AccountRow acc = m_db ? m_db->getAccount(author) : AccountRow();
QString name = authorName(author);
QString letter = acc.avatarLetter.isEmpty() ? name.mid(0, 1) : acc.avatarLetter;
QColor color(acc.avatarColor);
if (!m_avatarCache.contains(author))
m_avatarCache[author] = color;
auto avatar = makeAvatar(color, letter);
auto time = QDateTime::fromMSecsSinceEpoch((qint64)ts).toString("HH:mm");
auto *item = new QStandardItem();
setMsg(item, name, text, time, avatar, false,
false, QString(), QString(), QVariantList{});
item->setData((qint64)seq, MsgIdRole);
item->setData(QVariant::fromValue(author), MsgAuthorNodeIdRole);
item->setData(dir == 1 ? 1 : 0, MsgIsOutgoingRole);
m_model->appendRow(item);
}
m_view->scrollToBottom();
}