架构大改之前

This commit is contained in:
2026-08-23 17:11:12 +08:00
parent 73a3152733
commit 3938e72bb7
34 changed files with 1111 additions and 327 deletions
+2 -4
View File
@@ -136,11 +136,9 @@ target_link_libraries(Aethera_Web_Server PRIVATE
add_dependencies(Aethera_Web_Server Aethera_Web_Assets)
add_custom_command(TARGET Aethera_Web_Server POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"$<TARGET_FILE:Aethera_FFmpeg_avcodec>"
"$<TARGET_FILE:Aethera_FFmpeg_avutil>"
"$<TARGET_FILE:Aethera_FFmpeg_swscale>"
${Aethera_FFmpeg_runtime_libraries}
"$<TARGET_FILE_DIR:Aethera_Web_Server>"
COMMENT "Deploying FFmpeg runtime for Aethera WebRTC video"
COMMENT "Deploying complete FFmpeg shared runtime for Aethera WebRTC video"
VERBATIM)
if (MSVC)
target_compile_options(Aethera_Web_Server PRIVATE /utf-8 /bigobj)
+3 -3
View File
@@ -367,7 +367,7 @@ private:
};
struct Scene_Components_3D {
plot::Camera_Descriptor camera{};
Camera_Descriptor camera{};
std::array<plot::Axis_Descriptor, 3> axes{
plot::Axis_Descriptor{{-1.0, 1.0}, plot::Axis_Scale::linear, "X", "", 5, 2, true, true, true},
plot::Axis_Descriptor{{-1.0, 1.0}, plot::Axis_Scale::linear, "Y", "", 5, 2, true, true, true},
@@ -713,9 +713,9 @@ std::shared_ptr<Plot> make_datoviz_spectrogram_plot(asio::any_io_executor execut
auto marker_result = Impl<Marker_Visual>::Builder{}
.set(&Marker_Visual::Prop::items, std::vector<Marker>{
{{-0.35F, -0.18F, 0.72F}, color(255, 244, 170), 23.0F, 0.0F,
Marker_Shape::diamond, true},
Marker_Shape::diamond, false},
{{0.28F, 0.34F, 0.86F}, color(88, 236, 211), 21.0F, 0.0F,
Marker_Shape::cross, true}})
Marker_Shape::cross, false}})
.set(&Marker_Visual::Prop::depth_test, false)
.build();
if (!marker_result)
+143 -22
View File
@@ -1,5 +1,9 @@
#include "Graph_WebSocket.hpp"
#include "WebRtc_Video_Session.hpp"
#include <asio/any_io_executor.hpp>
#include <asio/post.hpp>
#include <asio/strand.hpp>
#include <asio/thread_pool.hpp>
#include <magic_enum/magic_enum.hpp>
#include <nlohmann/json.hpp>
#include <algorithm>
@@ -7,6 +11,8 @@
#include <chrono>
#include <mutex>
#include <string>
#include <thread>
#include <cctype>
namespace aethera::web {
namespace {
@@ -14,9 +20,56 @@ std::string graph_id_from_path(std::string_view path) {
const auto split = path.find_last_of('/');
return split == std::string_view::npos ? std::string{} : std::string(path.substr(split + 1));
}
struct Exclusive_Page_State {
std::mutex mutex;
std::string page;
std::size_t connection_count{};
};
Exclusive_Page_State exclusive_page;
asio::any_io_executor media_delivery_executor() {
/* rtc::Track 与 Drogon 发送可能产生媒体背压,禁止占用 Plot 编码域。
* 每个 Graph 仍通过自己的 strand 保持 H.264/RTP 顺序。 */
static asio::thread_pool pool([] {
const auto hardware = std::max(2U, std::thread::hardware_concurrency());
return std::min(8U, std::max(2U, hardware / 2U));
}());
return pool.get_executor();
}
bool valid_page_id(std::string_view page) {
return page.size() >= 16 && page.size() <= 64 &&
std::ranges::all_of(page, [](unsigned char value) {
return std::isalnum(value) || value == '-' || value == '_';
});
}
std::uint32_t video_dimension(std::uint32_t value, std::uint32_t minimum,
std::uint32_t maximum) {
const auto clamped = std::clamp(value, minimum, maximum);
return std::clamp(((clamped + 16U) / 32U) * 32U, minimum, maximum) & ~1U;
}
bool acquire_page(std::string_view page) {
std::lock_guard lock(exclusive_page.mutex);
if (exclusive_page.connection_count != 0 && exclusive_page.page != page) return false;
if (exclusive_page.connection_count == 0) exclusive_page.page = page;
++exclusive_page.connection_count;
return true;
}
void release_page(std::string_view page) {
std::lock_guard lock(exclusive_page.mutex);
if (exclusive_page.page != page || exclusive_page.connection_count == 0) return;
--exclusive_page.connection_count;
if (exclusive_page.connection_count == 0) exclusive_page.page.clear();
}
}
struct Graph_WebSocket::Private {
Private() : delivery_strand(asio::make_strand(media_delivery_executor())) {}
std::weak_ptr<drogon::WebSocketConnection> connection;
std::shared_ptr<Plot> plot;
std::unique_ptr<WebRtc_Video_Session> video;
@@ -25,37 +78,92 @@ struct Graph_WebSocket::Private {
std::mutex viewport_mutex;
std::uint32_t width{720};
std::uint32_t height{420};
std::string page_id; /* 同一网页的多图连接共享一个独占租约。 */
asio::strand<asio::any_io_executor> delivery_strand; /* 当前 Graph 唯一的媒体发送顺序域。 */
std::mutex pending_mutex;
std::shared_ptr<const Plot_Stream_Frame> pending_frame; /* 媒体背压时只保留尚未发送的最新完成帧。 */
bool delivery_scheduled{}; /* pending_frame 是否已有唯一消费任务。 */
};
Graph_WebSocket::Graph_WebSocket(drogon::WebSocketConnectionPtr connection,
std::shared_ptr<Plot> plot)
std::shared_ptr<Plot> plot, std::string page_id)
: d(std::make_unique<Private>()) {
d->connection = connection;
d->plot = std::move(plot);
d->page_id = std::move(page_id);
}
Graph_WebSocket::~Graph_WebSocket() { close(); }
void Graph_WebSocket::start() {
if (d->attached.exchange(true, std::memory_order_acq_rel)) return;
const auto weak = weak_from_this();
d->video = std::make_unique<WebRtc_Video_Session>([weak](std::string signal) {
const auto socket = weak.lock();
if (!socket) return;
const auto connection = socket->d->connection.lock();
if (connection && connection->connected())
connection->send(std::move(signal), drogon::WebSocketMessageType::Text);
});
d->stream = d->plot->subscribe([weak](std::shared_ptr<const Plot_Stream_Frame> frame) {
const auto socket = weak.lock();
if (!socket || !socket->d->attached.load(std::memory_order_acquire)) return;
const auto connection = socket->d->connection.lock();
if (!connection || !connection->connected()) return;
if (frame->video) socket->d->video->send(*frame->video);
connection->send(frame->metadata, drogon::WebSocketMessageType::Text);
});
d->video = std::make_unique<WebRtc_Video_Session>(
[weak](std::string signal) {
const auto socket = weak.lock();
if (!socket) return;
const auto connection = socket->d->connection.lock();
if (connection && connection->connected())
connection->send(std::move(signal), drogon::WebSocketMessageType::Text);
},
[weak] {
if (const auto socket = weak.lock())
socket->d->plot->request_video_key_frame();
});
d->stream = d->plot->subscribe(
[weak](std::shared_ptr<const Plot_Stream_Frame> frame) {
if (const auto socket = weak.lock())
socket->enqueue_frame(std::move(frame));
});
d->video->start();
}
void Graph_WebSocket::enqueue_frame(
std::shared_ptr<const Plot_Stream_Frame> frame) {
if (!frame || !d->attached.load(std::memory_order_acquire)) return;
bool schedule{};
{
std::lock_guard lock(d->pending_mutex);
d->pending_frame = std::move(frame);
if (!d->delivery_scheduled) {
d->delivery_scheduled = true;
schedule = true;
}
}
if (!schedule) return;
const auto weak = weak_from_this();
asio::post(d->delivery_strand, [weak] {
if (const auto socket = weak.lock()) socket->deliver_frame();
});
}
void Graph_WebSocket::deliver_frame() {
std::shared_ptr<const Plot_Stream_Frame> frame;
{
std::lock_guard lock(d->pending_mutex);
frame = std::move(d->pending_frame);
}
if (frame && d->attached.load(std::memory_order_acquire)) {
const auto connection = d->connection.lock();
if (connection && connection->connected()) {
if (frame->video)
static_cast<void>(d->video->send(*frame->video));
connection->send(frame->metadata,
drogon::WebSocketMessageType::Text);
}
}
bool schedule{};
{
std::lock_guard lock(d->pending_mutex);
if (d->pending_frame) schedule = true;
else d->delivery_scheduled = false;
}
if (!schedule) return;
const auto weak = weak_from_this();
asio::post(d->delivery_strand, [weak] {
if (const auto socket = weak.lock()) socket->deliver_frame();
});
}
void Graph_WebSocket::receive(std::string_view message) {
const auto json = nlohmann::json::parse(message, nullptr, false);
if (json.is_discarded() || !json.is_object()) return;
@@ -73,21 +181,19 @@ void Graph_WebSocket::receive(std::string_view message) {
return;
}
if (kind == "stream") {
const bool active = json.value("active", false);
const bool video = json.value("video", true);
std::uint32_t width{720};
std::uint32_t height{420};
if (const auto viewport = json.find("viewport");
viewport != json.end() && viewport->is_object()) {
width = std::clamp(viewport->value("width", 720U), 160U, 1920U);
height = std::clamp(viewport->value("height", 420U), 120U, 1080U);
width = video_dimension(viewport->value("width", 720U), 160U, 1920U);
height = video_dimension(viewport->value("height", 420U), 128U, 1080U);
}
{
std::lock_guard lock(d->viewport_mutex);
d->width = width;
d->height = height;
}
d->plot->configure_stream(d->stream, active, video, width, height);
d->plot->configure_stream(d->stream, width, height);
return;
}
if (kind == "manual_render") {
@@ -144,7 +250,12 @@ void Graph_WebSocket::receive(std::string_view message) {
void Graph_WebSocket::close() {
if (!d->attached.exchange(false, std::memory_order_acq_rel)) return;
d->plot->unsubscribe(d->stream);
{
std::lock_guard lock(d->pending_mutex);
d->pending_frame.reset();
}
if (d->video) d->video->close();
release_page(d->page_id);
}
Graph_WebSocket_Controller::Graph_WebSocket_Controller(Plot_Resolver resolver)
@@ -157,7 +268,17 @@ void Graph_WebSocket_Controller::handleNewConnection(
connection->shutdown(drogon::CloseCode::kViolation, "Unknown Aethera plot");
return;
}
auto socket = std::make_shared<Graph_WebSocket>(connection, std::move(plot));
const auto page_id = request->getParameter("page");
if (!valid_page_id(page_id) || !acquire_page(page_id)) {
connection->send(nlohmann::json{{"kind", "exclusive_page_rejected"},
{"message", "Aethera Gallery is already owned by another page instance"}}.dump(),
drogon::WebSocketMessageType::Text);
connection->shutdown(drogon::CloseCode::kViolation,
"Aethera Gallery allows one page instance");
return;
}
auto socket = std::make_shared<Graph_WebSocket>(
connection, std::move(plot), page_id);
connection->setContext(socket);
connection->setPingMessage("aethera-gallery", std::chrono::seconds(20));
socket->start();
+4 -1
View File
@@ -9,7 +9,8 @@
namespace aethera::web {
class Graph_WebSocket final : public std::enable_shared_from_this<Graph_WebSocket> {
public:
Graph_WebSocket(drogon::WebSocketConnectionPtr connection, std::shared_ptr<Plot> plot);
Graph_WebSocket(drogon::WebSocketConnectionPtr connection,
std::shared_ptr<Plot> plot, std::string page_id);
~Graph_WebSocket();
Graph_WebSocket(const Graph_WebSocket&) = delete;
Graph_WebSocket& operator=(const Graph_WebSocket&) = delete;
@@ -17,6 +18,8 @@ public:
void receive(std::string_view message);
void close();
private:
void enqueue_frame(std::shared_ptr<const Plot_Stream_Frame> frame);
void deliver_frame();
struct Private;
std::unique_ptr<Private> d;
};
+10 -4
View File
@@ -32,7 +32,7 @@ struct H264_Encoder::Private {
std::uint32_t width{}; /* 当前编码尺寸;尺寸变化时整体重建编码器。 */
std::uint32_t height{}; /* 当前编码尺寸;H.264 4:2:0 要求偶数。 */
Video_Pixel_Layout layout{Video_Pixel_Layout::bgra}; /* scaler 当前输入格式。 */
bool first_frame{true}; /* 编码器重建后必须立即产生可独立解码的关键帧。 */
bool key_frame_requested{true}; /* Track 新建或恢复时,下一 access unit 必须可独立解码。 */
explicit Private(double value_frame_rate) : frame_rate(value_frame_rate) {
if (!std::isfinite(frame_rate) || frame_rate <= 0.0)
@@ -47,7 +47,7 @@ struct H264_Encoder::Private {
avcodec_free_context(&codec_context);
width = 0;
height = 0;
first_frame = true;
key_frame_requested = true;
}
void configure(std::uint32_t next_width, std::uint32_t next_height,
Video_Pixel_Layout next_layout) {
@@ -103,6 +103,10 @@ H264_Encoder::H264_Encoder(double frame_rate)
: d(std::make_unique<Private>(frame_rate)) {}
H264_Encoder::~H264_Encoder() = default;
void H264_Encoder::request_key_frame() {
d->key_frame_requested = true;
}
std::shared_ptr<const Encoded_Video_Frame> H264_Encoder::encode(
std::span<const std::byte> pixels, std::uint32_t width,
std::uint32_t height, Video_Pixel_Layout layout,
@@ -118,7 +122,9 @@ std::shared_ptr<const Encoded_Video_Frame> H264_Encoder::encode(
d->yuv_frame->data, d->yuv_frame->linesize) != static_cast<int>(height))
throw std::runtime_error("FFmpeg did not convert the complete video frame");
d->yuv_frame->pts = presentation_time.count();
d->yuv_frame->pict_type = d->first_frame ? AV_PICTURE_TYPE_I : AV_PICTURE_TYPE_NONE;
d->yuv_frame->pict_type = d->key_frame_requested
? AV_PICTURE_TYPE_I
: AV_PICTURE_TYPE_NONE;
require_ffmpeg(avcodec_send_frame(d->codec_context, d->yuv_frame),
"submitting frame to H.264 encoder");
const int received = avcodec_receive_packet(d->codec_context, d->packet);
@@ -130,7 +136,7 @@ std::shared_ptr<const Encoded_Video_Frame> H264_Encoder::encode(
output->presentation_time = presentation_time;
output->sequence = sequence;
output->key_frame = (d->packet->flags & AV_PKT_FLAG_KEY) != 0;
d->first_frame = false;
d->key_frame_requested = false;
av_packet_unref(d->packet);
return output;
}
+1
View File
@@ -25,6 +25,7 @@ public:
~H264_Encoder();
H264_Encoder(const H264_Encoder&) = delete;
H264_Encoder& operator=(const H264_Encoder&) = delete;
void request_key_frame();
[[nodiscard]] std::shared_ptr<const Encoded_Video_Frame> encode(
std::span<const std::byte> pixels, std::uint32_t width,
std::uint32_t height, Video_Pixel_Layout layout,
+205 -83
View File
@@ -7,6 +7,7 @@
#include <asio/redirect_error.hpp>
#include <asio/steady_timer.hpp>
#include <asio/strand.hpp>
#include <asio/thread_pool.hpp>
#include <asio/use_awaitable.hpp>
#include <magic_enum/magic_enum.hpp>
#include <render_2D/plottable/Plottables.hpp>
@@ -19,6 +20,7 @@
#include <mutex>
#include <optional>
#include <stdexcept>
#include <thread>
#include <unordered_map>
#include <utility>
#include <variant>
@@ -32,6 +34,17 @@ using Scene_2D = Impl<Render_Scene_2D>;
using Scene_3D = Impl<Render_Scene_3D>;
constexpr std::uint16_t frame_protocol_version{7};
asio::any_io_executor video_encoding_executor() {
/* 编码与 Plot 帧时钟/2D 绘制是不同的 CPU 资源域。每个 Plot 的 strand
* FFmpeg Plot
* Web graph pool */
static asio::thread_pool pool([] {
const auto hardware = std::max(2U, std::thread::hardware_concurrency());
return std::min(8U, std::max(2U, hardware / 2U));
}());
return pool.get_executor();
}
enum class Frame_Pacing_Mode : std::uint8_t {
manual,
fixed_rate,
@@ -39,8 +52,10 @@ enum class Frame_Pacing_Mode : std::uint8_t {
};
struct Frame_Pacing_Properties {
bool render_enabled{true};
bool video_enabled{true};
Frame_Pacing_Mode mode{Frame_Pacing_Mode::fixed_rate}; /* 唯一职责是决定何时调用 Scene::render。 */
double fixed_rate_fps{30.0}; /* 不参与帧完成、编码或网络回调。 */
double fixed_rate_fps{100.0}; /* 不参与帧完成、编码或网络回调。 */
};
class Frame_Policy final {
@@ -78,6 +93,14 @@ nlohmann::json Frame_Policy::schema() const {
return {
{"id", "frame-analysis"}, {"label", "渲染与传输分析"}, {"kind", "analysis"},
{"fields", nlohmann::json::array({
{{"key", "render_enabled"}, {"label", "持续渲染与采样"}, {"editor", "boolean"},
{"editable", true}, {"description", "控制后端是否继续调用 Scene::render;浏览器画面隐藏不会修改此项。"},
{"technical_description", "Authoritative server-side render and sampling switch."},
{"value", pacing.render_enabled}},
{{"key", "video_enabled"}, {"label", "WebRTC 视频传输"}, {"editor", "boolean"},
{"editable", true}, {"description", "控制完成帧是否编码为 H.264 并通过 WebRTC 发送;默认开启以进行完整链路压测。"},
{"technical_description", "Authoritative server-side video encoding and delivery switch."},
{"value", pacing.video_enabled}},
{{"key", "pacing_mode"}, {"label", "服务端帧策略"}, {"editor", "select"},
{"editable", true}, {"description", "只控制服务端调用 Scene::render 的节奏;完成回调始终直接发布。"},
{"technical_description", "Server render clock policy; independent from completion and WebRTC delivery."},
@@ -98,6 +121,14 @@ nlohmann::json Frame_Policy::schema() const {
nlohmann::json Frame_Policy::write_prop(std::string_view key,
const nlohmann::json& value) {
std::lock_guard lock(mutex);
if (key == "render_enabled" || key == "video_enabled") {
if (!value.is_boolean())
return {{"success", false}, {"error", "frame policy switch requires a boolean"}};
bool& target = key == "render_enabled" ? pacing.render_enabled : pacing.video_enabled;
target = value.get<bool>();
return {{"success", true}, {"component", "frame-analysis"}, {"key", key},
{"value", target}};
}
if (key == "pacing_mode") {
if (!value.is_string()) return {{"success", false}, {"error", "pacing_mode requires a string"}};
const auto parsed = parse_pacing_mode(value.get_ref<const std::string&>());
@@ -140,7 +171,9 @@ nlohmann::json frame_metadata(Render_Frame& frame, std::uint32_t width,
{"video", {{"codec", "H264"}, {"transport", "WebRTC"},
{"encoded_bytes", encoded_bytes}}},
{"pacing", {{"mode", pacing_mode_name(pacing.mode)},
{"fixed_rate_fps", pacing.fixed_rate_fps}}},
{"fixed_rate_fps", pacing.fixed_rate_fps},
{"render_enabled", pacing.render_enabled},
{"video_enabled", pacing.video_enabled}}},
{"trace", {{"clock", "steady_elapsed_ns"}, {"markers", std::move(markers)},
{"measurements", std::move(measurements)}}}
};
@@ -206,22 +239,18 @@ struct Plot::Private {
using Scene = std::variant<std::unique_ptr<Scene_2D>, std::unique_ptr<Scene_3D>>;
using Frame = std::variant<std::unique_ptr<Frame_2D>, std::unique_ptr<Frame_3D>>;
struct Managed_Frame {
Plot_Render_Tick tick{};
std::chrono::microseconds presentation_time{}; /* Plot 帧时钟产生的媒体时间戳。 */
Frame frame{}; /* Scene 借用,Plot 保存到完成回调返回。 */
};
struct Consumer {
Stream_Handler handler;
bool render_enabled{};
bool video_enabled{};
std::uint32_t width{};
std::uint32_t height{};
};
struct Stream_Snapshot {
std::vector<Consumer> consumers;
bool render_enabled{};
bool video_enabled{};
std::uint32_t width{720};
std::uint32_t height{420};
std::uint32_t width{};
std::uint32_t height{};
};
asio::strand<asio::any_io_executor> strand;
@@ -236,28 +265,36 @@ struct Plot::Private {
std::atomic_uint64_t next_stream_id{1};
std::uint64_t next_frame_sequence{1};
Frame_Policy frame_policy{};
H264_Encoder encoder{60.0};
std::unordered_map<Render_Frame*, Managed_Frame> active_frames{};
std::size_t encoding_frames{}; /* 已离开 Scene、仍占用三缓冲槽的帧数。 */
H264_Encoder encoder{100.0};
std::atomic_uint64_t last_stream_publish_tail_ns{}; /* 最近一帧编码结束到消费者投递返回的诊断值。 */
mutable std::mutex frame_mutex; /* 三缓冲帧所有权从 Plot 时钟移交给 Scene 回调与编码域。 */
std::unordered_map<Render_Frame*, Managed_Frame> active_frames{}; /* Scene 当前借用、尚未完成回调的帧。 */
std::shared_ptr<Managed_Frame> pending_encoding_frame{}; /* 编码器忙时唯一保留的最新完成帧。 */
bool encoder_running{}; /* 当前是否有且仅有一个编码任务占用本 Plot 编码器。 */
static constexpr std::size_t scene_frame_capacity = 3; /* Scene/GPU 独立三缓冲;媒体域另有正在处理与最新待处理两个有界槽位。 */
std::chrono::steady_clock::time_point clock_origin{std::chrono::steady_clock::now()};
template <typename Scene_Object>
Private(asio::any_io_executor executor, std::unique_ptr<Scene_Object> value_scene,
std::unique_ptr<Scene_View> value_view)
: strand(asio::make_strand(executor)), encode_strand(asio::make_strand(std::move(executor))),
: strand(asio::make_strand(executor)),
encode_strand(asio::make_strand(video_encoding_executor())),
commands(strand, 32),
render_clock(strand), view(std::move(value_view)), scene(std::move(value_scene)) {}
[[nodiscard]] nlohmann::json schema() const;
[[nodiscard]] Stream_Snapshot stream_snapshot() const;
[[nodiscard]] std::size_t frame_capacity() const noexcept;
[[nodiscard]] std::chrono::steady_clock::duration clock_interval() const;
void start_clock();
void schedule_clock();
void clock_tick();
void render_frame();
void queue_completed_frame(Render_Frame* frame, std::weak_ptr<Plot> lifetime);
void schedule_encoding(std::shared_ptr<Managed_Frame> managed,
std::weak_ptr<Plot> lifetime);
void encode_and_publish(std::shared_ptr<Managed_Frame> managed,
std::weak_ptr<Plot> lifetime);
void finish_encoding(std::weak_ptr<Plot> lifetime);
};
nlohmann::json Plot::Private::schema() const {
@@ -275,10 +312,8 @@ Plot::Private::Stream_Snapshot Plot::Private::stream_snapshot() const {
result.consumers.reserve(consumers.size());
for (const auto& [id, consumer] : consumers) {
static_cast<void>(id);
if (consumer.width == 0 || consumer.height == 0) continue;
result.consumers.push_back(consumer);
if (!consumer.render_enabled) continue;
result.render_enabled = true;
result.video_enabled = result.video_enabled || consumer.video_enabled;
result.width = std::max(result.width, consumer.width);
result.height = std::max(result.height, consumer.height);
}
@@ -287,10 +322,6 @@ Plot::Private::Stream_Snapshot Plot::Private::stream_snapshot() const {
return result;
}
std::size_t Plot::Private::frame_capacity() const noexcept {
return 3U;
}
std::chrono::steady_clock::duration Plot::Private::clock_interval() const {
const auto pacing = frame_policy.snapshot();
if (pacing.mode == Frame_Pacing_Mode::manual) return std::chrono::milliseconds(100);
@@ -299,26 +330,51 @@ std::chrono::steady_clock::duration Plot::Private::clock_interval() const {
std::chrono::duration<double>(1.0 / pacing.fixed_rate_fps));
}
void Plot::Private::schedule_clock() {
void Plot::Private::start_clock() {
render_clock.expires_after(clock_interval());
schedule_clock();
}
void Plot::Private::schedule_clock() {
render_clock.async_wait([this](const asio::error_code& error) {
if (error) return;
clock_tick();
/*
* The frame rate is a timeline frequency, not a sleep duration after render work. Advance
* from the previous absolute deadline so CPU preparation cannot accumulate clock drift.
* When a deadline was missed, skip the elapsed periods instead of issuing a catch-up burst.
*/
const auto interval = clock_interval();
auto next_deadline = render_clock.expiry() + interval;
const auto now = std::chrono::steady_clock::now();
if (next_deadline <= now) {
const auto missed_periods = (now - next_deadline) / interval + 1;
next_deadline += interval * missed_periods;
}
render_clock.expires_at(next_deadline);
schedule_clock();
clock_tick();
});
}
void Plot::Private::clock_tick() {
const auto pacing = frame_policy.snapshot();
if (pacing.mode == Frame_Pacing_Mode::manual) return;
if (active_frames.size() + encoding_frames < frame_capacity() &&
stream_snapshot().render_enabled)
render_frame();
if (!pacing.render_enabled || pacing.mode == Frame_Pacing_Mode::manual) return;
{
std::lock_guard lock(frame_mutex);
/* 已完成帧属于媒体域,不能反向占用 Scene/GPU 三缓冲槽位。 */
if (active_frames.size() >= scene_frame_capacity) return;
}
if (!stream_snapshot().consumers.empty()) render_frame();
}
void Plot::Private::render_frame() {
const auto streams = stream_snapshot();
if (!streams.render_enabled || active_frames.size() + encoding_frames >= frame_capacity()) return;
const auto pacing = frame_policy.snapshot();
if (!pacing.render_enabled || streams.consumers.empty()) return;
{
std::lock_guard lock(frame_mutex);
if (active_frames.size() >= scene_frame_capacity) return;
}
const auto elapsed = std::chrono::steady_clock::now() - clock_origin;
Plot_Render_Tick tick{next_frame_sequence,
std::chrono::duration<double, std::milli>(elapsed).count(), streams.width, streams.height};
@@ -327,43 +383,77 @@ void Plot::Private::render_frame() {
view->update(tick); /* CPU 数据准备发生在 render 提交之前。 */
Managed_Frame managed;
managed.tick = tick;
managed.presentation_time = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::duration<double, std::milli>(tick.time_milliseconds));
if (std::holds_alternative<std::unique_ptr<Scene_2D>>(scene))
managed.frame = std::make_unique<Frame_2D>(identity, Frame_2D::native_pixel_format);
else
managed.frame = std::make_unique<Frame_3D>(identity,
streams.video_enabled
pacing.video_enabled
? Frame_3D_Output::pixels
: Frame_3D_Output::diagnostics,
Frame_3D::native_pixel_format);
Render_Frame* address = std::visit([](const auto& value) -> Render_Frame* { return value.get(); },
managed.frame);
auto [active, inserted] = active_frames.emplace(address, std::move(managed));
if (!inserted) throw std::logic_error("Plot received a duplicate frame address");
{
std::lock_guard lock(frame_mutex);
const auto [active, inserted] =
active_frames.emplace(address, std::move(managed));
if (!inserted)
throw std::logic_error("Plot received a duplicate frame address");
}
if (auto* scene_2d = std::get_if<std::unique_ptr<Scene_2D>>(&scene)) {
(*scene_2d)->set<&Render_Scene_2D::Prop::viewport>(
Size{static_cast<int>(tick.width), static_cast<int>(tick.height)});
const auto result = (*scene_2d)->render(
std::get<std::unique_ptr<Frame_2D>>(active->second.frame).get());
if (result != Render_Scene_2D::Render_Result::completed) active_frames.erase(address);
static_cast<Frame_2D*>(address));
if (result != Render_Scene_2D::Render_Result::completed) {
std::lock_guard lock(frame_mutex);
active_frames.erase(address);
}
return;
}
auto& scene_3d = std::get<std::unique_ptr<Scene_3D>>(scene);
scene_3d->set<&Render_Scene_3D::Prop::viewport>(Extent{tick.width, tick.height});
const auto result = scene_3d->render(
std::get<std::unique_ptr<Frame_3D>>(active->second.frame).get());
if (result != Render_Scene_3D::Render_Result::submitted) active_frames.erase(address);
static_cast<Frame_3D*>(address));
if (result != Render_Scene_3D::Render_Result::submitted) {
std::lock_guard lock(frame_mutex);
active_frames.erase(address);
}
}
void Plot::Private::queue_completed_frame(Render_Frame* frame,
std::weak_ptr<Plot> lifetime) {
const auto active = active_frames.find(frame);
if (active == active_frames.end())
throw std::logic_error("frame callback has no externally owned active frame");
auto managed = std::make_shared<Managed_Frame>(std::move(active->second));
active_frames.erase(active);
++encoding_frames;
std::shared_ptr<Managed_Frame> managed;
std::shared_ptr<Managed_Frame> discarded;
std::shared_ptr<Managed_Frame> scheduled;
{
std::lock_guard lock(frame_mutex);
const auto active = active_frames.find(frame);
if (active == active_frames.end())
throw std::logic_error(
"frame callback has no externally owned active frame");
managed = std::make_shared<Managed_Frame>(std::move(active->second));
active_frames.erase(active);
/*
* Scene
*
* 线
*/
discarded = std::exchange(pending_encoding_frame, std::move(managed));
if (!encoder_running) {
encoder_running = true;
scheduled = std::exchange(pending_encoding_frame, {});
}
}
frame->mark(Frame_Trace_Marker::video_encode_queued);
if (scheduled) schedule_encoding(std::move(scheduled), std::move(lifetime));
}
void Plot::Private::schedule_encoding(std::shared_ptr<Managed_Frame> managed,
std::weak_ptr<Plot> lifetime) {
asio::post(encode_strand, [lifetime, managed = std::move(managed)] {
if (const auto owner = lifetime.lock())
owner->d->encode_and_publish(managed, lifetime);
@@ -372,47 +462,82 @@ void Plot::Private::queue_completed_frame(Render_Frame* frame,
void Plot::Private::encode_and_publish(std::shared_ptr<Managed_Frame> managed,
std::weak_ptr<Plot> lifetime) {
struct Encoding_Completion final {
Private& plot; /* 需要归还编码调度权的 Plot 实现。 */
std::weak_ptr<Plot> lifetime; /* 下一待编码帧只有在 Plot 存活时才继续调度。 */
~Encoding_Completion() { plot.finish_encoding(std::move(lifetime)); }
} completion{*this, lifetime};
const auto streams = stream_snapshot();
const auto pacing = frame_policy.snapshot();
std::shared_ptr<const Encoded_Video_Frame> video;
std::uint32_t output_width{};
std::uint32_t output_height{};
Render_Frame* frame = std::visit(
[](const auto& value) -> Render_Frame* { return value.get(); }, managed->frame);
frame->record(
Frame_Trace_Measurement::stream_publish_tail_ns,
last_stream_publish_tail_ns.load(std::memory_order_acquire));
frame->mark(Frame_Trace_Marker::video_encode_started);
if (streams.video_enabled) {
const auto presentation_time = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::duration<double, std::milli>(managed->tick.time_milliseconds));
if (pacing.video_enabled) {
const auto sequence = frame->identity().sequence;
if (auto* frame_2d = std::get_if<std::unique_ptr<Frame_2D>>(&managed->frame)) {
auto pixels = (*frame_2d)->output_pixels();
video = encoder.encode(pixels.bytes, managed->tick.width, managed->tick.height,
Video_Pixel_Layout::bgra, managed->tick.sequence,
presentation_time);
output_width = static_cast<std::uint32_t>(pixels.width);
output_height = static_cast<std::uint32_t>(pixels.height);
video = encoder.encode(pixels.bytes, output_width, output_height,
Video_Pixel_Layout::bgra, sequence,
managed->presentation_time);
} else {
auto& frame_3d = std::get<std::unique_ptr<Frame_3D>>(managed->frame);
video = encoder.encode(frame_3d->pixels(), managed->tick.width, managed->tick.height,
Video_Pixel_Layout::rgba, managed->tick.sequence,
presentation_time);
const auto extent = frame_3d->extent();
output_width = extent.width;
output_height = extent.height;
/* 三维后端满载时会把多次提交合并为最新画面,并完成全部历史回调。
* Frame extent tick
* */
if (frame_3d->output() == Frame_3D_Output::pixels)
video = encoder.encode(frame_3d->pixels(), output_width,
output_height, Video_Pixel_Layout::rgba,
sequence, managed->presentation_time);
}
} else if (auto* frame_2d = std::get_if<std::unique_ptr<Frame_2D>>(&managed->frame)) {
const auto image = (*frame_2d)->image();
output_width = static_cast<std::uint32_t>(image.width);
output_height = static_cast<std::uint32_t>(image.height);
} else {
const auto extent = std::get<std::unique_ptr<Frame_3D>>(managed->frame)->extent();
output_width = extent.width;
output_height = extent.height;
}
frame->mark(Frame_Trace_Marker::video_encode_finished);
const auto publish_tail_started = std::chrono::steady_clock::now();
frame->mark(Frame_Trace_Marker::stream_publish_started);
const auto bytes = video ? video->annex_b.size() : 0U;
frame->mark(Frame_Trace_Marker::stream_publish_finished);
const auto metadata = frame_metadata(*frame, managed->tick.width, managed->tick.height,
bytes, frame_policy.snapshot()).dump();
const auto with_video = std::make_shared<Plot_Stream_Frame>(
Plot_Stream_Frame{metadata, video});
const auto diagnostics = std::make_shared<Plot_Stream_Frame>(
Plot_Stream_Frame{metadata, {}});
const auto metadata = frame_metadata(*frame, output_width, output_height,
bytes, pacing).dump();
const auto published = std::make_shared<Plot_Stream_Frame>(
Plot_Stream_Frame{metadata, pacing.video_enabled ? video : nullptr});
for (const auto& consumer : streams.consumers) {
if (!consumer.render_enabled || !consumer.handler) continue;
consumer.handler(consumer.video_enabled ? with_video : diagnostics);
if (!consumer.handler) continue;
consumer.handler(published);
}
asio::post(strand, [lifetime] {
if (const auto owner = lifetime.lock()) {
if (owner->d->encoding_frames == 0)
throw std::logic_error("Plot encoding frame accounting underflow");
--owner->d->encoding_frames;
}
});
last_stream_publish_tail_ns.store(
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - publish_tail_started).count()),
std::memory_order_release);
}
void Plot::Private::finish_encoding(std::weak_ptr<Plot> lifetime) {
std::shared_ptr<Managed_Frame> scheduled;
{
std::lock_guard lock(frame_mutex);
if (!encoder_running)
throw std::logic_error("Plot encoding scheduler completed without an active encoder");
scheduled = std::exchange(pending_encoding_frame, {});
if (!scheduled) encoder_running = false;
}
if (scheduled) schedule_encoding(std::move(scheduled), std::move(lifetime));
}
Plot::Plot(asio::any_io_executor executor, std::unique_ptr<Scene_2D> scene,
@@ -435,25 +560,17 @@ void Plot::ensure_started() {
*/
if (auto* scene = std::get_if<std::unique_ptr<Scene_2D>>(&d->scene)) {
(*scene)->set_frame_callback([weak = weak_from_this()](Frame_2D* frame) {
if (auto owner = weak.lock()) {
frame->mark(Frame_Trace_Marker::callback_finished);
asio::post(owner->d->strand, [weak, frame] {
if (auto next = weak.lock()) next->d->queue_completed_frame(frame, next);
});
}
if (auto owner = weak.lock())
owner->d->queue_completed_frame(frame, owner);
});
} else {
std::get<std::unique_ptr<Scene_3D>>(d->scene)->set_frame_callback(
[weak = weak_from_this()](Frame_3D* frame) {
if (auto owner = weak.lock()) {
frame->mark(Frame_Trace_Marker::callback_finished);
asio::post(owner->d->strand, [weak, frame] {
if (auto next = weak.lock()) next->d->queue_completed_frame(frame, next);
});
}
if (auto owner = weak.lock())
owner->d->queue_completed_frame(frame, owner);
});
}
d->schedule_clock();
d->start_clock();
asio::co_spawn(d->strand, [self]() -> asio::awaitable<void> {
for (;;) {
asio::error_code error;
@@ -488,17 +605,22 @@ void Plot::unsubscribe(Stream_Id stream) {
d->consumers.erase(stream);
}
void Plot::configure_stream(Stream_Id stream, bool render_enabled, bool video_enabled,
std::uint32_t width, std::uint32_t height) {
void Plot::configure_stream(Stream_Id stream, std::uint32_t width, std::uint32_t height) {
std::lock_guard lock(d->consumers_mutex);
const auto found = d->consumers.find(stream);
if (found == d->consumers.end()) return;
found->second.render_enabled = render_enabled;
found->second.video_enabled = video_enabled;
found->second.width = width;
found->second.height = height;
}
void Plot::request_video_key_frame() {
ensure_started();
const auto weak = weak_from_this();
asio::post(d->encode_strand, [weak] {
if (const auto owner = weak.lock()) owner->d->encoder.request_key_frame();
});
}
void Plot::render_once() {
ensure_started();
const auto weak = weak_from_this();
+2 -2
View File
@@ -69,8 +69,8 @@ public:
[[nodiscard]] Stream_Id subscribe(Stream_Handler handler);
void unsubscribe(Stream_Id stream);
void configure_stream(Stream_Id stream, bool render_enabled, bool video_enabled,
std::uint32_t width, std::uint32_t height);
void configure_stream(Stream_Id stream, std::uint32_t width, std::uint32_t height);
void request_video_key_frame();
void render_once();
void submit_input(Plot_Input_Event event);
void async_schema(Json_Handler handler);
+34 -10
View File
@@ -6,6 +6,7 @@
#include <cstdint>
#include <random>
#include <stdexcept>
#include <mutex>
#include <utility>
namespace aethera::web {
@@ -21,6 +22,7 @@ std::uint32_t make_ssrc() {
struct WebRtc_Video_Session::Private {
struct Callback_State {
Signal_Handler signal_handler; /* WebSocket 只负责 SDP/ICE 信令,不承载帧像素。 */
Ready_Handler ready_handler; /* Track 打开后请求首个可独立解码的视频帧。 */
std::atomic_bool track_open{};
std::atomic_bool closed{};
};
@@ -28,18 +30,24 @@ struct WebRtc_Video_Session::Private {
std::shared_ptr<rtc::PeerConnection> peer;
std::shared_ptr<rtc::Track> video_track;
std::shared_ptr<rtc::RtpPacketizationConfig> rtp_config;
std::mutex media_mutex; /* 关闭与发送之间不得跨越 Track 生命周期。 */
explicit Private(Signal_Handler value_handler)
Private(Signal_Handler value_signal_handler,
Ready_Handler value_ready_handler)
: callbacks(std::make_shared<Callback_State>()) {
callbacks->signal_handler = std::move(value_handler);
callbacks->signal_handler = std::move(value_signal_handler);
callbacks->ready_handler = std::move(value_ready_handler);
}
};
WebRtc_Video_Session::WebRtc_Video_Session(Signal_Handler signal_handler)
: d(std::make_unique<Private>(std::move(signal_handler))) {}
WebRtc_Video_Session::WebRtc_Video_Session(
Signal_Handler signal_handler, Ready_Handler ready_handler)
: d(std::make_unique<Private>(std::move(signal_handler),
std::move(ready_handler))) {}
WebRtc_Video_Session::~WebRtc_Video_Session() { close(); }
void WebRtc_Video_Session::start() {
std::lock_guard lock(d->media_mutex);
if (d->peer) return;
d->peer = std::make_shared<rtc::PeerConnection>(rtc::Configuration{});
const std::weak_ptr callbacks{d->callbacks};
@@ -71,8 +79,12 @@ void WebRtc_Video_Session::start() {
packetizer->addToChain(std::make_shared<rtc::RtcpNackResponder>());
d->video_track->setMediaHandler(packetizer);
d->video_track->onOpen([callbacks] {
if (const auto state = callbacks.lock())
if (const auto state = callbacks.lock()) {
state->track_open.store(true, std::memory_order_release);
if (!state->closed.load(std::memory_order_acquire) &&
state->ready_handler)
state->ready_handler();
}
});
d->video_track->onClosed([callbacks] {
if (const auto state = callbacks.lock())
@@ -82,25 +94,37 @@ void WebRtc_Video_Session::start() {
}
void WebRtc_Video_Session::accept_answer(std::string_view sdp) {
std::lock_guard lock(d->media_mutex);
if (!d->peer) throw std::logic_error("WebRTC session has not started");
d->peer->setRemoteDescription(rtc::Description(std::string(sdp), "answer"));
}
void WebRtc_Video_Session::add_remote_candidate(std::string_view candidate,
std::string_view mid) {
std::lock_guard lock(d->media_mutex);
if (!d->peer) throw std::logic_error("WebRTC session has not started");
d->peer->addRemoteCandidate(rtc::Candidate(std::string(candidate), std::string(mid)));
}
void WebRtc_Video_Session::send(const Encoded_Video_Frame& frame) {
WebRtc_Video_Session::Send_Result WebRtc_Video_Session::send(
const Encoded_Video_Frame& frame) {
std::lock_guard lock(d->media_mutex);
if (!d->video_track || !d->callbacks->track_open.load(std::memory_order_acquire) ||
frame.annex_b.empty()) return;
d->video_track->sendFrame(
reinterpret_cast<const rtc::byte*>(frame.annex_b.data()), frame.annex_b.size(),
rtc::FrameInfo(std::chrono::duration<double>(frame.presentation_time)));
frame.annex_b.empty()) return Send_Result::not_open;
try {
d->video_track->sendFrame(
reinterpret_cast<const rtc::byte*>(frame.annex_b.data()), frame.annex_b.size(),
rtc::FrameInfo(std::chrono::duration<double>(frame.presentation_time)));
return Send_Result::sent;
} catch (const std::runtime_error&) {
/* LibDataChannel 可在 isOpen() 后由其内部线程关闭 Track。 */
if (!d->video_track->isOpen()) return Send_Result::not_open;
throw;
}
}
void WebRtc_Video_Session::close() {
std::lock_guard lock(d->media_mutex);
if (d->callbacks->closed.exchange(true, std::memory_order_acq_rel)) return;
d->callbacks->track_open.store(false, std::memory_order_release);
if (d->video_track) d->video_track->close();
+8 -2
View File
@@ -8,9 +8,15 @@
namespace aethera::web {
class WebRtc_Video_Session final {
public:
enum class Send_Result : std::uint8_t {
sent,
not_open
};
using Signal_Handler = std::function<void(std::string)>;
using Ready_Handler = std::function<void()>;
explicit WebRtc_Video_Session(Signal_Handler signal_handler);
WebRtc_Video_Session(Signal_Handler signal_handler,
Ready_Handler ready_handler);
~WebRtc_Video_Session();
WebRtc_Video_Session(const WebRtc_Video_Session&) = delete;
WebRtc_Video_Session& operator=(const WebRtc_Video_Session&) = delete;
@@ -18,7 +24,7 @@ public:
void start();
void accept_answer(std::string_view sdp);
void add_remote_candidate(std::string_view candidate, std::string_view mid);
void send(const Encoded_Video_Frame& frame);
[[nodiscard]] Send_Result send(const Encoded_Video_Frame& frame);
void close();
private:
+35 -28
View File
@@ -36,34 +36,39 @@ std::shared_ptr<Plot> find_plot(const Plot_Map& plots, std::string_view id) {
int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root) {
const auto hardware_threads = std::max(2U, std::thread::hardware_concurrency());
auto graph_pool = std::make_shared<asio::thread_pool>(std::min(8U, hardware_threads));
const auto executor = graph_pool->get_executor();
const auto plot_workers = std::min(8U, hardware_threads);
/* 2D 的同步 Blend2D 绘制不能占满 3D 的帧时钟与 Visual Prepare 域。
* Plot 使 Plot/ */
auto plot_2d_pool = std::make_shared<asio::thread_pool>(plot_workers);
auto plot_3d_pool = std::make_shared<asio::thread_pool>(plot_workers);
const auto executor_2d = plot_2d_pool->get_executor();
const auto executor_3d = plot_3d_pool->get_executor();
auto plots = std::make_shared<Plot_Map>();
plots->emplace("axes", make_axes_plot(executor));
plots->emplace("spectrum", make_spectrum_plot(executor));
plots->emplace("frequency_trace", make_frequency_trace_plot(executor));
plots->emplace("sweep_spectrum", make_sweep_spectrum_plot(executor));
plots->emplace("afterglow", make_afterglow_plot(executor));
plots->emplace("waterfall", make_waterfall_plot(executor));
plots->emplace("constellation", make_constellation_plot(executor));
plots->emplace("selection_overlay", make_selection_overlay_plot(executor));
plots->emplace("datoviz_point", make_datoviz_point_plot(executor));
plots->emplace("datoviz_splat", make_datoviz_splat_plot(executor));
plots->emplace("datoviz_pixel", make_datoviz_pixel_plot(executor));
plots->emplace("datoviz_marker", make_datoviz_marker_plot(executor));
plots->emplace("datoviz_sphere", make_datoviz_sphere_plot(executor));
plots->emplace("datoviz_segment", make_datoviz_segment_plot(executor));
plots->emplace("datoviz_vector", make_datoviz_vector_plot(executor));
plots->emplace("datoviz_primitive", make_datoviz_primitive_plot(executor));
plots->emplace("datoviz_mesh", make_datoviz_mesh_plot(executor));
plots->emplace("datoviz_spectrogram", make_datoviz_spectrogram_plot(executor));
plots->emplace("datoviz_path", make_datoviz_path_plot(executor));
plots->emplace("datoviz_image", make_datoviz_image_plot(executor));
plots->emplace("datoviz_labels", make_datoviz_labels_plot(executor));
plots->emplace("datoviz_glyph", make_datoviz_glyph_plot(executor));
plots->emplace("datoviz_text", make_datoviz_text_plot(executor));
plots->emplace("datoviz_volume", make_datoviz_volume_plot(executor));
plots->emplace("axes", make_axes_plot(executor_2d));
plots->emplace("spectrum", make_spectrum_plot(executor_2d));
plots->emplace("frequency_trace", make_frequency_trace_plot(executor_2d));
plots->emplace("sweep_spectrum", make_sweep_spectrum_plot(executor_2d));
plots->emplace("afterglow", make_afterglow_plot(executor_2d));
plots->emplace("waterfall", make_waterfall_plot(executor_2d));
plots->emplace("constellation", make_constellation_plot(executor_2d));
plots->emplace("selection_overlay", make_selection_overlay_plot(executor_2d));
plots->emplace("datoviz_point", make_datoviz_point_plot(executor_3d));
plots->emplace("datoviz_splat", make_datoviz_splat_plot(executor_3d));
plots->emplace("datoviz_pixel", make_datoviz_pixel_plot(executor_3d));
plots->emplace("datoviz_marker", make_datoviz_marker_plot(executor_3d));
plots->emplace("datoviz_sphere", make_datoviz_sphere_plot(executor_3d));
plots->emplace("datoviz_segment", make_datoviz_segment_plot(executor_3d));
plots->emplace("datoviz_vector", make_datoviz_vector_plot(executor_3d));
plots->emplace("datoviz_primitive", make_datoviz_primitive_plot(executor_3d));
plots->emplace("datoviz_mesh", make_datoviz_mesh_plot(executor_3d));
plots->emplace("datoviz_spectrogram", make_datoviz_spectrogram_plot(executor_3d));
plots->emplace("datoviz_path", make_datoviz_path_plot(executor_3d));
plots->emplace("datoviz_image", make_datoviz_image_plot(executor_3d));
plots->emplace("datoviz_labels", make_datoviz_labels_plot(executor_3d));
plots->emplace("datoviz_glyph", make_datoviz_glyph_plot(executor_3d));
plots->emplace("datoviz_text", make_datoviz_text_plot(executor_3d));
plots->emplace("datoviz_volume", make_datoviz_volume_plot(executor_3d));
auto resolve_plot = [plots](std::string_view id) { return find_plot(*plots, id); };
auto websocket = std::make_shared<Graph_WebSocket_Controller>(resolve_plot);
@@ -158,8 +163,10 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
.setThreadNum(std::min(8U, hardware_threads))
.setIdleConnectionTimeout(90)
.run();
graph_pool->stop();
graph_pool->join();
plot_2d_pool->stop();
plot_3d_pool->stop();
plot_2d_pool->join();
plot_3d_pool->join();
return 0;
}
}
+5
View File
@@ -2,6 +2,11 @@ include_guard(GLOBAL)
set(Aethera_FFmpeg_root
"${CMAKE_CURRENT_LIST_DIR}/ffmpeg-9.0.1-full_build-shared")
file(GLOB Aethera_FFmpeg_runtime_libraries CONFIGURE_DEPENDS
"${Aethera_FFmpeg_root}/bin/*.dll")
if (NOT Aethera_FFmpeg_runtime_libraries)
message(FATAL_ERROR "Aethera FFmpeg runtime libraries were not found")
endif ()
foreach(component IN ITEMS avutil avcodec swscale)
add_library(Aethera_FFmpeg_${component} SHARED IMPORTED GLOBAL)