246 lines
9.0 KiB
C++
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 (...) {}
|
|
}
|
|
}
|