Browse Source

Add broadcast RS settings to desktop and Android

master
evgeny 3 days ago
parent
commit
700034eec8
  1. 4
      doc/stream.txt
  2. 2
      src/chat/chat_setting.c
  3. 31
      src/stream/stream_publisher.c
  4. 42
      tests/test_android_stream.c
  5. 15
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt
  6. 56
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt
  7. 7
      tools/chatgui-android/app/src/main/res/values-ru/strings.xml
  8. 7
      tools/chatgui-android/app/src/main/res/values/strings.xml
  9. 45
      tools/chatgui/src/settingsdialog.cpp
  10. 3
      tools/chatgui/src/settingsdialog.h
  11. 6
      tools/chatgui/translations/chatgui_en.ts
  12. 6
      tools/chatgui/translations/chatgui_ru.ts

4
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 проверяются расход памяти и место для подписанных метаданных, подписи и хотя бы одного байта содержимого.

2
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},

31
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, &parameters, &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; }

42
tests/test_android_stream.c

@ -1,5 +1,6 @@
/* Android actor: локальный preview, очередь, stop/FIN, отменённый open и detach concurrent producer. */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -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);

15
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")

56
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

7
tools/chatgui-android/app/src/main/res/values-ru/strings.xml

@ -479,4 +479,11 @@
<string name="stream_no_modes">Нет совместимой камеры или видеокодировщика</string>
<string name="stream_orientation">Поворот: %1$d°</string>
<string name="stream_top">Верх</string>
<string name="broadcast">Трансляция</string>
<string name="stream_subblocks">Всего субблоков</string>
<string name="stream_rs_redundant">Избыточных субблоков RS</string>
<string name="stream_settings_hint">Общее число включает избыточные субблоки. Можно указать 0 избыточных. Изменения применяются при следующем запуске трансляции.</string>
<string name="stream_settings_invalid">Всего субблоков: 1–255. Избыточных: от 0 до общего числа минус один.</string>
<string name="stream_settings_save_failed">Не удалось сохранить настройки трансляции.</string>
<string name="stream_settings_saved">Сохранено. Настройки применятся при следующем запуске трансляции.</string>
</resources>

7
tools/chatgui-android/app/src/main/res/values/strings.xml

@ -478,4 +478,11 @@
<string name="stream_no_modes">No compatible camera or video encoder found</string>
<string name="stream_orientation">Rotation: %1$d°</string>
<string name="stream_top">Top</string>
<string name="broadcast">Broadcast</string>
<string name="stream_subblocks">Total subblocks</string>
<string name="stream_rs_redundant">Redundant RS subblocks</string>
<string name="stream_settings_hint">The total includes redundant subblocks. Zero redundancy is allowed. Changes apply to the next broadcast.</string>
<string name="stream_settings_invalid">Total subblocks: 1–255. Redundant subblocks: 0 to total minus one.</string>
<string name="stream_settings_save_failed">Failed to save broadcast settings.</string>
<string name="stream_settings_saved">Saved. Settings apply to the next broadcast.</string>
</resources>

45
tools/chatgui/src/settingsdialog.cpp

@ -35,6 +35,8 @@ extern "C" {
#include <QMessageBox>
#include <QFileDialog>
#include <QScrollArea>
#include <QSpinBox>
#include <QSettings>
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<int>::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<chat_setting_arg*>(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 */

3
tools/chatgui/src/settingsdialog.h

@ -10,6 +10,7 @@
#include <QCheckBox>
#include <QString>
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;

6
tools/chatgui/translations/chatgui_en.ts

@ -3,6 +3,12 @@
<TS version="2.1" language="en_US" sourcelanguage="en_US">
<context>
<name>ChatGui</name>
<message><source>Broadcast</source><translation>Broadcast</translation></message>
<message><source>Total subblocks:</source><translation>Total subblocks:</translation></message>
<message><source>Redundant RS subblocks:</source><translation>Redundant RS subblocks:</translation></message>
<message><source>The total includes redundant subblocks. Zero redundancy is allowed. Changes apply to the next broadcast.</source><translation>The total includes redundant subblocks. Zero redundancy is allowed. Changes apply to the next broadcast.</translation></message>
<message><source>Failed to save broadcast settings. Check the configuration file.</source><translation>Failed to save broadcast settings. Check the configuration file.</translation></message>
<message><source>Call settings</source><translation>Call settings</translation></message>
<message><source>Show video</source><translation>Show video</translation></message>
<message><source>Video — %1</source><translation>Video — %1</translation></message>

6
tools/chatgui/translations/chatgui_ru.ts

@ -3,6 +3,12 @@
<TS version="2.1" language="ru_RU" sourcelanguage="en_US">
<context>
<name>ChatGui</name>
<message><source>Broadcast</source><translation>Трансляция</translation></message>
<message><source>Total subblocks:</source><translation>Всего субблоков:</translation></message>
<message><source>Redundant RS subblocks:</source><translation>Избыточных субблоков RS:</translation></message>
<message><source>The total includes redundant subblocks. Zero redundancy is allowed. Changes apply to the next broadcast.</source><translation>Общее число включает избыточные субблоки. Можно указать 0 избыточных. Изменения применяются при следующем запуске трансляции.</translation></message>
<message><source>Failed to save broadcast settings. Check the configuration file.</source><translation>Не удалось сохранить настройки трансляции. Проверьте файл конфигурации.</translation></message>
<message><source>Call settings</source><translation>Настройки звонка</translation></message>
<message><source>Show video</source><translation>Показать видео</translation></message>
<message><source>Video — %1</source><translation>Видео — %1</translation></message>

Loading…
Cancel
Save