#include #include #include #include #include #include #include #include #include #include namespace aethera::web::media { namespace { using Clock = std::chrono::steady_clock; void corun_until_or_throw(std::string_view operation, const std::function& completed) { std::string failure; Task_Graph coordinator{"gallery.video.test.await"}; coordinator.add("await", [&] { const auto deadline = Clock::now() + std::chrono::seconds{10}; Task_Graph::corun_until([&] { if (completed()) return true; if (Clock::now() < deadline) return false; failure = std::string{operation} + " timed out"; return true; }); }); aethera::detail::run_taskflow(coordinator); if (!failure.empty()) throw std::runtime_error(failure); } void configure_manual_pixel_delivery(const std::shared_ptr& plot) { ASSERT_TRUE(plot); ASSERT_TRUE(plot->write_prop( "frame-analysis", "pacing_mode", "manual").value( "success", false)); ASSERT_TRUE(plot->write_prop( "frame-analysis", "pixel_delivery_enabled", true).value( "success", false)); ASSERT_NO_THROW(corun_until_or_throw("manual Plot policy", [&] { const auto diagnostics = plot->diagnostics(); const auto& configuration = diagnostics.at("frame_policy").at( "configuration"); return configuration.at("mode") == "manual" && configuration.at("pixel_delivery_enabled").get(); })); } void schedule_manual_frame(const std::shared_ptr& plot, std::uint64_t correlation_sequence) { const auto now = Clock::now(); plot->schedule_render(Frame_Request{ .issued_at = now, .sequence = correlation_sequence, .time_milliseconds = std::chrono::duration( now.time_since_epoch()).count(), .width = 720, .height = 420, .source = Frame_Request_Source::immediate }); } TEST(Gallery_Video_Stream, Publication_Feedback_Task_Releases_Each_Plots_Independent_Frame) { const auto definitions = gallery_plot_definitions(); const auto found = std::ranges::find_if( definitions, [](const Plot_Definition& definition) { return definition.dimension == Plot_Dimension::two_d; }); ASSERT_NE(found, definitions.end()); auto plot_a = found->create(); auto plot_b = found->create(); configure_manual_pixel_delivery(plot_a); configure_manual_pixel_delivery(plot_b); auto stream = Gallery_Video_Stream::create({ {"plot-a", plot_a}, {"plot-b", plot_b}}); ASSERT_TRUE(stream); constexpr std::size_t frame_count{5}; std::array sequences{}; std::atomic_size_t received{}; std::atomic> outstanding{}; const auto subscription = stream->subscribe( "gallery-video-test", [&](Gallery_Stream_Frame frame) { if (!frame.video) return false; const auto index = received.fetch_add( 1, std::memory_order_acq_rel); if (index < sequences.size()) sequences[index].store( frame.video->sequence, std::memory_order_release); outstanding.store(std::move(frame.video), std::memory_order_release); return true; }, [] { return true; }, [] { return nlohmann::json::object(); }); const std::array, frame_count> schedule{ plot_a, plot_a, plot_b, plot_a, plot_b}; stream->request_taskflow_trace(1); for (std::size_t index = 0; index < schedule.size(); ++index) { const auto published_before = schedule[index]->diagnostics() .at("frame_policy").at("lifecycle") .at("published_frame_count").get(); schedule_manual_frame(schedule[index], index + 1U); ASSERT_NO_THROW(corun_until_or_throw("encoded Plot frame", [&] { return received.load(std::memory_order_acquire) > index; })); ASSERT_NO_THROW(corun_until_or_throw( "frame policy publication feedback", [&] { const auto diagnostics = schedule[index]->diagnostics(); return diagnostics.at("frame_lifecycle") == "ready" && diagnostics.at("frame_policy").at("lifecycle") .at("published_frame_count") > published_before; })); outstanding.store({}, std::memory_order_release); } EXPECT_EQ(received.load(std::memory_order_acquire), frame_count); for (std::size_t index = 0; index < frame_count; ++index) EXPECT_EQ(sequences[index].load(std::memory_order_acquire), index + 1U); const auto diagnostics = stream->diagnostics(); EXPECT_EQ(diagnostics.at("received_frame_count"), frame_count); EXPECT_EQ(diagnostics.at("processed_frame_count"), frame_count); EXPECT_EQ(diagnostics.at("encoded_frame_count"), frame_count); EXPECT_EQ(diagnostics.at("pending_frame_count"), 0U); ASSERT_NO_THROW(corun_until_or_throw("gallery Taskflow trace", [&] { return stream->taskflow_trace().value("complete", false); })); const auto trace = stream->taskflow_trace(); ASSERT_EQ(trace.at("frames").size(), 1U); const auto& graphs = trace.at("frames").front().at("graphs"); const auto graph = std::ranges::find_if(graphs, [](const auto& value) { return value.value("name", std::string{}) == "gallery.video.frame"; }); ASSERT_NE(graph, graphs.end()); const auto& nodes = graph->at("nodes"); const auto feedback = std::ranges::find_if(nodes, [](const auto& value) { return value.value("name", std::string{}) == "frame.policy.publish.feedback"; }); ASSERT_NE(feedback, nodes.end()); EXPECT_EQ(feedback->at("attributes").at("owner"), "frame_policy"); stream->unsubscribe(subscription); stream->shutdown(); } } }