Files
Aethera/web_server/src/Graph_WebSocket.cpp
T
2026-08-24 17:28:17 +08:00

246 lines
9.0 KiB
C++

#include "Graph_WebSocket.hpp"
#include "Exclusive_Page.hpp"
#include <magic_enum/magic_enum.hpp>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <mutex>
#include <string>
namespace aethera::web {
namespace {
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));
}
std::uint32_t input_dimension(std::uint32_t value, std::uint32_t minimum,
std::uint32_t maximum) {
return std::clamp(value, minimum, maximum);
}
}
struct Graph_WebSocket::Private {
std::weak_ptr<drogon::WebSocketConnection> connection;
std::shared_ptr<Plot> plot;
Plot::Stream_Id stream{};
std::atomic_bool attached{};
std::mutex viewport_mutex;
std::uint32_t width{320}; /* 当前图集槽位对应的输入坐标宽度。 */
std::uint32_t height{192}; /* 当前图集槽位对应的输入坐标高度。 */
std::string page_id; /* 与页面级媒体连接共用的独占租约。 */
};
Graph_WebSocket::Graph_WebSocket(drogon::WebSocketConnectionPtr connection,
std::shared_ptr<Plot> plot,
std::string page_id)
: d(std::make_unique<Private>()) {
d->connection = std::move(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->stream = d->plot->subscribe(
[weak](std::shared_ptr<const Plot_Stream_Frame> frame) {
if (const auto socket = weak.lock())
socket->deliver_frame(std::move(frame));
});
}
void Graph_WebSocket::deliver_frame(
std::shared_ptr<const Plot_Stream_Frame> frame) {
if (!frame || frame->notification.empty() ||
!d->attached.load(std::memory_order_acquire)) return;
const auto connection = d->connection.lock();
if (connection && connection->connected()) {
try {
connection->send(frame->notification,
drogon::WebSocketMessageType::Text);
}
catch (...) {}
}
}
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;
const auto kind = json.value("kind", std::string{});
try {
if (kind == "stream") {
std::uint32_t width{320};
std::uint32_t height{192};
if (const auto viewport = json.find("viewport");
viewport != json.end() && viewport->is_object()) {
width = input_dimension(viewport->value("width", 320U), 160U, 1920U);
height = input_dimension(viewport->value("height", 192U), 120U, 1080U);
}
std::lock_guard lock(d->viewport_mutex);
d->width = width;
d->height = height;
return;
}
if (kind == "manual_render") {
d->plot->render_once();
return;
}
if (kind != "input") return;
const auto input = json.find("event");
if (input == json.end() || !input->is_object()) return;
const auto type = magic_enum::enum_cast<Event_Type>(
input->value("type", std::string{}));
if (!type) return;
std::uint32_t width{};
std::uint32_t height{};
{
std::lock_guard lock(d->viewport_mutex);
width = d->width;
height = d->height;
}
Plot_Input_Event decoded;
decoded.type = *type;
const auto read_point = [&](std::string_view key,
render_2d::Point_F& point,
bool viewport_relative) {
const auto value = input->find(key);
if (value == input->end() || !value->is_object()) return;
const auto x = value->value("x", 0.0);
const auto y = value->value("y", 0.0);
point.x = viewport_relative
? std::clamp(x, 0.0, static_cast<double>(width))
: x;
point.y = viewport_relative
? std::clamp(y, 0.0, static_cast<double>(height))
: y;
};
read_point("position", decoded.position, true);
read_point("global_position", decoded.global_position, false);
decoded.button = magic_enum::enum_cast<Mouse_Button>(
input->value("button", std::string{"none"})).value_or(Mouse_Button::none);
decoded.buttons = static_cast<Mouse_Button_Mask>(
std::clamp(input->value("buttons", 0), 0, 255));
decoded.modifiers = static_cast<Keyboard_Modifier>(
std::clamp(input->value("modifiers", 0), 0, 15));
decoded.pixel_delta_x = std::clamp(
input->value("pixel_delta_x", 0.0), -4096.0, 4096.0);
decoded.pixel_delta_y = std::clamp(
input->value("pixel_delta_y", 0.0), -4096.0, 4096.0);
decoded.angle_delta_x = std::clamp(
input->value("angle_delta_x", 0.0), -120.0, 120.0);
decoded.angle_delta_y = std::clamp(
input->value("angle_delta_y", 0.0), -120.0, 120.0);
decoded.key = magic_enum::enum_cast<Key>(
input->value("key", std::string{"unknown"})).value_or(Key::unknown);
decoded.native_key = input->value("native_key", 0U);
decoded.auto_repeat = input->value("auto_repeat", false);
d->plot->submit_input(std::move(decoded));
}
catch (const nlohmann::json::exception&) {}
catch (...) {}
}
void Graph_WebSocket::close() noexcept {
if (!d->attached.exchange(false, std::memory_order_acq_rel)) return;
try { d->plot->unsubscribe(d->stream); }
catch (...) {}
try { release_exclusive_page(d->page_id); }
catch (...) {}
}
Graph_WebSocket_Controller::Graph_WebSocket_Controller(Plot_Resolver resolver)
: resolve_plot(std::move(resolver)) {}
void Graph_WebSocket_Controller::initPathRouting() {
drogon::app().registerWebSocketControllerRegex(
"^/ws/plot/[^/]+$", classTypeName());
}
void Graph_WebSocket_Controller::handleNewConnection(
const drogon::HttpRequestPtr& request,
const drogon::WebSocketConnectionPtr& connection) {
try {
auto plot = resolve_plot(graph_id_from_path(request->path()));
if (!plot) {
try {
connection->shutdown(drogon::CloseCode::kViolation,
"Unknown Aethera plot");
}
catch (...) {}
return;
}
const auto page_id = request->getParameter("page");
if (!valid_page_id(page_id) || !acquire_exclusive_page(page_id)) {
try {
connection->send(nlohmann::json{{"kind", "exclusive_page_rejected"},
{"message", "Aethera Gallery is already owned by another page instance"}}.dump(),
drogon::WebSocketMessageType::Text);
}
catch (...) {}
try {
connection->shutdown(drogon::CloseCode::kViolation,
"Aethera Gallery allows one page instance");
}
catch (...) {}
return;
}
try {
auto socket = std::make_shared<Graph_WebSocket>(
connection, std::move(plot), page_id);
connection->setContext(socket);
connection->setPingMessage(
"aethera-gallery-plot", std::chrono::seconds(20));
socket->start();
}
catch (...) {
if (const auto socket = connection->getContext<Graph_WebSocket>())
socket->close();
else
release_exclusive_page(page_id);
try {
connection->shutdown(drogon::CloseCode::kViolation,
"Aethera plot setup failed");
}
catch (...) {}
}
}
catch (...) {
try {
connection->shutdown(drogon::CloseCode::kViolation,
"Aethera plot controller failed");
}
catch (...) {}
}
}
void Graph_WebSocket_Controller::handleNewMessage(
const drogon::WebSocketConnectionPtr& connection,
std::string&& message,
const drogon::WebSocketMessageType& type) {
try {
if (type != drogon::WebSocketMessageType::Text ||
message.size() > 64 * 1024) return;
if (const auto socket = connection->getContext<Graph_WebSocket>())
socket->receive(message);
}
catch (...) {}
}
void Graph_WebSocket_Controller::handleConnectionClosed(
const drogon::WebSocketConnectionPtr& connection) {
try {
if (const auto socket = connection->getContext<Graph_WebSocket>())
socket->close();
connection->clearContext();
}
catch (...) {}
}
}