Add local per-member radio mute controls on desktop and Android
This commit is contained in:
1 parent
a069d11ef9
commit
7e19cbaf3a
33 files changed
+735
-27
No files matched your search
@@ -64,6 +64,7 @@ void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_i
|
||||
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return;
|
||||
if (chat_feedback_ensure_channel(inst, ch_id) < 0) return;
|
||||
if (si_find(inst, ch_id)) return;
|
||||
if (topo_node_sqlite_member_local_init(cc->db, ch_id) < 0) return;
|
||||
|
||||
char tbl_msg[80]; msg_table_name(ch_id, tbl_msg, sizeof(tbl_msg));
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data,
|
||||
[43]="RADIO_TALK",
|
||||
[44]="CALL_CONNECTION",
|
||||
[45]="DM_CONVERSATIONS", [46]="ATTACHMENT_PREPARATION", [47]="DM_CALL_ROUTE",
|
||||
[55]="RADIO_MUTE_RESULT",
|
||||
};
|
||||
const char* n = (type >= 1 && type < (int)(sizeof(names) / sizeof(names[0]))) ? names[type] : "?";
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "chat_event: %s(%d) data=%d bytes", n, type, len);
|
||||
|
||||
@@ -76,6 +76,7 @@ extern "C" {
|
||||
#define CHAT_EVT_GROUP_AVATAR_RESULT 52 /* request_id/group:8LE, failed:1; результат публикации аватарки группы */
|
||||
#define CHAT_EVT_FEEDBACK_CHANGED 53 /* [channel_len:1][channel][message_id:32][kind:1] — состояние после commit */
|
||||
#define CHAT_EVT_FEEDBACK_RESULT 54 /* [request_id:8LE][success:1][JSON] — результат команды интерфейса */
|
||||
#define CHAT_EVT_RADIO_MUTE_RESULT 55 /* [group_id:8LE][node_id:8LE][success:1] — локальный переключатель рации */
|
||||
|
||||
typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
|
||||
|
||||
|
||||
+34
-2
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "../utun_instance.h"
|
||||
#include "../routing_layer/topo_group.h"
|
||||
#include "../radio/radio.h"
|
||||
#include "../../lib/mem.h"
|
||||
#include "../../lib/ll_queue.h"
|
||||
#include "../../lib/json_flat.h"
|
||||
@@ -148,7 +149,7 @@ int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uin
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT p.node_id, COALESCE(n.name,''),"
|
||||
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
|
||||
" COALESCE(p.storage,0), COALESCE(p.deleted,0), COALESCE(p.userinfo,''),p.source,p.adm_tags_sig"
|
||||
" COALESCE(p.storage,0), COALESCE(p.deleted,0), COALESCE(p.userinfo,''),p.source,p.adm_tags_sig,p.radio_muted"
|
||||
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
|
||||
" ORDER BY p.node_id ASC", peers_tbl);
|
||||
|
||||
@@ -179,6 +180,7 @@ int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uin
|
||||
m->storage_role = member_storage_role(adm_tags, st, 7, 8, sqlite3_column_int(st, 5));
|
||||
m->is_self = (uint8_t)(m->node_id == my_id ? 1 : 0);
|
||||
m->flags = parse_adm_tags_flags(adm_tags) | member_radio_flag(inst, ch_id, m->node_id);
|
||||
if (sqlite3_column_int(st, 9)) m->flags |= CHAT_MEMBER_FLAG_RADIO_MUTED;
|
||||
if (sqlite3_column_int(st, 5)) m->flags |= CHAT_MEMBER_FLAG_DELETED;
|
||||
m->rtt = 0xFFFF;
|
||||
m->nat_type = 0xFF;
|
||||
@@ -277,7 +279,7 @@ int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, u
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT COALESCE(n.name,''),"
|
||||
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
|
||||
" COALESCE(p.storage,0), COALESCE(p.deleted,0), COALESCE(p.userinfo,''),p.source,p.adm_tags_sig"
|
||||
" COALESCE(p.storage,0), COALESCE(p.deleted,0), COALESCE(p.userinfo,''),p.source,p.adm_tags_sig,p.radio_muted"
|
||||
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
|
||||
" WHERE p.node_id=?", peers_tbl);
|
||||
|
||||
@@ -296,6 +298,7 @@ int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, u
|
||||
m.storage_role = member_storage_role(adm_tags, st, 6, 7, sqlite3_column_int(st, 4));
|
||||
m.is_self = (uint8_t)(node_id == my_id ? 1 : 0);
|
||||
m.flags = parse_adm_tags_flags(adm_tags) | member_radio_flag(inst, ch_id, node_id);
|
||||
if (sqlite3_column_int(st, 8)) m.flags |= CHAT_MEMBER_FLAG_RADIO_MUTED;
|
||||
if (sqlite3_column_int(st, 4)) m.flags |= CHAT_MEMBER_FLAG_DELETED;
|
||||
m.rtt = 0xFFFF;
|
||||
m.nat_type = 0xFF;
|
||||
@@ -333,6 +336,35 @@ int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, u
|
||||
return 0;
|
||||
}
|
||||
|
||||
int chat_core_toggle_member_radio_mute(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
|
||||
uint8_t member[CHAT_MEMBER_DISPLAY_SIZE];
|
||||
if (!ch_id || !ch_id[0] || strlen(ch_id) >= 64 || chat_core_get_single_member(inst, ch_id, node_id, member) < 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio: mute toggle member not found node=%016llx", (unsigned long long)node_id); return -1;
|
||||
}
|
||||
int muted = !(member[10] & CHAT_MEMBER_FLAG_RADIO_MUTED);
|
||||
if (radio_set_member_muted(inst, strtoull(ch_id, NULL, 10), node_id, muted) < 0) return -1;
|
||||
if (chat_core_get_single_member(inst, ch_id, node_id, member) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: cannot publish local mute ch=%s node=%016llx",
|
||||
ch_id, (unsigned long long)node_id); return -1;
|
||||
}
|
||||
size_t cl = strlen(ch_id);
|
||||
uint8_t evt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE];
|
||||
evt[0] = (uint8_t)cl; memcpy(evt + 1, ch_id, cl); evt[1 + cl] = 1;
|
||||
memcpy(evt + 2 + cl, member, sizeof(member));
|
||||
chat_event_post(inst, CHAT_EVT_MEMBER_UPDATED, evt, (int)(cl + 2 + sizeof(member)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void chat_core_toggle_member_radio_mute_trampoline(void* arg) {
|
||||
struct chat_member_radio_mute_req* req = arg;
|
||||
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: null member mute request"); return; }
|
||||
int rc = chat_core_toggle_member_radio_mute(req->inst, req->ch_id, req->node_id);
|
||||
uint8_t result[17];
|
||||
mf_put64(result, strtoull(req->ch_id, NULL, 10)); mf_put64(result + 8, req->node_id); result[16] = rc == 0;
|
||||
chat_event_post(req->inst, CHAT_EVT_RADIO_MUTE_RESULT, result, sizeof(result));
|
||||
u_free(req);
|
||||
}
|
||||
|
||||
/* ─── трамплины для gui_bridge ─── */
|
||||
|
||||
void chat_core_request_member_list_trampoline(void* arg) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* [0-7] node_id uint64_t LE
|
||||
* [8] online uint8_t
|
||||
* [9] is_self uint8_t
|
||||
* [10] flags uint8_t 0x01=supernode 0x02=admin 0x04=moder 0x08=deleted 0x10=radio
|
||||
* [10] flags uint8_t 0x01=supernode 0x02=admin 0x04=moder 0x08=deleted 0x10=radio 0x20=local radio mute
|
||||
* [11-12] rtt uint16_t LE (ms, 0xFFFF=нет данных)
|
||||
* [13] nat_type uint8_t ADDR_TYPE_*
|
||||
* [14] storage uint8_t
|
||||
@@ -34,6 +34,7 @@ extern "C" {
|
||||
#define CHAT_MEMBER_FLAG_MODER 0x04
|
||||
#define CHAT_MEMBER_FLAG_DELETED 0x08
|
||||
#define CHAT_MEMBER_FLAG_RADIO 0x10
|
||||
#define CHAT_MEMBER_FLAG_RADIO_MUTED 0x20 /* локальное отключение голоса участника */
|
||||
|
||||
#define CHAT_MEMBER_DISPLAY_SIZE 158
|
||||
|
||||
@@ -74,6 +75,11 @@ void chat_core_request_member_list_trampoline(void* arg);
|
||||
* arg = struct chat_member_list_req*. Результат: CHAT_EVT_MEMBER_UPDATED с RTT-обновлениями. */
|
||||
void chat_core_request_member_rtt_trampoline(void* arg);
|
||||
|
||||
/* Локальный переключатель рации; каждый запрос меняет актуальное состояние в uasync. */
|
||||
int chat_core_toggle_member_radio_mute(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
|
||||
struct chat_member_radio_mute_req { struct UTUN_INSTANCE* inst; char ch_id[64]; uint64_t node_id; };
|
||||
void chat_core_toggle_member_radio_mute_trampoline(void* arg);
|
||||
|
||||
/* Инициализация: регистрирует node_props_changed callback для рассылки MEMBER_UPDATED. */
|
||||
void chat_member_init(struct UTUN_INSTANCE* inst);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "../routing_layer/topo_node_sqlite.h"
|
||||
#include "../routing_layer/topo_group.h"
|
||||
#include "../radio/radio.h"
|
||||
#include "chat_core.h"
|
||||
#include "chat_core_priv.h"
|
||||
#include "../utun_instance.h"
|
||||
@@ -884,6 +885,7 @@ int member_sync_verify_and_purge(struct UTUN_INSTANCE* inst, const char* ch_id)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_and_purge — PURGE broken record nid=0x%016llx ns=%s",
|
||||
MS_ID, (unsigned long long)nid, ch_id);
|
||||
topo_node_sqlite_member_del(db, ch_id, nid);
|
||||
radio_forget_member_mute(inst, strtoull(ch_id, NULL, 10), nid);
|
||||
merkle_sync_recompute_path(inst, MT_DATA_MEMBERS, ch_id, nid);
|
||||
/* пассивное удаление: если узел подключён напрямую — убрать из CHAT-группы */
|
||||
if (inst->topo_groups) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// Все функции — в uasync-потоке.
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "../lib/platform_compat.h"
|
||||
#include "../lib/debug_config.h"
|
||||
@@ -21,6 +22,7 @@
|
||||
#include "../transport_layer/etcp.h"
|
||||
#include "../routing_layer/topo_node.h"
|
||||
#include "../routing_layer/topo_group.h"
|
||||
#include "../routing_layer/topo_node_sqlite.h"
|
||||
#include "../chat/chat_event.h"
|
||||
#include "radio.h"
|
||||
#include "radio_proto.h"
|
||||
@@ -57,11 +59,16 @@ struct radio_ctx {
|
||||
void* talker_timer; /* периодический sweep зависших говорящих (self-rearming) */
|
||||
struct radio_seen seen[RADIO_DEDUP_MAX]; /* кэш дедупликации */
|
||||
int seen_count;
|
||||
uint64_t* muted_members; /* локальный кэш, принадлежит uasync-потоку */
|
||||
int muted_count;
|
||||
int muted_loaded;
|
||||
};
|
||||
|
||||
struct radio_instance {
|
||||
radio_frame_fn frame_fn;
|
||||
void* frame_arg;
|
||||
radio_mute_fn mute_fn;
|
||||
void* mute_arg;
|
||||
};
|
||||
|
||||
static void radio_talker_timer_cb(void* arg);
|
||||
@@ -99,6 +106,7 @@ void radio_destroy(struct TOPO_GROUP* group) {
|
||||
RADIO_ID, (unsigned long long)group->group_id,
|
||||
ctx->c_tx_frames, ctx->c_rx_frames, ctx->c_fwd_frames,
|
||||
ctx->c_loop_dropped, ctx->c_nosub_dropped, ctx->c_bad_dropped, ctx->c_dup_dropped);
|
||||
u_free(ctx->muted_members);
|
||||
u_free(ctx);
|
||||
group->radio = NULL;
|
||||
}
|
||||
@@ -439,12 +447,113 @@ void radio_set_frame_cb(struct UTUN_INSTANCE* inst, radio_frame_fn fn, void* arg
|
||||
ri->frame_arg = arg;
|
||||
}
|
||||
|
||||
void radio_set_mute_cb(struct UTUN_INSTANCE* inst, radio_mute_fn fn, void* arg) {
|
||||
struct radio_instance* ri = radio_inst(inst);
|
||||
if (!ri) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: mute callback without instance"); return; }
|
||||
ri->mute_fn = fn; ri->mute_arg = arg;
|
||||
}
|
||||
|
||||
static int radio_load_muted_members(struct radio_ctx* ctx) {
|
||||
struct UTUN_INSTANCE* inst = ctx->group->instance;
|
||||
sqlite3* db = inst->topo_sqlite_db;
|
||||
char sql[160];
|
||||
snprintf(sql, sizeof(sql), "SELECT node_id FROM \"peers_%llu\" WHERE radio_muted<>0",
|
||||
(unsigned long long)ctx->group->group_id);
|
||||
sqlite3_stmt* st = NULL;
|
||||
if (!db || sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: load muted members grp=%016llx: %s",
|
||||
(unsigned long long)ctx->group->group_id, db ? sqlite3_errmsg(db) : "no database"); return -1;
|
||||
}
|
||||
uint64_t* members = NULL;
|
||||
int count = 0, rc;
|
||||
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
|
||||
uint64_t* next = u_realloc(members, (size_t)(count + 1) * sizeof(*members));
|
||||
if (!next) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: muted member cache allocation failed");
|
||||
u_free(members); sqlite3_finalize(st); return -1;
|
||||
}
|
||||
members = next; members[count++] = (uint64_t)sqlite3_column_int64(st, 0);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: read muted members: %s", sqlite3_errmsg(db)); u_free(members); return -1;
|
||||
}
|
||||
u_free(ctx->muted_members); ctx->muted_members = members; ctx->muted_count = count; ctx->muted_loaded = 1;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio: loaded local mutes grp=%016llx count=%d",
|
||||
(unsigned long long)ctx->group->group_id, count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int radio_member_muted(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id) {
|
||||
if (!inst || !inst->topo_groups) return 0;
|
||||
struct radio_ctx* ctx = radio_of(topo_groups_find(inst->topo_groups, group_id));
|
||||
if (!ctx) return 0;
|
||||
for (int i = 0; i < ctx->muted_count; i++) if (ctx->muted_members[i] == node_id) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void radio_forget_member_mute(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id) {
|
||||
if (!inst || !inst->topo_groups) return;
|
||||
struct radio_ctx* ctx = radio_of(topo_groups_find(inst->topo_groups, group_id));
|
||||
if (!ctx) return;
|
||||
for (int i = 0; i < ctx->muted_count; i++) if (ctx->muted_members[i] == node_id) {
|
||||
ctx->muted_members[i] = ctx->muted_members[--ctx->muted_count];
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio: forget removed member mute grp=%016llx node=%016llx",
|
||||
(unsigned long long)group_id, (unsigned long long)node_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int radio_set_member_muted(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id, int muted) {
|
||||
struct TOPO_GROUP* group = inst && inst->topo_groups ? topo_groups_find(inst->topo_groups, group_id) : NULL;
|
||||
struct radio_ctx* ctx = radio_of(group);
|
||||
if (!ctx || group->group_type != TOPO_GROUP_TYPE_CHAT || !node_id || node_id == inst->node_id || (muted != 0 && muted != 1)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio: invalid local mute grp=%016llx node=%016llx muted=%d",
|
||||
(unsigned long long)group_id, (unsigned long long)node_id, muted); return -1;
|
||||
}
|
||||
if (!ctx->muted_loaded && radio_load_muted_members(ctx) < 0) return -1;
|
||||
int slot = -1;
|
||||
for (int i = 0; i < ctx->muted_count; i++) if (ctx->muted_members[i] == node_id) { slot = i; break; }
|
||||
/* Выделить память до записи БД: после commit применение не может отказать. */
|
||||
if (muted && slot < 0) {
|
||||
uint64_t* next = u_realloc(ctx->muted_members, (size_t)(ctx->muted_count + 1) * sizeof(*next));
|
||||
if (!next) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: grow muted member cache failed"); return -1; }
|
||||
ctx->muted_members = next;
|
||||
}
|
||||
sqlite3* db = inst->topo_sqlite_db;
|
||||
char sql[160];
|
||||
snprintf(sql, sizeof(sql), "UPDATE \"peers_%llu\" SET radio_muted=? WHERE node_id=?", (unsigned long long)group_id);
|
||||
sqlite3_stmt* st = NULL;
|
||||
if (!db || sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: prepare local mute: %s", db ? sqlite3_errmsg(db) : "no database"); return -1;
|
||||
}
|
||||
sqlite3_bind_int(st, 1, muted); sqlite3_bind_int64(st, 2, (sqlite3_int64)node_id);
|
||||
int rc = sqlite3_step(st); sqlite3_finalize(st);
|
||||
if (rc != SQLITE_DONE || sqlite3_changes(db) != 1) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: save local mute grp=%016llx node=%016llx rc=%d changed=%d: %s",
|
||||
(unsigned long long)group_id, (unsigned long long)node_id, rc, sqlite3_changes(db), sqlite3_errmsg(db)); return -1;
|
||||
}
|
||||
if (muted && slot < 0) ctx->muted_members[ctx->muted_count++] = node_id;
|
||||
else if (!muted && slot >= 0) ctx->muted_members[slot] = ctx->muted_members[--ctx->muted_count];
|
||||
struct radio_instance* ri = radio_inst(inst);
|
||||
if (ri && ri->mute_fn) ri->mute_fn(inst, group_id, node_id, muted, ri->mute_arg);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio: local member mute grp=%016llx node=%016llx muted=%d",
|
||||
(unsigned long long)group_id, (unsigned long long)node_id, muted);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── подписка (вкл/выкл прослушивание) ── */
|
||||
|
||||
void radio_set_active(struct UTUN_INSTANCE* inst, uint64_t group_id, int on) {
|
||||
if (!inst || !inst->topo_groups) return;
|
||||
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id);
|
||||
if (!group) { DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: set_active group 0x%016llx not found", RADIO_ID, (unsigned long long)group_id); return; }
|
||||
if (on && group->group_type == TOPO_GROUP_TYPE_CHAT) {
|
||||
struct radio_ctx* ctx = radio_of(group);
|
||||
if (!ctx || radio_load_muted_members(ctx) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: subscription rejected because local mutes could not be loaded"); return;
|
||||
}
|
||||
}
|
||||
if (!on) {
|
||||
/* выключение прослушивания: снять всех активных говорящих — индикация должна погаснуть
|
||||
* сразу, а не дожидаться FIN/таймаута */
|
||||
|
||||
+11
-2
@@ -2,8 +2,8 @@
|
||||
//
|
||||
// Флудит Opus-кадры всем подписчикам канала (узлы с TOPO_FLAG_RADIO в NODEINFO).
|
||||
// Передача полудуплексная: радио включается на прослушивание, PTT — на передачу.
|
||||
// Все функции — в uasync-потоке (кроме radio_set_frame_cb, который ставит
|
||||
// обработчик приёма: его вызывает radio_audio в момент init).
|
||||
// Все функции — в uasync-потоке, кроме регистрации frame/mute callbacks
|
||||
// аудио-потребителем при init/destroy.
|
||||
#ifndef RADIO_H
|
||||
#define RADIO_H
|
||||
|
||||
@@ -33,6 +33,15 @@ typedef void (*radio_frame_fn)(struct UTUN_INSTANCE* inst, uint64_t group_id,
|
||||
uint8_t fin, const uint8_t* opus, int len, void* arg);
|
||||
void radio_set_frame_cb(struct UTUN_INSTANCE* inst, radio_frame_fn fn, void* arg);
|
||||
|
||||
/* Локальные настройки участников: только uasync, без изменения подписки и пересылки. */
|
||||
int radio_member_muted(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id);
|
||||
int radio_set_member_muted(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id, int muted);
|
||||
/* После удаления записи участника из БД удалить её из локального кэша. */
|
||||
void radio_forget_member_mute(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id);
|
||||
/* Аудио-потребитель очищает накопленный звук источника при изменении настройки. */
|
||||
typedef void (*radio_mute_fn)(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id, int muted, void* arg);
|
||||
void radio_set_mute_cb(struct UTUN_INSTANCE* inst, radio_mute_fn fn, void* arg);
|
||||
|
||||
/* ── передача (PTT), uasync-поток ── */
|
||||
|
||||
int radio_talk_begin(struct UTUN_INSTANCE* inst, uint64_t group_id); /* новая передача, seq=0 */
|
||||
|
||||
+24
-1
@@ -383,6 +383,21 @@ static int radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, int begin
|
||||
static void radio_audio_update_transmission(void);
|
||||
static void radio_audio_flush_tx(void);
|
||||
|
||||
static void radio_audio_member_mute(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id, int muted, void* arg) {
|
||||
(void)arg;
|
||||
if (!muted) return;
|
||||
pthread_mutex_lock(&g_mtx);
|
||||
if (g_inst == inst && g_active && g_group_id == group_id) {
|
||||
struct radio_source* s = radio_src_find(node_id);
|
||||
if (s) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: clear muted source grp=%016llx src=%016llx stream=%u",
|
||||
RADIO_AUDIO_ID, (unsigned long long)group_id, (unsigned long long)node_id, s->stream_id);
|
||||
radio_src_free(s);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
}
|
||||
|
||||
/* ── lifecycle ── */
|
||||
|
||||
int radio_audio_init(struct UTUN_INSTANCE* inst) {
|
||||
@@ -392,6 +407,7 @@ int radio_audio_init(struct UTUN_INSTANCE* inst) {
|
||||
g_inst = inst; g_tx_pending = g_control_pending = 0; ++g_listen_generation; ++g_instance_generation;
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
radio_set_frame_cb(inst, radio_audio_on_frame, NULL);
|
||||
radio_set_mute_cb(inst, radio_audio_member_mute, NULL);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: initialized", RADIO_AUDIO_ID);
|
||||
return 0;
|
||||
}
|
||||
@@ -399,6 +415,7 @@ int radio_audio_init(struct UTUN_INSTANCE* inst) {
|
||||
void radio_audio_destroy(struct UTUN_INSTANCE* inst) {
|
||||
if (!inst || g_inst != inst) return;
|
||||
radio_set_frame_cb(inst, NULL, NULL);
|
||||
radio_set_mute_cb(inst, NULL, NULL);
|
||||
pthread_mutex_lock(&g_mtx);
|
||||
g_inst = NULL;
|
||||
++g_instance_generation;
|
||||
@@ -963,13 +980,19 @@ int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) {
|
||||
void radio_audio_on_frame(struct UTUN_INSTANCE* inst, uint64_t group_id,
|
||||
uint64_t src_node_id, uint16_t stream_id, uint16_t seq,
|
||||
uint8_t fin, const uint8_t* opus, int len, void* arg) {
|
||||
(void)inst; (void)arg;
|
||||
(void)arg;
|
||||
if (!fin && (!opus || len <= 0)) return;
|
||||
|
||||
uint64_t now = get_time_tb();
|
||||
int muted = radio_member_muted(inst, group_id, src_node_id);
|
||||
pthread_mutex_lock(&g_mtx);
|
||||
if (g_active && group_id == g_group_id) {
|
||||
if (len > 0) { g_last_rx_tb = now; g_last_rx_node = src_node_id; } /* busy-сигнал: чужой аудио-кадр */
|
||||
if (muted) {
|
||||
if (seq == 0) DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: muted burst grp=%016llx src=%016llx stream=%u; RX busy preserved",
|
||||
RADIO_AUDIO_ID, (unsigned long long)group_id, (unsigned long long)src_node_id, stream_id);
|
||||
pthread_mutex_unlock(&g_mtx); return;
|
||||
}
|
||||
struct radio_source* s = radio_src_acquire(src_node_id, stream_id);
|
||||
if (s) {
|
||||
if (len > 0) radio_src_push_frame(s, seq, opus, len, now);
|
||||
|
||||
@@ -162,6 +162,18 @@ static void rh_src_free(struct rh_source* s) {
|
||||
s->used = 0; s->ending = 0; s->rd = s->wr = s->count = 0;
|
||||
}
|
||||
|
||||
static void rh_member_mute(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id, int muted, void* arg) {
|
||||
(void)inst;
|
||||
struct radio_headless* rh = arg;
|
||||
if (!rh || !rh->running || rh->group_id != group_id || !muted) return;
|
||||
struct rh_source* s = rh_src_find(rh, node_id);
|
||||
if (s) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: clear muted source grp=%016llx src=%016llx",
|
||||
RH_ID, (unsigned long long)group_id, (unsigned long long)node_id);
|
||||
rh_src_free(s);
|
||||
}
|
||||
}
|
||||
|
||||
static struct rh_source* rh_src_acquire(struct radio_headless* rh, uint64_t src, uint16_t stream) {
|
||||
struct rh_source* s = rh_src_find(rh, src);
|
||||
if (s) {
|
||||
@@ -209,13 +221,17 @@ static void rh_src_push(struct rh_source* s, const int16_t* pcm, int n) {
|
||||
static void rh_frame_cb(struct UTUN_INSTANCE* inst, uint64_t group_id,
|
||||
uint64_t src_node_id, uint16_t stream_id, uint16_t seq,
|
||||
uint8_t fin, const uint8_t* opus, int len, void* arg) {
|
||||
(void)seq;
|
||||
struct radio_headless* rh = (struct radio_headless*)arg;
|
||||
if (!rh || !rh->running || !opus || len <= 0) return;
|
||||
|
||||
/* активный канал: первый кадр фиксирует group_id (узел слушает один канал) */
|
||||
if (rh->group_id == 0) rh->group_id = group_id;
|
||||
if (group_id != rh->group_id) return;
|
||||
if (radio_member_muted(inst, group_id, src_node_id)) {
|
||||
if (seq == 0) DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: muted burst grp=%016llx src=%016llx stream=%u",
|
||||
RH_ID, (unsigned long long)group_id, (unsigned long long)src_node_id, stream_id);
|
||||
return;
|
||||
}
|
||||
|
||||
struct rh_source* s = rh_src_acquire(rh, src_node_id, stream_id);
|
||||
if (!s) return;
|
||||
@@ -226,7 +242,6 @@ static void rh_frame_cb(struct UTUN_INSTANCE* inst, uint64_t group_id,
|
||||
rh->c_dec_frames++;
|
||||
s->last_frame_tb = get_time_tb();
|
||||
if (fin) s->ending = 1;
|
||||
(void)inst;
|
||||
}
|
||||
|
||||
/* ── периодический микшер: sum источников → PCM клиентам ── */
|
||||
@@ -460,6 +475,7 @@ int radio_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
|
||||
if (!rh->listen_sock_id) { socket_close_wrapper(rh->listen_fd); rh->listen_fd = SOCKET_INVALID; return -1; }
|
||||
|
||||
radio_set_frame_cb(inst, rh_frame_cb, rh);
|
||||
radio_set_mute_cb(inst, rh_member_mute, rh);
|
||||
uasync_set_timeout(ua, 5000, rh, rh_cleanup, "rh_cleanup");
|
||||
rh->mix_timer = uasync_set_timeout(ua, RH_MIX_TIMER_TB, rh, rh_mix_tick, "rh_mix");
|
||||
|
||||
@@ -473,6 +489,7 @@ void radio_headless_destroy(struct UTUN_INSTANCE* inst) {
|
||||
if (!rh || !rh->running) return;
|
||||
rh->running = 0;
|
||||
radio_set_frame_cb(inst, NULL, NULL);
|
||||
radio_set_mute_cb(inst, NULL, NULL);
|
||||
if (rh->mix_timer && rh->ua) { uasync_cancel_timeout(rh->ua, rh->mix_timer); rh->mix_timer = NULL; }
|
||||
if (rh->listen_sock_id && rh->ua) uasync_remove_socket_t(rh->ua, rh->listen_fd);
|
||||
if (rh->listen_fd != SOCKET_INVALID) { socket_close_wrapper(rh->listen_fd); rh->listen_fd = SOCKET_INVALID; }
|
||||
|
||||
@@ -380,6 +380,35 @@ int topo_node_sqlite_init(sqlite3* db) {
|
||||
}
|
||||
|
||||
|
||||
int topo_node_sqlite_member_local_init(sqlite3* db, const char* channel_id) {
|
||||
if (!db || !channel_id || !channel_id[0]) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: invalid local member schema arguments"); return -1;
|
||||
}
|
||||
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
|
||||
char sql[160]; snprintf(sql, sizeof(sql), "PRAGMA table_info(\"%s\")", peers_tbl);
|
||||
sqlite3_stmt* st = NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: inspect member schema ch=%s: %s", channel_id, sqlite3_errmsg(db)); return -1;
|
||||
}
|
||||
int has_radio_muted = 0, rc;
|
||||
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
|
||||
const char* column = (const char*)sqlite3_column_text(st, 1);
|
||||
if (column && !strcmp(column, "radio_muted")) has_radio_muted = 1;
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: read member schema ch=%s: %s", channel_id, sqlite3_errmsg(db)); return -1;
|
||||
}
|
||||
if (!has_radio_muted) {
|
||||
snprintf(sql, sizeof(sql), "ALTER TABLE \"%s\" ADD COLUMN radio_muted INTEGER NOT NULL DEFAULT 0", peers_tbl);
|
||||
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: extend member schema ch=%s: %s", channel_id, sqlite3_errmsg(db)); return -1;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio: added local member mute setting ch=%s", channel_id);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
|
||||
const char* name, uint64_t owner_node_id,
|
||||
const uint8_t* x25519_pub, const uint8_t* x25519_priv,
|
||||
@@ -416,6 +445,7 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
|
||||
" storage INTEGER NOT NULL DEFAULT 0," /* 1 = приоритетный хранитель аттачей */
|
||||
" deleted INTEGER NOT NULL DEFAULT 0," /* 1 = аккаунт удалён владельцем (не отображаем, не подключаемся) */
|
||||
" local_nick TEXT," /* локальный ник (GUI only, не синхронизируется) */
|
||||
" radio_muted INTEGER NOT NULL DEFAULT 0," /* локальное отключение голоса участника */
|
||||
" source INTEGER NOT NULL DEFAULT 0" /* 0=полная merkle-запись 1=упрощённый плейсхолдер topo_group
|
||||
(не участвует в merkle-хеше, дополняется полной записью) */
|
||||
")";
|
||||
@@ -430,6 +460,8 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
|
||||
u_free(ddl);
|
||||
}
|
||||
|
||||
if (topo_node_sqlite_member_local_init(db, channel_id) < 0) return -1;
|
||||
|
||||
int idx_sz = snprintf(NULL, 0,
|
||||
"CREATE INDEX IF NOT EXISTS \"idx_%s_type_rtt\" ON \"%s\" (node_type, node_RTT)", peers_tbl, peers_tbl);
|
||||
char* idx_sql = u_malloc(idx_sz + 1);
|
||||
|
||||
@@ -41,6 +41,9 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
|
||||
const uint8_t* ed25519_pub, const uint8_t* ed25519_priv,
|
||||
const uint8_t* signature);
|
||||
|
||||
/* Подготовить локальные поля участника при создании и загрузке канала. */
|
||||
int topo_node_sqlite_member_local_init(sqlite3* db, const char* channel_id);
|
||||
|
||||
/* Полное локальное удаление канала: DROP peers_<id>/msg_<id>, DELETE channels,
|
||||
* Удаляются feedback_<channel_id> и merkle_tree_hash всех datasets этого канала. 0=ок. */
|
||||
int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id);
|
||||
|
||||
Reference in new issue
Block a user