Add local per-member radio mute controls on desktop and Android

This commit is contained in:
evgeny committed 2026-10-07 13:35:25 +02:00
1 parent a069d11ef9
commit 7e19cbaf3a
33 files changed
+735 -27

No files matched your search

+1
View File
@@ -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));
+1
View File
@@ -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);
+1
View File
@@ -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
View File
@@ -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) {
+7 -1
View File
@@ -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);
+2
View File
@@ -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) {
+109
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+19 -2
View File
@@ -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; }
+32
View File
@@ -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);
+3
View File
@@ -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);