diff --git a/tests/test_stream_service.c b/tests/test_stream_service.c index 8763bd5c..d96ce025 100644 --- a/tests/test_stream_service.c +++ b/tests/test_stream_service.c @@ -9,6 +9,7 @@ #include "../src/stream/stream.h" #include "../src/stream/stream_member.h" #include "../src/stream/stream_priv.h" +#include "../src/radio/radio.h" #include "../src/chat/chat_core.h" #include "../src/chat/merkle_tree.h" #include "../src/routing_layer/topo_node_sqlite.h" @@ -1011,7 +1012,7 @@ static void test_profile_atomicity(void) { uint64_t before_version, after_version; profile(0, GROUP1, before, before_signature, &before_version); root_hash(0, GROUP1, before_hash); memset(huge, 'p', MS_USERINFO_MAX); huge[MS_USERINFO_MAX] = 0; - struct chat_profile_option options[] = {{"stream_source", "0000000000000001:8:2:1189"}, {"stream_subscriptions", huge}}; + struct chat_profile_option options[] = {{"stream_source", "0000000000000001:8:2:1189:0"}, {"stream_subscriptions", huge}}; CHECK(chat_core_set_my_profile_options(nodes[0], "710001", options, 2) < 0); profile(0, GROUP1, after, after_signature, &after_version); root_hash(0, GROUP1, after_hash); CHECK(!strcmp(before, after) && before_version == after_version && !memcmp(before_signature, after_signature, 64)); @@ -1385,21 +1386,22 @@ static void test_supernode_relay(void) { /* Канонический список исключает неоднозначные IDs, дубли и обрезание значений. */ static void test_member_format(void) { struct stream_member_state state = {.source = {.group_id = GROUP1, .source_node_id = 1, .source_epoch = 2, - .n = 8, .r = 2, .block_size = STREAM_RS_DEFAULT_SIZE}, .published = 1, .count = 2, + .n = 8, .r = 2, .block_size = STREAM_RS_DEFAULT_SIZE}, .published = 1, .audio_enabled = 1, .count = 2, .subscriptions = {{1, 2}, {UINT64_MAX, UINT64_MAX, 1}}}; char source[MS_USERINFO_MAX + 1], subscriptions[MS_USERINFO_MAX + 1], json[2 * MS_USERINFO_MAX + 64]; CHECK(stream_member_format(&state, source, subscriptions) == STREAM_OK); snprintf(json, sizeof(json), "{\"stream_source\":\"%s\",\"stream_subscriptions\":\"%s\"}", source, subscriptions); struct stream_member_state parsed; CHECK(stream_member_parse(GROUP1, 1, json, &parsed) == STREAM_OK); - CHECK(parsed.published && parsed.source.source_epoch == 2 && parsed.count == 2); + CHECK(parsed.published && parsed.audio_enabled && parsed.source.source_epoch == 2 && parsed.count == 2); CHECK(parsed.subscriptions[1].source_node_id == UINT64_MAX && parsed.subscriptions[1].source_epoch == UINT64_MAX); CHECK(!parsed.subscriptions[0].relay_only && parsed.subscriptions[1].relay_only); const char *bad[] = { "{\"stream_subscriptions\":\"1:2\"}", "{\"stream_subscriptions\":\"0000000000000001:0000000000000002,\"}", "{\"stream_subscriptions\":\"0000000000000001:0000000000000002,0000000000000001:0000000000000002\"}", - "{\"stream_source\":\"0000000000000002:256:2:1189\"}", + "{\"stream_source\":\"0000000000000002:256:2:1189:1\"}", + "{\"stream_source\":\"0000000000000002:8:2:1189:2\",\"stream_subscriptions\":\"0000000000000001:0000000000000002\"}", "{\"stream_source\":\"0000000000000002:8:2:1189\"}", "{\"stream_source\":\"\",\"stream_source\":\"\"}" }; @@ -1408,6 +1410,62 @@ static void test_member_format(void) { DEBUG_INFO(DEBUG_CATEGORY_STREAM, "[PASS] member format: full 64-bit IDs, canonical lists, duplicate/overflow rejection"); } +static void wait_source_audio(uint64_t epoch, int enabled) { + uint64_t deadline = get_time_tb() + 100000; + for (;;) { + struct stream_member_state state; + if (stream_member_load(nodes[1], GROUP1, nodes[0]->node_id, &state, NULL) == STREAM_OK + && state.published && state.source.source_epoch == epoch && state.audio_enabled == enabled) return; + CHECK(get_time_tb() < deadline); test_clock_poll(ua, 100); + } +} + +static void test_viewer_radio_mute(void) { + struct STREAM_HANDLE *publisher = NULL, *raw = NULL, *relay = NULL, *first = NULL, *second = NULL; + struct stream_info info; + uint64_t source = nodes[0]->node_id; + CHECK(stream_publish_open(nodes[0], GROUP1, NULL, &publisher) == STREAM_OK); + CHECK(stream_publish_set_audio_enabled(publisher, 1) == STREAM_OK); + CHECK(stream_get_info(publisher, &info) == STREAM_OK); + uint64_t epoch = info.descriptor.source_epoch; + wait_source_audio(epoch, 1); + CHECK(!radio_member_silenced(nodes[1], GROUP1, source)); + CHECK(stream_relay_open(nodes[1], &info.descriptor, &relay) == STREAM_OK); + CHECK(stream_subscribe_open(nodes[1], GROUP1, source, &raw) == STREAM_OK); + CHECK(!radio_member_silenced(nodes[1], GROUP1, source)); + CHECK(stream_media_open(nodes[1], GROUP1, source, epoch, STREAM_MEDIA_TRACK_VIDEO, &first) == STREAM_OK); + CHECK(stream_media_open(nodes[1], GROUP1, source, epoch, STREAM_MEDIA_TRACK_VIDEO | STREAM_MEDIA_TRACK_AUDIO, &second) == STREAM_OK); + CHECK(radio_member_silenced(nodes[1], GROUP1, source) && !radio_member_muted(nodes[1], GROUP1, source)); + CHECK(!radio_member_silenced(nodes[1], GROUP2, source)); + CHECK(!radio_member_silenced(nodes[1], GROUP1, nodes[2]->node_id)); + stream_close(first); first = NULL; + CHECK(radio_member_silenced(nodes[1], GROUP1, source)); + CHECK(radio_set_member_muted(nodes[1], GROUP1, source, 1) == 0); + CHECK(stream_publish_set_audio_enabled(publisher, 0) == STREAM_OK); wait_source_audio(epoch, 0); + CHECK(!stream_radio_muted(nodes[1], GROUP1, source) && radio_member_silenced(nodes[1], GROUP1, source)); + CHECK(radio_set_member_muted(nodes[1], GROUP1, source, 0) == 0); + CHECK(!radio_member_silenced(nodes[1], GROUP1, source)); + CHECK(stream_publish_set_audio_enabled(publisher, 1) == STREAM_OK); wait_source_audio(epoch, 1); + CHECK(radio_member_silenced(nodes[1], GROUP1, source)); + stream_close(second); second = NULL; + CHECK(!radio_member_silenced(nodes[1], GROUP1, source)); + CHECK(stream_media_open(nodes[1], GROUP1, source, epoch, STREAM_MEDIA_TRACK_VIDEO, &first) == STREAM_OK); + CHECK(radio_member_silenced(nodes[1], GROUP1, source)); + stream_close(publisher); publisher = NULL; + CHECK(stream_publish_open(nodes[0], GROUP1, NULL, &publisher) == STREAM_OK); + CHECK(stream_publish_set_audio_enabled(publisher, 1) == STREAM_OK); + CHECK(stream_get_info(publisher, &info) == STREAM_OK && info.descriptor.source_epoch != epoch); + wait_source_audio(info.descriptor.source_epoch, 1); + CHECK(!stream_handle_active(first) && !radio_member_silenced(nodes[1], GROUP1, source)); + stream_close(first); stream_close(raw); stream_close(relay); first = NULL; + CHECK(stream_media_open(nodes[1], GROUP1, source, info.descriptor.source_epoch, STREAM_MEDIA_TRACK_VIDEO, &first) == STREAM_OK); + CHECK(radio_member_silenced(nodes[1], GROUP1, source)); + stream_cancel_group(nodes[1], GROUP1); + CHECK(!radio_member_silenced(nodes[1], GROUP1, source)); + stream_close(first); stream_close(publisher); + DEBUG_INFO(DEBUG_CATEGORY_STREAM, "[PASS] viewer radio mute: source audio, manual mute, group isolation, owners, relay, epoch, close"); +} + /* Пауза player не останавливает live ingest/selector; FIN сохраняет источник до дочитывания локального owner. */ static void test_delayed_media_reader(void) { struct STREAM_HANDLE *publisher = NULL, *remote = NULL, *preview = NULL; @@ -1766,6 +1824,7 @@ int main(int argc, char **argv) { for (unsigned i = 0; i < 4; i++) stream_set_event_callback(nodes[i], NULL, NULL); test_partial_bootstrap_and_end(); test_delayed_media_reader(); + test_viewer_radio_mute(); for (unsigned i = 0; i < 3; i++) topo_group_peer_close(connections[i]); for (unsigned i = 0; i < 4; i++) utun_instance_destroy(nodes[i]); uasync_destroy(ua, 1); test_clock_finish(); diff --git a/tools/chatgui/tests/test_stream_publish.cpp b/tools/chatgui/tests/test_stream_publish.cpp index f081a868..a5e78f27 100644 --- a/tools/chatgui/tests/test_stream_publish.cpp +++ b/tools/chatgui/tests/test_stream_publish.cpp @@ -172,6 +172,31 @@ private slots: gui_stream_publish_stop(id); QVERIFY(record(viewer, StreamRecordKind::End, out)); QTRY_VERIFY_WITH_TIMEOUT(ended(id), 4000); QCOMPARE(gui_stream_publish_feed(id, true, 300000, true, second, &format), -1); gui_stream_stop(viewer); } + void audioToggleKeepsEpochAndTrack() { + uint64_t id = 104; + stream_media_format format{160, 90, 0, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS_STEREO, 1}; + QVERIFY(gui_stream_publish_open(id, group, format)); QTRY_VERIFY(live(id)); + stream_publisher_status status{}; QVERIFY(gui_stream_publish_status(id, status)); QVERIFY(status.audio_enabled); + uint64_t epoch = status.epoch; + stream_member_state member{}; + QCOMPARE(stream_member_load(inst, group, inst->node_id, &member, nullptr), STREAM_OK); QVERIFY(member.audio_enabled); + QVERIFY(gui_stream_publish_audio(id, false)); + QTRY_VERIFY(gui_stream_publish_status(id, status) && !status.audio_enabled); + QCOMPARE(status.epoch, epoch); + QCOMPARE(stream_member_load(inst, group, inst->node_id, &member, nullptr), STREAM_OK); QVERIFY(!member.audio_enabled); + QVERIFY(!gui_stream_publish_audio(id + 1, true)); + QVERIFY(gui_stream_publish_audio(id, true)); + QTRY_VERIFY(gui_stream_publish_status(id, status) && status.audio_enabled); QCOMPARE(status.epoch, epoch); + auto* encoder = opus_codec_encoder_create(48000, 2); QVERIFY(encoder); + int16_t silence[1920]{}; uint8_t packet[STREAM_MEDIA_MAX_OPUS]; + int size = opus_codec_encode(encoder, silence, 960, packet, sizeof(packet)); QVERIFY(size > 0); + opus_codec_encoder_destroy(encoder); + QCOMPARE(gui_stream_publish_feed(id, false, 100000, false, QByteArray(reinterpret_cast(packet), size)), 1); + gui_stream_publish_stop(id); QVERIFY(!gui_stream_publish_audio(id, false)); QTRY_VERIFY_WITH_TIMEOUT(ended(id), 4000); + format.audio_codec = 0; + QVERIFY(gui_stream_publish_open(++id, group, format)); QTRY_VERIFY(live(id)); + QVERIFY(!gui_stream_publish_audio(id, true)); gui_stream_publish_stop(id); QTRY_VERIFY_WITH_TIMEOUT(ended(id), 4000); + } void liveSoftwareBitrateKeepsDecoder() { #ifdef HAVE_FFMPEG LiveVideoEncoder encoder; LiveVideoConfig config; config.width = config.height = 16;