From 700034eec8b13506cc1a321a0fb6f2656e2b440a Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 16 Oct 2026 09:49:58 +0200 Subject: [PATCH] Add broadcast RS settings to desktop and Android --- doc/stream.txt | 4 ++ src/chat/chat_setting.c | 2 + src/stream/stream_publisher.c | 31 +++++++--- tests/test_android_stream.c | 42 +++++++++++++- .../java/com/utun/chat/data/ConfigProvider.kt | 15 +++++ .../utun/chat/ui/screens/SettingsScreen.kt | 56 ++++++++++++++++++- .../app/src/main/res/values-ru/strings.xml | 7 +++ .../app/src/main/res/values/strings.xml | 7 +++ tools/chatgui/src/settingsdialog.cpp | 45 +++++++++++++++ tools/chatgui/src/settingsdialog.h | 3 + tools/chatgui/translations/chatgui_en.ts | 6 ++ tools/chatgui/translations/chatgui_ru.ts | 6 ++ 12 files changed, 212 insertions(+), 12 deletions(-) diff --git a/doc/stream.txt b/doc/stream.txt index 4f3b431c..ca137453 100644 --- a/doc/stream.txt +++ b/doc/stream.txt @@ -77,6 +77,10 @@ INFO/SUBSCRIBE/REVERSE_REQ к неподписанному узлу получа Дескриптор содержит ключ трансляции, n, r и максимальный размер RS-кодограммы block_size. Параметры задаёт только источник и не меняет в пределах source_epoch; изменение создаёт новый запуск. +В настройках desktop/Android раздел «Трансляция» сохраняет общее число субблоков и число избыточных RS. +В [chatserver] это stream_subblocks (1..255, по умолчанию 8) и stream_rs_redundant (0..n-1, по умолчанию 2). +Медиапубликатор читает параметры при запуске и один раз выделяет рабочий буфер по ёмкости пачки, +но не больше предела медиаблока 65536 байт. Изменение настроек действует только со следующего запуска. Дескриптор подтверждается подписанной опцией источника; поля FEC также входят в подписанную пачку. Ограничения кодека: 1 <= n <= 255, 0 <= r < n, 10 <= block_size <= 4096. Кроме допустимости RS проверяются расход памяти и место для подписанных метаданных, подписи и хотя бы одного байта содержимого. diff --git a/src/chat/chat_setting.c b/src/chat/chat_setting.c index f45ab5c9..06238352 100644 --- a/src/chat/chat_setting.c +++ b/src/chat/chat_setting.c @@ -33,6 +33,8 @@ struct chat_setting_def { /* ─── реестр всех известных настроек ─── */ static const struct chat_setting_def g_setting_defs[] = { + {"stream_subblocks", CHAT_SETTING_INT, 8, 1, 255}, + {"stream_rs_redundant", CHAT_SETTING_INT, 2, 0, 254}, {"storage_autoload", CHAT_SETTING_BOOL, 1, 0, 1}, {"opus_codec_preset", CHAT_SETTING_INT, 1, 0, 4}, {"radio_opus_mode", CHAT_SETTING_INT, 0, 0, 1}, diff --git a/src/stream/stream_publisher.c b/src/stream/stream_publisher.c index 210d40e9..8e4effb7 100644 --- a/src/stream/stream_publisher.c +++ b/src/stream/stream_publisher.c @@ -31,6 +31,8 @@ struct stream_publisher { struct stream_media_format format; struct stream_publisher_status status; struct publisher_queue video, audio; + uint8_t *wire; + size_t capacity; uint64_t group, next_frame, last_pts[2], drain_until, report_us; int seen[2], stopping, audio_enabled, audio_dirty; }; @@ -80,6 +82,7 @@ static void publisher_close(struct stream_publisher *p, int reason) { if (p->stats_timer) uasync_cancel_timeout(p->inst->ua, p->stats_timer); p->timer = p->stats_timer = NULL; if (p->handle) stream_close(p->handle); + u_free(p->wire); p->wire = NULL; p->capacity = 0; p->handle = NULL; records_clear(&p->video); records_clear(&p->audio); p->status.state = reason ? STREAM_PUBLISHER_FAILED : STREAM_PUBLISHER_ENDED; p->status.reason = reason; p->status.video_bytes = p->status.audio_bytes = p->status.total_bytes = 0; @@ -121,9 +124,9 @@ static void publisher_stats(void *arg) { /* Упаковка общая для обеих платформ: Opus целиком, видео с ограниченными продолжениями. */ static int publisher_block(struct stream_publisher *p) { struct stream_media_block block = {.format = p->format}; - uint8_t wire[STREAM_DEFAULT_INPUT_CAPACITY]; - size_t capacity = stream_publish_capacity(p->handle), used = STREAM_MEDIA_HEADER_SIZE; - if (capacity > sizeof(wire) || capacity < 256) { + uint8_t *wire = p->wire; + size_t capacity = p->capacity, used = STREAM_MEDIA_HEADER_SIZE; + if (!wire || capacity < 256) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "publisher invalid capacity=%zu", capacity); return STREAM_ERR_STATE; } for (struct publisher_record *r = p->audio.head; r && block.audio_count < 8; r = r->next) { @@ -158,7 +161,21 @@ static void publisher_process(void *arg) { struct stream_publisher *p = arg; pthread_mutex_lock(&p->lock); p->post = NULL; p->timer = NULL; if (p->status.state == STREAM_PUBLISHER_OPENING) { - int result = p->stopping ? STREAM_ERR_STATE : stream_publish_open(p->inst, p->group, NULL, &p->handle); + struct stream_parameters parameters = { + .n = chat_setting_get_int(p->inst, "stream_subblocks", STREAM_DEFAULT_N), + .r = chat_setting_get_int(p->inst, "stream_rs_redundant", STREAM_DEFAULT_R), + .block_size = STREAM_RS_DEFAULT_SIZE + }; + int result = p->stopping ? STREAM_ERR_STATE : stream_publish_open(p->inst, p->group, ¶meters, &p->handle); + if (result == STREAM_OK) { + p->capacity = stream_publish_capacity(p->handle); + if (p->capacity > STREAM_MEDIA_MAX_BLOCK) p->capacity = STREAM_MEDIA_MAX_BLOCK; + p->wire = u_malloc(p->capacity); + if (!p->wire) { + DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "publisher buffer allocation failed capacity=%zu", p->capacity); + result = STREAM_ERR_MEMORY; + } + } if (result == STREAM_OK) result = stream_media_enable(p->handle, 0); struct stream_info info; if (result == STREAM_OK) result = stream_get_info(p->handle, &info); @@ -173,9 +190,9 @@ static void publisher_process(void *arg) { p->status.compressor_rise_rate = chat_setting_get_int(p->inst, "compressor_rise_rate", 2); p->stats_timer = uasync_set_timeout(p->inst->ua, 1000, p, publisher_stats, "publisher_stats"); if (!p->stats_timer) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "publisher stats start failed"); publisher_close(p, STREAM_CLOSE_FAILED); } - DEBUG_INFO(DEBUG_CATEGORY_STREAM, "publisher opened id=%llu group=%llx epoch=%llx codec=%u size=%ux%u audio=%u", + DEBUG_INFO(DEBUG_CATEGORY_STREAM, "publisher opened id=%llu group=%llx epoch=%llx codec=%u size=%ux%u audio=%u n=%u r=%u capacity=%zu", (unsigned long long)p->status.id, (unsigned long long)p->group, (unsigned long long)p->status.epoch, - p->format.video_codec, p->format.width, p->format.height, p->format.audio_codec); + p->format.video_codec, p->format.width, p->format.height, p->format.audio_codec, parameters.n, parameters.r, p->capacity); } } uint64_t now = get_time_us(); @@ -198,7 +215,7 @@ static void publisher_process(void *arg) { uint64_t oldest = p->video.head ? p->video.head->created : p->audio.head->created; if (p->audio.head && p->audio.head->created < oldest) oldest = p->audio.head->created; int immediate = p->video.head && p->video.head->key; - if (!p->stopping && !immediate && pending + STREAM_MEDIA_HEADER_SIZE + STREAM_MEDIA_VIDEO_SIZE < stream_publish_capacity(p->handle) + if (!p->stopping && !immediate && pending + STREAM_MEDIA_HEADER_SIZE + STREAM_MEDIA_VIDEO_SIZE < p->capacity && now - oldest < 20000) break; int result = publisher_block(p); if (result < 0) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "publisher block failed result=%d", result); publisher_close(p, STREAM_CLOSE_FAILED); break; } diff --git a/tests/test_android_stream.c b/tests/test_android_stream.c index aa3221de..c390fbda 100644 --- a/tests/test_android_stream.c +++ b/tests/test_android_stream.c @@ -1,5 +1,6 @@ /* Android actor: локальный preview, очередь, stop/FIN, отменённый open и detach concurrent producer. */ #include +#include #include #include @@ -8,6 +9,7 @@ #include "../lib/sqlite3.h" #include "../src/chat/chat_core.h" #include "../src/chat/chat_event.h" +#include "../src/chat/chat_setting.h" #include "../src/stream/stream.h" #include "../src/stream/stream_member.h" #include "../src/utun_instance.h" @@ -61,7 +63,7 @@ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_STREAM, DEBUG_LEVEL_INFO); size_t before = u_get_allocated_count(); test_clock_init(100); ua = uasync_create(); CHECK(ua); struct UTUN_INSTANCE *inst = utun_instance_create_from_str(ua, - "[global]\nauto_sockets=no\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\nmember_files_enabled=0\n"); + "[global]\nauto_sockets=no\n[chatserver]\ngroup_autoconnect=0\nstream_subblocks=8\nstream_rs_redundant=0\nstorage_autoload=0\nmember_files_enabled=0\n"); CHECK(inst && !utun_core_start(inst) && !chat_service_start(inst)); chat_core_create_channel_auto(inst, "Stream \"test\" 😀"); sqlite3_stmt *statement = NULL; @@ -80,11 +82,15 @@ int main(void) { CHECK(source_events == 2 && strstr(source_snapshot, "epoch")); struct stream_member_state member; CHECK(stream_member_load(inst, group, inst->node_id, &member, NULL) == STREAM_OK && member.published); - CHECK(member.audio_enabled); + CHECK(member.audio_enabled && member.source.n == 8 && member.source.r == 0); + CHECK(chat_setting_set(inst, "stream_subblocks", "8") == 0); + CHECK(chat_setting_set(inst, "stream_rs_redundant", "2") == 0); + CHECK(chat_setting_set(inst, "stream_subblocks", "256") == -2); + CHECK(chat_setting_set(inst, "stream_rs_redundant", "-1") == -2); CHECK(android_stream_set_audio_enabled(1, 0) == 0); test_clock_poll(ua, 100); struct stream_member_state muted; CHECK(stream_member_load(inst, group, inst->node_id, &muted, NULL) == STREAM_OK && !muted.audio_enabled); - CHECK(muted.source.source_epoch == member.source.source_epoch); + CHECK(muted.source.source_epoch == member.source.source_epoch && muted.source.n == 8 && muted.source.r == 0); CHECK(android_stream_set_audio_enabled(2, 1) < 0); CHECK(android_stream_set_audio_enabled(1, 1) == 0); test_clock_poll(ua, 100); CHECK(stream_member_load(inst, group, inst->node_id, &muted, NULL) == STREAM_OK && muted.audio_enabled); @@ -178,6 +184,8 @@ int main(void) { CHECK(!strcmp(source_snapshot, "[]")); current_format.rotation = 0; current_format.audio_codec = 0; CHECK(android_stream_open(3, group, 0, 1280, 720, 0, 0, 0, STREAM_MEDIA_H264) == 0); u_free(wait_event(3, 4, &size)); + CHECK(stream_member_load(inst, group, inst->node_id, &member, NULL) == STREAM_OK); + CHECK(member.source.n == 8 && member.source.r == 2); CHECK(android_stream_set_audio_enabled(3, 1) < 0); uint8_t *large = u_malloc(500000); CHECK(large); memset(large, 1, 500000); CHECK(feed(3, 1, 500000, 1, large, 500000) == 1); u_free(large); @@ -195,6 +203,34 @@ int main(void) { android_stream_attach(inst); CHECK(android_stream_open(4, group, 0, 1280, 720, 0, 0, 0, STREAM_MEDIA_H264) == 0); android_stream_stop(4); u_free(wait_event(4, 5, &size)); + /* Маленькая пачка и пачка сверх предела медиаблока: полный кадр проходит через локальный viewer. */ + const unsigned totals[] = {1, 16, 64}; + for (unsigned test = 0; test < sizeof(totals) / sizeof(totals[0]); test++) { + unsigned total = totals[test], parity = total == 16 ? 4 : 0; + char setting[8]; snprintf(setting, sizeof(setting), "%u", total); + CHECK(chat_setting_set(inst, "stream_subblocks", setting) == 0); + snprintf(setting, sizeof(setting), "%u", parity); + CHECK(chat_setting_set(inst, "stream_rs_redundant", setting) == 0); + CHECK(android_stream_open(6, group, 0, 1280, 720, 0, 0, 0, STREAM_MEDIA_H264) == 0); + u_free(wait_event(6, 4, &size)); + CHECK(stream_member_load(inst, group, inst->node_id, &member, NULL) == STREAM_OK); + CHECK(member.source.n == total && member.source.r == parity); + struct STREAM_HANDLE *reader = NULL; + CHECK(stream_media_open(inst, group, inst->node_id, member.source.source_epoch, STREAM_MEDIA_TRACK_VIDEO, &reader) == STREAM_OK); + size_t length = total == 1 ? sizeof(video) : 100000; + uint8_t *frame = u_malloc(length); CHECK(frame); memset(frame, 7, length); + CHECK(feed(6, 1, 100000, 1, frame, length) == 1); + struct stream_media_packet packet; int received = 0; + for (unsigned i = 0; i < 1000 && !received; i++) { + test_clock_poll(ua, 100); + int result = stream_media_read(reader, 100000, 1, -1, &packet); + if (result == STREAM_OK) { + CHECK(packet.kind == STREAM_MEDIA_READ_VIDEO && packet.video.length == length && !memcmp(packet.video.data, frame, length)); received = 1; + } else CHECK(result == STREAM_WAIT || result == STREAM_MORE); + } + CHECK(received); u_free(frame); stream_close(reader); + android_stream_stop(6); u_free(wait_event(6, 5, &size)); + } android_stream_detach(inst); uasync_mark_stopped(ua); utun_instance_destroy(inst); uasync_destroy(ua, 0); u_free(source_snapshot); CHECK(u_get_allocated_count() == before); diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt index 26081346..47e244bd 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt @@ -54,6 +54,8 @@ class ConfigProvider(private val context: Context) { private val chatStorageAutoloadMaxsizeMb = intPreferencesKey("chat_storage_autoload_maxsize_mb") private val chatStorageMaxsizeGb = intPreferencesKey("chat_storage_maxsize_gb") private val chatOpusCodecPreset = intPreferencesKey("chat_opus_codec_preset") + private val streamSubblocks = intPreferencesKey("stream_subblocks") + private val streamRsRedundant = intPreferencesKey("stream_rs_redundant") private val callJitterMaxReserveMs = intPreferencesKey("call_jitter_max_reserve_ms") private val callVideoBitrateRange = stringPreferencesKey("call_video_bitrate_range_kbps") private val chatCompressorEnabled = intPreferencesKey("chat_compressor_enabled") @@ -155,6 +157,15 @@ class ConfigProvider(private val context: Context) { LogManager.addLog("INFO", "CallSettings", "Video bitrate range saved: ${range.encode()} kbps") } + suspend fun saveStreamSettings(subblocks: Int, redundant: Int) { + require(subblocks in 1..255 && redundant in 0 until subblocks) + context.dataStore.edit { prefs -> + prefs[streamSubblocks] = subblocks + prefs[streamRsRedundant] = redundant + } + LogManager.addLog("INFO", "STREAM", "Broadcast settings saved n=$subblocks r=$redundant; apply on next start") + } + fun getString(key: String): String? = runBlocking { when { key == "node.private_key" -> readKeysFromFile()?.second ?: context.dataStore.data.first()[nodePrivateKey] @@ -179,6 +190,8 @@ class ConfigProvider(private val context: Context) { key == "chat.storage_autoload_maxsize_mb" -> context.dataStore.data.first()[chatStorageAutoloadMaxsizeMb] ?: 10 key == "chat.storage_maxsize_gb" -> context.dataStore.data.first()[chatStorageMaxsizeGb] ?: 1 key == "chat.opus_codec_preset" -> context.dataStore.data.first()[chatOpusCodecPreset] ?: 1 + key == "chat.stream_subblocks" -> context.dataStore.data.first()[streamSubblocks] ?: 8 + key == "chat.stream_rs_redundant" -> context.dataStore.data.first()[streamRsRedundant] ?: 2 key == "chat.call_jitter_max_reserve_ms" -> context.dataStore.data.first()[callJitterMaxReserveMs] ?: 1000 key == "chat.compressor_enabled" -> context.dataStore.data.first()[chatCompressorEnabled] ?: 1 key == "chat.compressor_max_gain_db" -> context.dataStore.data.first()[chatCompressorMaxGainDb] ?: 15 @@ -682,6 +695,8 @@ class ConfigProvider(private val context: Context) { appendLine("radio_capture_stereo=$radioCaptureStereo") appendLine("radio_compressor_enabled=$radioCompEnabled") appendLine("radio_compressor_max_gain_db=$radioCompMaxGain") + appendLine("stream_subblocks=${getInt("chat.stream_subblocks")}") + appendLine("stream_rs_redundant=${getInt("chat.stream_rs_redundant")}") appendLine("call_jitter_max_reserve_ms=${getInt("chat.call_jitter_max_reserve_ms")}") appendLine("call_video_bitrate_range_kbps=${getCallVideoBitrateRange().encode()}") appendLine("compressor_enabled=$chatCompEnabled") diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt index 05faae39..675a7556 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt @@ -73,7 +73,7 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = { var dirty by rememberSaveable { mutableStateOf(false) } var tabIndex by rememberSaveable { mutableStateOf(0) } - val tabs = listOf(R.string.settings, R.string.audio, R.string.storage, R.string.radio, R.string.dev) + val tabs = listOf(R.string.settings, R.string.audio, R.string.storage, R.string.radio, R.string.broadcast, R.string.dev) val handleBack = { if (dirty && NativeLib.isReady()) { @@ -106,12 +106,64 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = { 1 -> AudioSettingsTab(vm) 2 -> StorageSettingsTab() 3 -> RadioSettingsTab() - 4 -> DeveloperSettingsTab(vm, onDirty = { dirty = it }, onExit) + 4 -> StreamSettingsTab(vm) + 5 -> DeveloperSettingsTab(vm, onDirty = { dirty = it }, onExit) } } } } +/* Параметры RS сохраняются для следующей эпохи трансляции. */ +@Composable +private fun StreamSettingsTab(vm: ChatViewModel) { + val provider = ChatApplication.instance.configProvider + var subblocks by rememberSaveable { mutableStateOf(provider.getInt("chat.stream_subblocks").toString()) } + var redundant by rememberSaveable { mutableStateOf(provider.getInt("chat.stream_rs_redundant").toString()) } + var saving by remember { mutableStateOf(false) } + var failed by remember { mutableStateOf(false) } + var saved by remember { mutableStateOf(false) } + val n = subblocks.toIntOrNull() + val r = redundant.toIntOrNull() + val valid = n != null && n in 1..255 && r != null && r in 0 until n + Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(stringResource(R.string.broadcast), style = MaterialTheme.typography.titleMedium) + OutlinedTextField( + value = subblocks, onValueChange = { subblocks = it; saved = false }, enabled = !saving, + label = { Text(stringResource(R.string.stream_subblocks)) }, singleLine = true, + isError = n == null || n !in 1..255, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + ) + OutlinedTextField( + value = redundant, onValueChange = { redundant = it; saved = false }, enabled = !saving, + label = { Text(stringResource(R.string.stream_rs_redundant)) }, singleLine = true, + isError = r == null || r < 0 || n == null || r >= n, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + ) + Text(stringResource(R.string.stream_settings_hint)) + if (!valid) Text(stringResource(R.string.stream_settings_invalid), color = MaterialTheme.colorScheme.error) + Button(enabled = valid && !saving, onClick = { + val total = n ?: return@Button + val parity = r ?: return@Button + saving = true; failed = false; saved = false + vm.viewModelScope.launch { + try { + provider.saveStreamSettings(total, parity) + NativeLib.setChatSetting("stream_subblocks", total.toString()) + NativeLib.setChatSetting("stream_rs_redundant", parity.toString()) + saved = true + } catch (e: IOException) { + failed = true + LogManager.addLog("ERROR", "STREAM", "Broadcast settings save failed: $e") + } finally { + saving = false + } + } + }) { Text(stringResource(R.string.save)) } + if (failed) Text(stringResource(R.string.stream_settings_save_failed), color = MaterialTheme.colorScheme.error) + if (saved) Text(stringResource(R.string.stream_settings_saved)) + } +} + /* Основные настройки профиля и приложения. */ @Composable diff --git a/tools/chatgui-android/app/src/main/res/values-ru/strings.xml b/tools/chatgui-android/app/src/main/res/values-ru/strings.xml index d91efc3e..8b6aba42 100644 --- a/tools/chatgui-android/app/src/main/res/values-ru/strings.xml +++ b/tools/chatgui-android/app/src/main/res/values-ru/strings.xml @@ -479,4 +479,11 @@ Нет совместимой камеры или видеокодировщика Поворот: %1$d° Верх + Трансляция + Всего субблоков + Избыточных субблоков RS + Общее число включает избыточные субблоки. Можно указать 0 избыточных. Изменения применяются при следующем запуске трансляции. + Всего субблоков: 1–255. Избыточных: от 0 до общего числа минус один. + Не удалось сохранить настройки трансляции. + Сохранено. Настройки применятся при следующем запуске трансляции. diff --git a/tools/chatgui-android/app/src/main/res/values/strings.xml b/tools/chatgui-android/app/src/main/res/values/strings.xml index 6ec1110d..9dc79a0a 100644 --- a/tools/chatgui-android/app/src/main/res/values/strings.xml +++ b/tools/chatgui-android/app/src/main/res/values/strings.xml @@ -478,4 +478,11 @@ No compatible camera or video encoder found Rotation: %1$d° Top + Broadcast + Total subblocks + Redundant RS subblocks + The total includes redundant subblocks. Zero redundancy is allowed. Changes apply to the next broadcast. + Total subblocks: 1–255. Redundant subblocks: 0 to total minus one. + Failed to save broadcast settings. + Saved. Settings apply to the next broadcast. diff --git a/tools/chatgui/src/settingsdialog.cpp b/tools/chatgui/src/settingsdialog.cpp index d50d3534..120795d9 100644 --- a/tools/chatgui/src/settingsdialog.cpp +++ b/tools/chatgui/src/settingsdialog.cpp @@ -35,6 +35,8 @@ extern "C" { #include #include #include +#include +#include SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNode* node, AudioRecorder* recorder, QWidget* parent) @@ -67,6 +69,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Sound")); m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Audio")); m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Radio")); + m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Broadcast")); mainLayout->addWidget(m_categoryList); // --- Right panel: stacked pages --- @@ -217,6 +220,27 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod m_radioPage = new RadioSettingsPage(db, radioScroll); radioScroll->setWidget(m_radioPage); m_pages->addWidget(radioScroll); + auto* streamPage = new QWidget(m_pages); + auto* streamForm = new QFormLayout(streamPage); + m_streamSubblocks = new QSpinBox(streamPage); + m_streamSubblocks->setObjectName(QStringLiteral("streamSubblocks")); + m_streamSubblocks->setRange(1, 255); + m_streamRedundant = new QSpinBox(streamPage); + m_streamRedundant->setObjectName(QStringLiteral("streamRedundant")); + QSettings streamConfig(m_configPath, QSettings::IniFormat); + m_streamSubblocks->setValue(streamConfig.value("chatserver/stream_subblocks", 8).toInt()); + m_streamRedundant->setRange(0, m_streamSubblocks->value() - 1); + m_streamRedundant->setValue(streamConfig.value("chatserver/stream_rs_redundant", 2).toInt()); + connect(m_streamSubblocks, QOverload::of(&QSpinBox::valueChanged), this, [this](int n) { + m_streamRedundant->setMaximum(n - 1); + }); + streamForm->addRow(QCoreApplication::translate("ChatGui", "Total subblocks:"), m_streamSubblocks); + streamForm->addRow(QCoreApplication::translate("ChatGui", "Redundant RS subblocks:"), m_streamRedundant); + auto* streamNote = new QLabel(QCoreApplication::translate("ChatGui", + "The total includes redundant subblocks. Zero redundancy is allowed. Changes apply to the next broadcast."), streamPage); + streamNote->setWordWrap(true); + streamForm->addRow(streamNote); + m_pages->addWidget(streamPage); rightLayout->addWidget(m_pages, 1); // --- Bottom buttons --- @@ -341,6 +365,27 @@ void SettingsDialog::onSave() { } } + for (const auto& setting : {qMakePair("stream_subblocks", m_streamSubblocks->value()), + qMakePair("stream_rs_redundant", m_streamRedundant->value())}) { + if (!config_update_option(m_configPath, "chatserver", setting.first, QString::number(setting.second))) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Broadcast settings: failed to save %s", setting.first); + QMessageBox::warning(this, QCoreApplication::translate("ChatGui", "Broadcast"), + QCoreApplication::translate("ChatGui", "Failed to save broadcast settings. Check the configuration file.")); + return; + } + auto* arg = static_cast(u_calloc(1, sizeof(chat_setting_arg))); + if (!arg) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Broadcast settings: request allocation failed"); + return; + } + arg->inst = gui_bridge_get_inst(); + const size_t keyLength = strlen(setting.first) + 1; + memcpy(arg->data, setting.first, keyLength); + snprintf(arg->data + keyLength, sizeof(arg->data) - keyLength, "%d", setting.second); + gui_bridge_post_uasync_fn(chat_core_set_setting_trampoline, arg); + } + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Broadcast settings saved n=%d r=%d; apply on next start", + m_streamSubblocks->value(), m_streamRedundant->value()); m_networkPage->saveToConfig(m_configPath); /* save channel sort preference to ui_state */ diff --git a/tools/chatgui/src/settingsdialog.h b/tools/chatgui/src/settingsdialog.h index aecdf30d..abed3197 100644 --- a/tools/chatgui/src/settingsdialog.h +++ b/tools/chatgui/src/settingsdialog.h @@ -10,6 +10,7 @@ #include #include +class QSpinBox; class NetworkSettingsPage; class DatabaseSettingsPage; class StatusPage; @@ -46,6 +47,8 @@ private: QComboBox* m_languageCombo; QComboBox* m_joinPolicyCombo; QCheckBox* m_autoConnectCheck; + QSpinBox* m_streamSubblocks; + QSpinBox* m_streamRedundant; QString m_oldNickname; NetworkSettingsPage* m_networkPage; DatabaseSettingsPage* m_databasePage; diff --git a/tools/chatgui/translations/chatgui_en.ts b/tools/chatgui/translations/chatgui_en.ts index 7ee7d87a..532bfd0c 100644 --- a/tools/chatgui/translations/chatgui_en.ts +++ b/tools/chatgui/translations/chatgui_en.ts @@ -3,6 +3,12 @@ ChatGui + BroadcastBroadcast + Total subblocks:Total subblocks: + Redundant RS subblocks:Redundant RS subblocks: + The total includes redundant subblocks. Zero redundancy is allowed. Changes apply to the next broadcast.The total includes redundant subblocks. Zero redundancy is allowed. Changes apply to the next broadcast. + Failed to save broadcast settings. Check the configuration file.Failed to save broadcast settings. Check the configuration file. + Call settingsCall settings Show videoShow video Video — %1Video — %1 diff --git a/tools/chatgui/translations/chatgui_ru.ts b/tools/chatgui/translations/chatgui_ru.ts index a8345481..9207c7d4 100644 --- a/tools/chatgui/translations/chatgui_ru.ts +++ b/tools/chatgui/translations/chatgui_ru.ts @@ -3,6 +3,12 @@ ChatGui + BroadcastТрансляция + Total subblocks:Всего субблоков: + Redundant RS subblocks:Избыточных субблоков RS: + The total includes redundant subblocks. Zero redundancy is allowed. Changes apply to the next broadcast.Общее число включает избыточные субблоки. Можно указать 0 избыточных. Изменения применяются при следующем запуске трансляции. + Failed to save broadcast settings. Check the configuration file.Не удалось сохранить настройки трансляции. Проверьте файл конфигурации. + Call settingsНастройки звонка Show videoПоказать видео Video — %1Видео — %1