Add broadcast RS settings to desktop and Android
This commit is contained in:
1 parent
6cdc2ce4dc
commit
700034eec8
12 files changed
+212
-12
No files matched your search
@@ -77,6 +77,10 @@ INFO/SUBSCRIBE/REVERSE_REQ к неподписанному узлу получа
|
|||||||
|
|
||||||
Дескриптор содержит ключ трансляции, n, r и максимальный размер RS-кодограммы block_size.
|
Дескриптор содержит ключ трансляции, n, r и максимальный размер RS-кодограммы block_size.
|
||||||
Параметры задаёт только источник и не меняет в пределах source_epoch; изменение создаёт новый запуск.
|
Параметры задаёт только источник и не меняет в пределах source_epoch; изменение создаёт новый запуск.
|
||||||
|
В настройках desktop/Android раздел «Трансляция» сохраняет общее число субблоков и число избыточных RS.
|
||||||
|
В [chatserver] это stream_subblocks (1..255, по умолчанию 8) и stream_rs_redundant (0..n-1, по умолчанию 2).
|
||||||
|
Медиапубликатор читает параметры при запуске и один раз выделяет рабочий буфер по ёмкости пачки,
|
||||||
|
но не больше предела медиаблока 65536 байт. Изменение настроек действует только со следующего запуска.
|
||||||
Дескриптор подтверждается подписанной опцией источника; поля FEC также входят в подписанную пачку.
|
Дескриптор подтверждается подписанной опцией источника; поля FEC также входят в подписанную пачку.
|
||||||
Ограничения кодека: 1 <= n <= 255, 0 <= r < n, 10 <= block_size <= 4096.
|
Ограничения кодека: 1 <= n <= 255, 0 <= r < n, 10 <= block_size <= 4096.
|
||||||
Кроме допустимости RS проверяются расход памяти и место для подписанных метаданных, подписи и хотя бы одного байта содержимого.
|
Кроме допустимости RS проверяются расход памяти и место для подписанных метаданных, подписи и хотя бы одного байта содержимого.
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ struct chat_setting_def {
|
|||||||
/* ─── реестр всех известных настроек ─── */
|
/* ─── реестр всех известных настроек ─── */
|
||||||
|
|
||||||
static const struct chat_setting_def g_setting_defs[] = {
|
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},
|
{"storage_autoload", CHAT_SETTING_BOOL, 1, 0, 1},
|
||||||
{"opus_codec_preset", CHAT_SETTING_INT, 1, 0, 4},
|
{"opus_codec_preset", CHAT_SETTING_INT, 1, 0, 4},
|
||||||
{"radio_opus_mode", CHAT_SETTING_INT, 0, 0, 1},
|
{"radio_opus_mode", CHAT_SETTING_INT, 0, 0, 1},
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ struct stream_publisher {
|
|||||||
struct stream_media_format format;
|
struct stream_media_format format;
|
||||||
struct stream_publisher_status status;
|
struct stream_publisher_status status;
|
||||||
struct publisher_queue video, audio;
|
struct publisher_queue video, audio;
|
||||||
|
uint8_t *wire;
|
||||||
|
size_t capacity;
|
||||||
uint64_t group, next_frame, last_pts[2], drain_until, report_us;
|
uint64_t group, next_frame, last_pts[2], drain_until, report_us;
|
||||||
int seen[2], stopping, audio_enabled, audio_dirty;
|
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);
|
if (p->stats_timer) uasync_cancel_timeout(p->inst->ua, p->stats_timer);
|
||||||
p->timer = p->stats_timer = NULL;
|
p->timer = p->stats_timer = NULL;
|
||||||
if (p->handle) stream_close(p->handle);
|
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->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.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;
|
p->status.video_bytes = p->status.audio_bytes = p->status.total_bytes = 0;
|
||||||
@@ -121,9 +124,9 @@ static void publisher_stats(void *arg) {
|
|||||||
/* Упаковка общая для обеих платформ: Opus целиком, видео с ограниченными продолжениями. */
|
/* Упаковка общая для обеих платформ: Opus целиком, видео с ограниченными продолжениями. */
|
||||||
static int publisher_block(struct stream_publisher *p) {
|
static int publisher_block(struct stream_publisher *p) {
|
||||||
struct stream_media_block block = {.format = p->format};
|
struct stream_media_block block = {.format = p->format};
|
||||||
uint8_t wire[STREAM_DEFAULT_INPUT_CAPACITY];
|
uint8_t *wire = p->wire;
|
||||||
size_t capacity = stream_publish_capacity(p->handle), used = STREAM_MEDIA_HEADER_SIZE;
|
size_t capacity = p->capacity, used = STREAM_MEDIA_HEADER_SIZE;
|
||||||
if (capacity > sizeof(wire) || capacity < 256) {
|
if (!wire || capacity < 256) {
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "publisher invalid capacity=%zu", capacity); return STREAM_ERR_STATE;
|
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) {
|
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;
|
struct stream_publisher *p = arg;
|
||||||
pthread_mutex_lock(&p->lock); p->post = NULL; p->timer = NULL;
|
pthread_mutex_lock(&p->lock); p->post = NULL; p->timer = NULL;
|
||||||
if (p->status.state == STREAM_PUBLISHER_OPENING) {
|
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);
|
if (result == STREAM_OK) result = stream_media_enable(p->handle, 0);
|
||||||
struct stream_info info;
|
struct stream_info info;
|
||||||
if (result == STREAM_OK) result = stream_get_info(p->handle, &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->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");
|
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); }
|
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,
|
(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();
|
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;
|
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;
|
if (p->audio.head && p->audio.head->created < oldest) oldest = p->audio.head->created;
|
||||||
int immediate = p->video.head && p->video.head->key;
|
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;
|
&& now - oldest < 20000) break;
|
||||||
int result = publisher_block(p);
|
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; }
|
if (result < 0) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "publisher block failed result=%d", result); publisher_close(p, STREAM_CLOSE_FAILED); break; }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* Android actor: локальный preview, очередь, stop/FIN, отменённый open и detach concurrent producer. */
|
/* Android actor: локальный preview, очередь, stop/FIN, отменённый open и detach concurrent producer. */
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@
|
|||||||
#include "../lib/sqlite3.h"
|
#include "../lib/sqlite3.h"
|
||||||
#include "../src/chat/chat_core.h"
|
#include "../src/chat/chat_core.h"
|
||||||
#include "../src/chat/chat_event.h"
|
#include "../src/chat/chat_event.h"
|
||||||
|
#include "../src/chat/chat_setting.h"
|
||||||
#include "../src/stream/stream.h"
|
#include "../src/stream/stream.h"
|
||||||
#include "../src/stream/stream_member.h"
|
#include "../src/stream/stream_member.h"
|
||||||
#include "../src/utun_instance.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);
|
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);
|
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,
|
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));
|
CHECK(inst && !utun_core_start(inst) && !chat_service_start(inst));
|
||||||
chat_core_create_channel_auto(inst, "Stream \"test\" 😀");
|
chat_core_create_channel_auto(inst, "Stream \"test\" 😀");
|
||||||
sqlite3_stmt *statement = NULL;
|
sqlite3_stmt *statement = NULL;
|
||||||
@@ -80,11 +82,15 @@ int main(void) {
|
|||||||
CHECK(source_events == 2 && strstr(source_snapshot, "epoch"));
|
CHECK(source_events == 2 && strstr(source_snapshot, "epoch"));
|
||||||
struct stream_member_state member;
|
struct stream_member_state member;
|
||||||
CHECK(stream_member_load(inst, group, inst->node_id, &member, NULL) == STREAM_OK && member.published);
|
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);
|
CHECK(android_stream_set_audio_enabled(1, 0) == 0); test_clock_poll(ua, 100);
|
||||||
struct stream_member_state muted;
|
struct stream_member_state muted;
|
||||||
CHECK(stream_member_load(inst, group, inst->node_id, &muted, NULL) == STREAM_OK && !muted.audio_enabled);
|
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(2, 1) < 0);
|
||||||
CHECK(android_stream_set_audio_enabled(1, 1) == 0); test_clock_poll(ua, 100);
|
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);
|
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, "[]"));
|
CHECK(!strcmp(source_snapshot, "[]"));
|
||||||
current_format.rotation = 0; current_format.audio_codec = 0;
|
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(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);
|
CHECK(android_stream_set_audio_enabled(3, 1) < 0);
|
||||||
uint8_t *large = u_malloc(500000); CHECK(large); memset(large, 1, 500000);
|
uint8_t *large = u_malloc(500000); CHECK(large); memset(large, 1, 500000);
|
||||||
CHECK(feed(3, 1, 500000, 1, large, 500000) == 1); u_free(large);
|
CHECK(feed(3, 1, 500000, 1, large, 500000) == 1); u_free(large);
|
||||||
@@ -195,6 +203,34 @@ int main(void) {
|
|||||||
android_stream_attach(inst);
|
android_stream_attach(inst);
|
||||||
CHECK(android_stream_open(4, group, 0, 1280, 720, 0, 0, 0, STREAM_MEDIA_H264) == 0); android_stream_stop(4);
|
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));
|
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);
|
android_stream_detach(inst); uasync_mark_stopped(ua); utun_instance_destroy(inst); uasync_destroy(ua, 0);
|
||||||
u_free(source_snapshot);
|
u_free(source_snapshot);
|
||||||
CHECK(u_get_allocated_count() == before);
|
CHECK(u_get_allocated_count() == before);
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ class ConfigProvider(private val context: Context) {
|
|||||||
private val chatStorageAutoloadMaxsizeMb = intPreferencesKey("chat_storage_autoload_maxsize_mb")
|
private val chatStorageAutoloadMaxsizeMb = intPreferencesKey("chat_storage_autoload_maxsize_mb")
|
||||||
private val chatStorageMaxsizeGb = intPreferencesKey("chat_storage_maxsize_gb")
|
private val chatStorageMaxsizeGb = intPreferencesKey("chat_storage_maxsize_gb")
|
||||||
private val chatOpusCodecPreset = intPreferencesKey("chat_opus_codec_preset")
|
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 callJitterMaxReserveMs = intPreferencesKey("call_jitter_max_reserve_ms")
|
||||||
private val callVideoBitrateRange = stringPreferencesKey("call_video_bitrate_range_kbps")
|
private val callVideoBitrateRange = stringPreferencesKey("call_video_bitrate_range_kbps")
|
||||||
private val chatCompressorEnabled = intPreferencesKey("chat_compressor_enabled")
|
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")
|
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 {
|
fun getString(key: String): String? = runBlocking {
|
||||||
when {
|
when {
|
||||||
key == "node.private_key" -> readKeysFromFile()?.second ?: context.dataStore.data.first()[nodePrivateKey]
|
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_autoload_maxsize_mb" -> context.dataStore.data.first()[chatStorageAutoloadMaxsizeMb] ?: 10
|
||||||
key == "chat.storage_maxsize_gb" -> context.dataStore.data.first()[chatStorageMaxsizeGb] ?: 1
|
key == "chat.storage_maxsize_gb" -> context.dataStore.data.first()[chatStorageMaxsizeGb] ?: 1
|
||||||
key == "chat.opus_codec_preset" -> context.dataStore.data.first()[chatOpusCodecPreset] ?: 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.call_jitter_max_reserve_ms" -> context.dataStore.data.first()[callJitterMaxReserveMs] ?: 1000
|
||||||
key == "chat.compressor_enabled" -> context.dataStore.data.first()[chatCompressorEnabled] ?: 1
|
key == "chat.compressor_enabled" -> context.dataStore.data.first()[chatCompressorEnabled] ?: 1
|
||||||
key == "chat.compressor_max_gain_db" -> context.dataStore.data.first()[chatCompressorMaxGainDb] ?: 15
|
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_capture_stereo=$radioCaptureStereo")
|
||||||
appendLine("radio_compressor_enabled=$radioCompEnabled")
|
appendLine("radio_compressor_enabled=$radioCompEnabled")
|
||||||
appendLine("radio_compressor_max_gain_db=$radioCompMaxGain")
|
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_jitter_max_reserve_ms=${getInt("chat.call_jitter_max_reserve_ms")}")
|
||||||
appendLine("call_video_bitrate_range_kbps=${getCallVideoBitrateRange().encode()}")
|
appendLine("call_video_bitrate_range_kbps=${getCallVideoBitrateRange().encode()}")
|
||||||
appendLine("compressor_enabled=$chatCompEnabled")
|
appendLine("compressor_enabled=$chatCompEnabled")
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = {
|
|||||||
var dirty by rememberSaveable { mutableStateOf(false) }
|
var dirty by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
var tabIndex by rememberSaveable { mutableStateOf(0) }
|
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 = {
|
val handleBack = {
|
||||||
if (dirty && NativeLib.isReady()) {
|
if (dirty && NativeLib.isReady()) {
|
||||||
@@ -106,12 +106,64 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = {
|
|||||||
1 -> AudioSettingsTab(vm)
|
1 -> AudioSettingsTab(vm)
|
||||||
2 -> StorageSettingsTab()
|
2 -> StorageSettingsTab()
|
||||||
3 -> RadioSettingsTab()
|
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
|
@Composable
|
||||||
|
|||||||
@@ -479,4 +479,11 @@
|
|||||||
<string name="stream_no_modes">Нет совместимой камеры или видеокодировщика</string>
|
<string name="stream_no_modes">Нет совместимой камеры или видеокодировщика</string>
|
||||||
<string name="stream_orientation">Поворот: %1$d°</string>
|
<string name="stream_orientation">Поворот: %1$d°</string>
|
||||||
<string name="stream_top">Верх</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>
|
</resources>
|
||||||
@@ -478,4 +478,11 @@
|
|||||||
<string name="stream_no_modes">No compatible camera or video encoder found</string>
|
<string name="stream_no_modes">No compatible camera or video encoder found</string>
|
||||||
<string name="stream_orientation">Rotation: %1$d°</string>
|
<string name="stream_orientation">Rotation: %1$d°</string>
|
||||||
<string name="stream_top">Top</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>
|
</resources>
|
||||||
@@ -35,6 +35,8 @@ extern "C" {
|
|||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNode* node,
|
SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNode* node,
|
||||||
AudioRecorder* recorder, QWidget* parent)
|
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", "Sound"));
|
||||||
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Audio"));
|
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Audio"));
|
||||||
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Radio"));
|
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Radio"));
|
||||||
|
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Broadcast"));
|
||||||
mainLayout->addWidget(m_categoryList);
|
mainLayout->addWidget(m_categoryList);
|
||||||
|
|
||||||
// --- Right panel: stacked pages ---
|
// --- Right panel: stacked pages ---
|
||||||
@@ -217,6 +220,27 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
|
|||||||
m_radioPage = new RadioSettingsPage(db, radioScroll);
|
m_radioPage = new RadioSettingsPage(db, radioScroll);
|
||||||
radioScroll->setWidget(m_radioPage);
|
radioScroll->setWidget(m_radioPage);
|
||||||
m_pages->addWidget(radioScroll);
|
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);
|
rightLayout->addWidget(m_pages, 1);
|
||||||
|
|
||||||
// --- Bottom buttons ---
|
// --- 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);
|
m_networkPage->saveToConfig(m_configPath);
|
||||||
|
|
||||||
/* save channel sort preference to ui_state */
|
/* save channel sort preference to ui_state */
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
class QSpinBox;
|
||||||
class NetworkSettingsPage;
|
class NetworkSettingsPage;
|
||||||
class DatabaseSettingsPage;
|
class DatabaseSettingsPage;
|
||||||
class StatusPage;
|
class StatusPage;
|
||||||
@@ -46,6 +47,8 @@ private:
|
|||||||
QComboBox* m_languageCombo;
|
QComboBox* m_languageCombo;
|
||||||
QComboBox* m_joinPolicyCombo;
|
QComboBox* m_joinPolicyCombo;
|
||||||
QCheckBox* m_autoConnectCheck;
|
QCheckBox* m_autoConnectCheck;
|
||||||
|
QSpinBox* m_streamSubblocks;
|
||||||
|
QSpinBox* m_streamRedundant;
|
||||||
QString m_oldNickname;
|
QString m_oldNickname;
|
||||||
NetworkSettingsPage* m_networkPage;
|
NetworkSettingsPage* m_networkPage;
|
||||||
DatabaseSettingsPage* m_databasePage;
|
DatabaseSettingsPage* m_databasePage;
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
<TS version="2.1" language="en_US" sourcelanguage="en_US">
|
<TS version="2.1" language="en_US" sourcelanguage="en_US">
|
||||||
<context>
|
<context>
|
||||||
<name>ChatGui</name>
|
<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>Call settings</source><translation>Call settings</translation></message>
|
||||||
<message><source>Show video</source><translation>Show video</translation></message>
|
<message><source>Show video</source><translation>Show video</translation></message>
|
||||||
<message><source>Video — %1</source><translation>Video — %1</translation></message>
|
<message><source>Video — %1</source><translation>Video — %1</translation></message>
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
<TS version="2.1" language="ru_RU" sourcelanguage="en_US">
|
<TS version="2.1" language="ru_RU" sourcelanguage="en_US">
|
||||||
<context>
|
<context>
|
||||||
<name>ChatGui</name>
|
<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>Call settings</source><translation>Настройки звонка</translation></message>
|
||||||
<message><source>Show video</source><translation>Показать видео</translation></message>
|
<message><source>Show video</source><translation>Показать видео</translation></message>
|
||||||
<message><source>Video — %1</source><translation>Видео — %1</translation></message>
|
<message><source>Video — %1</source><translation>Видео — %1</translation></message>
|
||||||
|
|||||||
Reference in new issue
Block a user