架构大改之前
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user