mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 19:01:21 +00:00
fix: fetch webcam info in ws loop
This commit is contained in:
@@ -930,55 +930,40 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
|
||||
|
||||
CameraStreamMode MoonrakerPrinterAgent::get_camera_stream_mode() const
|
||||
{
|
||||
refresh_webcam_info();
|
||||
// Cache read only: the status loop refreshes this off the GUI thread (see
|
||||
// refresh_webcam_info), so a getter must never block on an HTTP request.
|
||||
std::lock_guard<std::recursive_mutex> lock(payload_mutex);
|
||||
return webcam_stream_mode;
|
||||
}
|
||||
|
||||
std::string MoonrakerPrinterAgent::get_camera_url() const
|
||||
{
|
||||
refresh_webcam_info();
|
||||
std::lock_guard<std::recursive_mutex> lock(payload_mutex);
|
||||
return webcam_stream_url;
|
||||
}
|
||||
|
||||
void MoonrakerPrinterAgent::refresh_webcam_info() const
|
||||
{
|
||||
ConnectionSettings connection;
|
||||
uint64_t generation;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(connect_mutex);
|
||||
connection.dev_id = device_info.dev_id;
|
||||
connection.base_url = device_info.base_url;
|
||||
connection.api_key = device_info.api_key;
|
||||
connection.use_ssl = device_info.use_ssl;
|
||||
connection.ca_file = device_info.ca_file;
|
||||
generation = connect_generation.load();
|
||||
}
|
||||
|
||||
// Called every status-loop iteration; the gate below keeps the HTTP lookup rare.
|
||||
// Only the ws thread calls this, so scheduling needs no extra locking.
|
||||
const uint64_t now_ms = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
if (now_ms < webcam_info_next_attempt_ms.load(std::memory_order_relaxed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ConnectionSettings connection = get_connection_settings();
|
||||
if (connection.base_url.empty()) {
|
||||
std::lock_guard<std::recursive_mutex> lock(payload_mutex);
|
||||
webcam_stream_url.clear();
|
||||
webcam_stream_mode = CameraStreamMode::none;
|
||||
webcam_info_last_lookup_ms = 0;
|
||||
webcam_info_generation = generation;
|
||||
webcam_info_next_attempt_ms.store(now_ms + WEBCAM_INFO_FAILURE_BACKOFF_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(payload_mutex);
|
||||
if (webcam_info_generation == generation &&
|
||||
now_ms - webcam_info_last_lookup_ms < WEBCAM_INFO_REFRESH_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
webcam_info_generation = generation;
|
||||
webcam_info_last_lookup_ms = now_ms;
|
||||
}
|
||||
|
||||
fetch_webcam_info(connection, generation);
|
||||
const bool ok = fetch_webcam_info(connection, connect_generation.load());
|
||||
webcam_info_next_attempt_ms.store(now_ms + (ok ? WEBCAM_INFO_REFRESH_INTERVAL_MS
|
||||
: WEBCAM_INFO_FAILURE_BACKOFF_MS));
|
||||
}
|
||||
|
||||
std::string MoonrakerPrinterAgent::trim_and_upper(const std::string& input)
|
||||
@@ -1892,6 +1877,68 @@ bool MoonrakerPrinterAgent::send_ws_rpc(const std::string& method, const nlohman
|
||||
return false;
|
||||
}
|
||||
|
||||
bool moonraker_parse_webcam_list(const nlohmann::json& response, const std::string& base_url,
|
||||
MoonrakerWebcamSelection& out)
|
||||
{
|
||||
const auto result = response.contains("result") ? response["result"] : response;
|
||||
if (!result.contains("webcams") || !result["webcams"].is_array()) {
|
||||
out.error = "Unexpected JSON structure";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& webcam : result["webcams"]) {
|
||||
if (!webcam.is_object()) {
|
||||
continue;
|
||||
}
|
||||
// /server/webcams/list returns disabled webcams too; skip them.
|
||||
if (webcam.contains("enabled") && webcam["enabled"].is_boolean() && !webcam["enabled"].get<bool>()) {
|
||||
continue;
|
||||
}
|
||||
if (webcam.contains("stream_url") && webcam["stream_url"].is_string() &&
|
||||
!webcam["stream_url"].get<std::string>().empty()) {
|
||||
out.url = webcam["stream_url"].get<std::string>();
|
||||
out.mode = CameraStreamMode::http;
|
||||
} else if (webcam.contains("snapshot_url") && webcam["snapshot_url"].is_string() &&
|
||||
!webcam["snapshot_url"].get<std::string>().empty()) {
|
||||
out.url = webcam["snapshot_url"].get<std::string>();
|
||||
out.mode = CameraStreamMode::http_snapshot;
|
||||
}
|
||||
if (webcam.contains("name") && webcam["name"].is_string()) {
|
||||
out.name = webcam["name"].get<std::string>();
|
||||
}
|
||||
if (!out.url.empty()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (out.url.empty()) {
|
||||
out.error = "No enabled webcam";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (out.url.rfind("rtsp://", 0) == 0 || out.url.rfind("rtsps://", 0) == 0) {
|
||||
out.mode = CameraStreamMode::rtsp;
|
||||
} else if (out.url.rfind("http", 0) != 0 && out.url.front() == '/') {
|
||||
// why: Moonraker's API port serves a JSON 404 for /webcam; relative camera URLs use the printer web root.
|
||||
const size_t scheme_end = base_url.find("://");
|
||||
const size_t authority_start = scheme_end == std::string::npos ? 0 : scheme_end + 3;
|
||||
const size_t authority_end = base_url.find('/', authority_start);
|
||||
const std::string scheme = scheme_end == std::string::npos ? "" : base_url.substr(0, scheme_end + 3);
|
||||
std::string authority = base_url.substr(authority_start, authority_end - authority_start);
|
||||
const size_t port_start = authority.rfind(':');
|
||||
if (port_start != std::string::npos && port_start + 1 < authority.size() &&
|
||||
std::all_of(authority.begin() + port_start + 1, authority.end(), [](char c) { return c >= '0' && c <= '9'; })) {
|
||||
authority.erase(port_start);
|
||||
}
|
||||
out.url = scheme + authority + out.url;
|
||||
} else if (out.url.rfind("http", 0) != 0) {
|
||||
out.error = "Unsupported webcam URL";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MoonrakerPrinterAgent::fetch_webcam_info(const ConnectionSettings& connection, uint64_t generation) const
|
||||
{
|
||||
std::string camera_url;
|
||||
@@ -1934,61 +1981,16 @@ bool MoonrakerPrinterAgent::fetch_webcam_info(const ConnectionSettings& connecti
|
||||
if (json.is_discarded()) {
|
||||
error = "Invalid JSON response";
|
||||
} else {
|
||||
const auto result = json.contains("result") ? json["result"] : json;
|
||||
if (!result.contains("webcams") || !result["webcams"].is_array()) {
|
||||
error = "Unexpected JSON structure";
|
||||
MoonrakerWebcamSelection selection;
|
||||
if (moonraker_parse_webcam_list(json, connection.base_url, selection)) {
|
||||
camera_url = selection.url;
|
||||
stream_mode = selection.mode;
|
||||
webcam_name = selection.name;
|
||||
} else {
|
||||
for (const auto& webcam : result["webcams"]) {
|
||||
if (webcam.is_object())
|
||||
{
|
||||
// /server/webcams/list returns disabled webcams too; skip them.
|
||||
if (webcam.contains("enabled") && webcam["enabled"].is_boolean() &&
|
||||
!webcam["enabled"].get<bool>()) {
|
||||
continue;
|
||||
}
|
||||
if (webcam.contains("stream_url") && webcam["stream_url"].is_string() &&
|
||||
!webcam["stream_url"].get<std::string>().empty()) {
|
||||
camera_url = webcam["stream_url"].get<std::string>();
|
||||
stream_mode = CameraStreamMode::http;
|
||||
} else if (webcam.contains("snapshot_url") && webcam["snapshot_url"].is_string() &&
|
||||
!webcam["snapshot_url"].get<std::string>().empty()) {
|
||||
camera_url = webcam["snapshot_url"].get<std::string>();
|
||||
stream_mode = CameraStreamMode::http_snapshot;
|
||||
}
|
||||
if (webcam.contains("name") && webcam["name"].is_string()) {
|
||||
webcam_name = webcam["name"].get<std::string>();
|
||||
}
|
||||
if (!camera_url.empty())
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (camera_url.empty()) {
|
||||
error = "No enabled webcam";
|
||||
}
|
||||
error = selection.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error.empty()) {
|
||||
if (camera_url.rfind("rtsp://", 0) == 0 || camera_url.rfind("rtsps://", 0) == 0) {
|
||||
stream_mode = CameraStreamMode::rtsp;
|
||||
} else if (camera_url.rfind("http", 0) != 0 && !camera_url.empty() && camera_url.front() == '/') {
|
||||
// why: Moonraker's API port serves a JSON 404 for /webcam; relative camera URLs use the printer web root.
|
||||
const size_t scheme_end = connection.base_url.find("://");
|
||||
const size_t authority_start = scheme_end == std::string::npos ? 0 : scheme_end + 3;
|
||||
const size_t authority_end = connection.base_url.find('/', authority_start);
|
||||
const std::string scheme = scheme_end == std::string::npos ? "" : connection.base_url.substr(0, scheme_end + 3);
|
||||
std::string authority = connection.base_url.substr(authority_start, authority_end - authority_start);
|
||||
const size_t port_start = authority.rfind(':');
|
||||
if (port_start != std::string::npos && port_start + 1 < authority.size() &&
|
||||
std::all_of(authority.begin() + port_start + 1, authority.end(), [](char c) { return c >= '0' && c <= '9'; })) {
|
||||
authority.erase(port_start);
|
||||
}
|
||||
camera_url = scheme + authority + camera_url;
|
||||
} else if (camera_url.rfind("http", 0) != 0) {
|
||||
error = "Unsupported webcam URL";
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
error = e.what();
|
||||
} catch (...) {
|
||||
@@ -2351,6 +2353,7 @@ void MoonrakerPrinterAgent::start_status_stream(const std::string& dev_id, Conne
|
||||
}
|
||||
|
||||
ws_stop.store(false);
|
||||
webcam_info_next_attempt_ms.store(0); // new connection: refresh the webcam promptly
|
||||
ws_thread = std::thread([this, dev_id, connection = std::move(connection)]() mutable {
|
||||
run_status_stream(dev_id, std::move(connection));
|
||||
});
|
||||
@@ -2492,6 +2495,9 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, ConnectionSett
|
||||
// Read loop
|
||||
while (!ws_stop.load()) {
|
||||
on_status_loop_tick(dev_id);
|
||||
// Webcam discovery is self-gated (WEBCAM_INFO_REFRESH_INTERVAL_MS) and runs
|
||||
// here so the GUI-thread getters stay non-blocking.
|
||||
refresh_webcam_info();
|
||||
|
||||
ws.expires_after(std::chrono::seconds(2));
|
||||
std::string payload;
|
||||
|
||||
@@ -31,6 +31,20 @@ bool moonraker_is_light_name(const std::string& name);
|
||||
// e.g. LIGHT_ON -> +1, LIGHT_OFF -> -1, LIGHT -> 0.
|
||||
int moonraker_light_name_direction(const std::string& name);
|
||||
|
||||
struct MoonrakerWebcamSelection
|
||||
{
|
||||
std::string url;
|
||||
CameraStreamMode mode = CameraStreamMode::none;
|
||||
std::string name;
|
||||
std::string error; // set when no selectable webcam was found
|
||||
};
|
||||
|
||||
// Selects a webcam from a parsed /server/webcams/list response (the top-level JSON or its
|
||||
// "result" value). Returns true and fills url/mode/name on success; on failure returns
|
||||
// false and sets error.
|
||||
bool moonraker_parse_webcam_list(const nlohmann::json& response, const std::string& base_url,
|
||||
MoonrakerWebcamSelection& out);
|
||||
|
||||
class MoonrakerWebsocket
|
||||
{
|
||||
public:
|
||||
@@ -302,11 +316,12 @@ private:
|
||||
std::string thumbnail_url;
|
||||
mutable std::string webcam_stream_url;
|
||||
mutable CameraStreamMode webcam_stream_mode = CameraStreamMode::none;
|
||||
mutable uint64_t webcam_info_last_lookup_ms = 0;
|
||||
mutable uint64_t webcam_info_generation = 0;
|
||||
// Next time the status loop may look the webcam up again (steady_clock ms).
|
||||
mutable std::atomic<uint64_t> webcam_info_next_attempt_ms{0};
|
||||
unsigned thumbnail_lookup_attempts = 0;
|
||||
|
||||
static constexpr uint64_t WEBCAM_INFO_REFRESH_INTERVAL_MS = 1000;
|
||||
static constexpr uint64_t WEBCAM_INFO_REFRESH_INTERVAL_MS = 30000;
|
||||
static constexpr uint64_t WEBCAM_INFO_FAILURE_BACKOFF_MS = 60000;
|
||||
|
||||
std::atomic<int> next_jsonrpc_id{1};
|
||||
std::set<std::string> available_objects; // Track for feature detection
|
||||
|
||||
@@ -98,6 +98,70 @@ TEST_CASE("unit: Moonraker light name matching", "[unit][moonraker]")
|
||||
CHECK(moonraker_is_light_name("MODLELIGHT_SWITCH"));
|
||||
}
|
||||
|
||||
TEST_CASE("Moonraker webcam selection skips disabled webcams and prefers the first enabled one",
|
||||
"[unit][moonraker]")
|
||||
{
|
||||
const auto response = nlohmann::json::parse(R"({
|
||||
"result": { "webcams": [
|
||||
{ "name": "disabled", "enabled": false, "stream_url": "http://192.168.1.9:8080/stream" },
|
||||
{ "name": "enabled", "enabled": true, "stream_url": "http://192.168.1.9:8080/stream" }
|
||||
]}
|
||||
})");
|
||||
|
||||
MoonrakerWebcamSelection selection;
|
||||
REQUIRE(moonraker_parse_webcam_list(response, "http://192.168.1.9:7125", selection));
|
||||
CHECK(selection.name == "enabled");
|
||||
CHECK(selection.url == "http://192.168.1.9:8080/stream");
|
||||
CHECK(selection.mode == CameraStreamMode::http);
|
||||
CHECK(selection.error.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Moonraker webcam selection resolves relative URLs, maps rtsp, and rejects other schemes",
|
||||
"[unit][moonraker]")
|
||||
{
|
||||
const auto relative = nlohmann::json::parse(R"({
|
||||
"result": { "webcams": [ { "name": "cam", "snapshot_url": "/webcam/?action=snapshot" } ] }
|
||||
})");
|
||||
MoonrakerWebcamSelection rel;
|
||||
REQUIRE(moonraker_parse_webcam_list(relative, "http://192.168.1.9:7125", rel));
|
||||
// Relative URLs use the printer web root, without the Moonraker API port.
|
||||
CHECK(rel.url == "http://192.168.1.9/webcam/?action=snapshot");
|
||||
CHECK(rel.mode == CameraStreamMode::http_snapshot);
|
||||
|
||||
const auto rtsp = nlohmann::json::parse(R"({
|
||||
"result": { "webcams": [ { "name": "cam", "stream_url": "rtsp://192.168.1.9:554/live" } ] }
|
||||
})");
|
||||
MoonrakerWebcamSelection rt;
|
||||
REQUIRE(moonraker_parse_webcam_list(rtsp, "http://192.168.1.9:7125", rt));
|
||||
CHECK(rt.mode == CameraStreamMode::rtsp);
|
||||
|
||||
const auto unsupported = nlohmann::json::parse(R"({
|
||||
"result": { "webcams": [ { "name": "cam", "stream_url": "weird://host/x" } ] }
|
||||
})");
|
||||
MoonrakerWebcamSelection bad;
|
||||
CHECK_FALSE(moonraker_parse_webcam_list(unsupported, "http://192.168.1.9:7125", bad));
|
||||
CHECK(bad.error == "Unsupported webcam URL");
|
||||
}
|
||||
|
||||
TEST_CASE("Moonraker webcam selection reports no webcam and malformed structure", "[unit][moonraker]")
|
||||
{
|
||||
const auto empty = nlohmann::json::parse(R"({ "result": { "webcams": [] } })");
|
||||
MoonrakerWebcamSelection none;
|
||||
CHECK_FALSE(moonraker_parse_webcam_list(empty, "http://host:7125", none));
|
||||
CHECK(none.error == "No enabled webcam");
|
||||
|
||||
const auto disabled_only = nlohmann::json::parse(R"({
|
||||
"result": { "webcams": [ { "name": "disabled", "enabled": false, "stream_url": "http://host/stream" } ] }
|
||||
})");
|
||||
MoonrakerWebcamSelection off;
|
||||
CHECK_FALSE(moonraker_parse_webcam_list(disabled_only, "http://host:7125", off));
|
||||
|
||||
const auto malformed = nlohmann::json::parse(R"({ "result": { "nope": 1 } })");
|
||||
MoonrakerWebcamSelection shape;
|
||||
CHECK_FALSE(moonraker_parse_webcam_list(malformed, "http://host:7125", shape));
|
||||
CHECK(shape.error == "Unexpected JSON structure");
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// UNIT - handle_request's not-supported default.
|
||||
// The agent is the only thing that knows what it can translate, so an untranslated
|
||||
|
||||
Reference in New Issue
Block a user