From 08efb0cabda831309410ca2809e86471b5cfa8b4 Mon Sep 17 00:00:00 2001 From: Norihiro Kamae Date: Wed, 10 Jul 2024 20:17:57 +0900 Subject: [PATCH 1/5] ptz-http-backend: Add HTTP backend --- CMakeLists.txt | 1 + src/face-tracker-ptz.cpp | 3 + src/ptz-http-backend.cpp | 424 +++++++++++++++++++++++++++++++++++++++ src/ptz-http-backend.hpp | 31 +++ 4 files changed, 459 insertions(+) create mode 100644 src/ptz-http-backend.cpp create mode 100644 src/ptz-http-backend.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c10d61..d952940 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,7 @@ set(PLUGIN_SOURCES src/helper.cpp src/ptz-backend.cpp src/obsptz-backend.cpp + src/ptz-http-backend.cpp src/dummy-backend.cpp ) diff --git a/src/face-tracker-ptz.cpp b/src/face-tracker-ptz.cpp index a6d5d72..627b65f 100644 --- a/src/face-tracker-ptz.cpp +++ b/src/face-tracker-ptz.cpp @@ -17,6 +17,7 @@ #ifdef WITH_PTZ_TCP #include "libvisca-thread.hpp" #endif +#include "ptz-http-backend.hpp" #include "dummy-backend.hpp" #define PTZ_MAX_X 0x18 @@ -107,6 +108,7 @@ static const struct ptz_backend_type_s backends[] = #ifdef WITH_PTZ_TCP BACKEND("visca-over-tcp", libvisca_thread), #endif // WITH_PTZ_TCP + BACKEND("http", ptz_http_backend), BACKEND("dummy", dummy_backend), {NULL, NULL, NULL} #undef BACKEND @@ -452,6 +454,7 @@ static obs_properties_t *ftptz_properties(void *data) #ifdef WITH_PTZ_TCP obs_property_list_add_string(p, obs_module_text("VISCA over TCP"), "visca-over-tcp"); #endif // WITH_PTZ_TCP + obs_property_list_add_string(p, obs_module_text("HTTP"), "http"); obs_property_set_modified_callback(p, ptz_type_modified); obs_properties_add_bool(pp, "invert_x", obs_module_text("Invert control (Pan)")); diff --git a/src/ptz-http-backend.cpp b/src/ptz-http-backend.cpp new file mode 100644 index 0000000..8e32ea8 --- /dev/null +++ b/src/ptz-http-backend.cpp @@ -0,0 +1,424 @@ +#include +#include +#include +#include +#include +#include +#include "plugin-macros.generated.h" +#include "ptz-http-backend.hpp" + +#define CAMERA_MODELS_FILE "ptz/ptz-http-backend-camera-models.json" + +static obs_data_array_t *get_camera_models() +{ + BPtr path = obs_module_file(CAMERA_MODELS_FILE); + OBSDataAutoRelease root = obs_data_create_from_json_file(path); + return obs_data_get_array(root, "camera-models"); +} + +static obs_data_t *get_camera_model(const char *ptz_http_id) +{ + if (!ptz_http_id) + return nullptr; + + OBSDataArrayAutoRelease cameras = get_camera_models(); + for (size_t i = 0, n = obs_data_array_count(cameras); i < n; i++) { + obs_data_t *camera = obs_data_array_item(cameras, i); + const char *id = obs_data_get_string(camera, "id"); + + if (strcmp(id, ptz_http_id) == 0) + return camera; + + obs_data_release(camera); + } + + return nullptr; +} + +static void append_data_value(std::string &ret, obs_data *data, const char *name) +{ + char buf[64] = {0}; + obs_data_item_t *item = obs_data_item_byname(data, name); + if (!item) + return; + + switch (obs_data_item_gettype(item)) { + case OBS_DATA_STRING: + ret += obs_data_item_get_string(item); + break; + case OBS_DATA_NUMBER: + switch (obs_data_item_numtype(item)) { + case OBS_DATA_NUM_INT: + snprintf(buf, sizeof(buf) - 1, "%lld", obs_data_item_get_int(item)); + ret += buf; + break; + case OBS_DATA_NUM_DOUBLE: + snprintf(buf, sizeof(buf) - 1, "%f", obs_data_item_get_double(item)); + ret += buf; + break; + case OBS_DATA_NUM_INVALID: + break; + } + break; + default: + blog(LOG_ERROR, "Cannot convert camera settings '%s'", name); + } + + obs_data_item_release(&item); +} + +static std::string replace_placeholder(const char *str, obs_data *data) +{ + /** + * Replaces `{name}` in `str` with the actual value in `data`. + * Also replaces `{{}` in `str` with `{` as an escape. + */ + + std::string ret; + + while (*str) { + if (strncmp(str, "{{}", 3) == 0) { + ret += '{'; + str += 3; + } + else if (*str == '{') { + str++; + int end; + for (end = 0; str[end] && str[end] != '}'; end++); + if (str[end] == '}') { + std::string name(str, str + end); + append_data_value(ret, data, name.c_str()); + str += end + 1; + } + } + else + ret += *str++; + } + + return ret; +} + +struct control_change_s +{ + int u_int = 0; + bool is_int = false; + + bool update(float u, obs_data_t *control_function, const char *name); +}; + +bool control_change_s::update(float u, obs_data_t *control_function, const char *name) +{ + OBSDataAutoRelease func = obs_data_get_obj(control_function, name); + const char *type = obs_data_get_string(func, "type"); + if (strcmp(type, "linear-int") == 0) { + double k1 = obs_data_get_double(func, "k1"); + double k0 = obs_data_get_double(func, "k0"); + int max = (int)obs_data_get_int(func, "max"); + int u_int_next = (int)(k1 * u + k0); + if (u_int_next > max) + u_int_next = max; + else if (u_int_next < -max) + u_int_next = -max; + + if (!is_int || u_int_next != u_int) { + is_int = true; + u_int = u_int_next; + + blog(LOG_INFO, "control_change_s::update: k1=%f k0=%f u=%f max=%d u_int=%d", + k1, k0, u, max, u_int_next); + + return true; + } + return false; + } + + return false; +} + +static void add_control_value(obs_data_t *data, const char *name, control_change_s &u) +{ + if (u.is_int) { + obs_data_set_int(data, name, u.u_int); + } else { + blog(LOG_WARNING, "control data for '%s' is not defined", name); + } +} + +struct ptz_http_backend_data_s +{ + std::mutex mutex; + + /* User input data such as + * "id" + * "host" + */ + OBSData user_data; + + std::atomic data_changed; + std::atomic preset_changed; + std::atomic p_next, t_next, z_next; +}; + +ptz_http_backend::ptz_http_backend() +{ + data = new ptz_http_backend_data_s; + + add_ref(); + pthread_t thread; + pthread_create(&thread, NULL, ptz_http_backend::thread_main, (void*)this); + pthread_detach(thread); +} + +ptz_http_backend::~ptz_http_backend() +{ + delete data; +} + +void *ptz_http_backend::thread_main(void *data) +{ + auto *p = (ptz_http_backend*)data; + p->thread_loop(); + p->release(); + return NULL; +} + +static void call_url(const char *method, const char *url, const char *payload) +{ + blog(LOG_INFO, "call_url(method='%s', url='%s', payload='%s')", method, url, payload); + // TODO: Implement +} + +static bool send_ptz(obs_data_t *user_data, obs_data_t *camera_settings) +{ + const char *method = obs_data_get_string(camera_settings, "ptz-method"); + const char *url_t = obs_data_get_string(camera_settings, "ptz-url"); + const char *payload_t = obs_data_get_string(camera_settings, "ptz-payload"); + if (!method || !url_t) + return false; + + std::string url = replace_placeholder(url_t, user_data); + std::string payload = replace_placeholder(payload_t, user_data); + + blog(LOG_INFO, "send_ptz: url_t='%s' url='%s' payload_t='%s' payload='%s'", + url_t, url.c_str(), payload_t, payload.c_str()); + + call_url(method, url.c_str(), payload.c_str()); + + return true; +} + +void ptz_http_backend::thread_loop() +{ + bool p_changed = true, t_changed = true, z_changed = true; + control_change_s up, ut, uz; + + OBSData user_data; + BPtr ptz_http_id; + + /* Camera settings such as + * "ptz-method" + * "ptz-url" and "ptz-payload" + */ + OBSDataAutoRelease camera_settings; + + /* Camera control function including these objects. + * "p", "t", "z" + */ + OBSDataAutoRelease control_function; + + while (get_ref() > 1) { + if (data->data_changed.exchange(false)) { + blog(LOG_INFO, "%s: data_changed was true", __func__); + + { + std::lock_guard lock(data->mutex); + /* Assuming `data->user_data` won't be touched by the other thread. */ + user_data = data->user_data.Get(); + blog(LOG_INFO, "got user_data=%p", user_data.Get()); + } + + for (obs_data_item_t *item = obs_data_first(user_data); item; obs_data_item_next(&item)) { + const char *name = obs_data_item_get_name(item); + switch (obs_data_item_gettype(item)) { + case OBS_DATA_STRING: + blog(LOG_INFO, "ptz_http_backend::thread_loop: name='%s' value='%s'", name, obs_data_item_get_string(item)); + break; + default: + blog(LOG_INFO, "ptz_http_backend::thread_loop: name='%s'", name); + } + } + + const char *ptz_http_id_new = obs_data_get_string(user_data, "id"); + if (!ptz_http_id_new) + continue; + if (!ptz_http_id || strcmp(ptz_http_id_new, ptz_http_id) != 0) { + ptz_http_id = bstrdup(ptz_http_id_new); + OBSDataAutoRelease camera = get_camera_model(ptz_http_id_new); + if (!camera) { + blog(LOG_ERROR, "Camera model for '%s' was not found", ptz_http_id.Get()); + continue; + } + + camera_settings = obs_data_get_obj(camera, "settings"); + control_function = obs_data_get_obj(camera, "control-function"); + + blog(LOG_INFO, "loaded camera model '%s' %p %p", ptz_http_id_new, camera_settings.Get(), control_function.Get()); + } + } + + if (!user_data || !ptz_http_id || !camera_settings || !control_function) { + blog(LOG_INFO, "%s: user_data=%p ptz_http_id=%p camera_settings=%p control_function=%p", + __func__, user_data.Get(), ptz_http_id.Get(), camera_settings.Get(), control_function.Get()); + os_sleep_ms(500); + continue; + } + + p_changed |= up.update(data->p_next, control_function, "p"); + t_changed |= ut.update(data->t_next, control_function, "t"); + z_changed |= uz.update(data->z_next, control_function, "z"); + + add_control_value(user_data, "p", up); + add_control_value(user_data, "t", ut); + add_control_value(user_data, "z", uz); + + if (p_changed || t_changed || z_changed) { + if (send_ptz(user_data, camera_settings)) { + p_changed = t_changed = z_changed = false; + } + } + + // TODO: If send_ptz failed, try other variant such as send_pt and send_z. + } +} + +void ptz_http_backend::set_config(struct obs_data *user_data) +{ + blog(LOG_INFO, "%s: got user_data=%p", __func__, user_data); + for (obs_data_item_t *item = obs_data_first(user_data); item; obs_data_item_next(&item)) { + const char *name = obs_data_item_get_name(item); + switch (obs_data_item_gettype(item)) { + case OBS_DATA_STRING: + blog(LOG_INFO, "ptz_http_backend::set_config: name='%s' value='%s'", name, obs_data_item_get_string(item)); + break; + default: + blog(LOG_INFO, "ptz_http_backend::set_config: name='%s'", name); + } + } + + std::lock_guard lock(data->mutex); + data->user_data = user_data; + data->data_changed = true; +} + +void ptz_http_backend::set_pantiltzoom_speed(float pan, float tilt, float zoom) +{ + data->p_next = pan; + data->t_next = tilt; + data->z_next = zoom; +} + +static void remove_id_specific_props(obs_properties_t *group) +{ + std::vector names; + + for (obs_property_t *prop = obs_properties_first(group); prop; obs_property_next(&prop)) { + const char *name = obs_property_name(prop); + if (strncmp(name, "ptz.http.", 9) != 0) + continue; + if (strcmp(name, "ptz.http.id") == 0) + continue; + names.push_back(name); + } + + for (const char *name : names) { + obs_properties_remove_by_name(group, name); + } +} + +static void add_id_specific_props(obs_properties_t *group, const char *ptz_http_id) +{ + if (!ptz_http_id) + return; + + OBSDataAutoRelease camera = get_camera_model(ptz_http_id); + if (!camera) { + blog(LOG_ERROR, "Cannot find camera model '%s'", ptz_http_id); + return; + } + + OBSDataAutoRelease properties = obs_data_get_obj(camera, "properties"); + for (obs_data_item_t *item = obs_data_first(properties); item; obs_data_item_next(&item)) { + const char *name = obs_data_item_get_name(item); + OBSDataAutoRelease obj = obs_data_item_get_obj(item); + const char *type = obs_data_get_string(obj, "type"); + const char *description = obs_data_get_string(obj, "description"); + const char *long_description = obs_data_get_string(obj, "long-description"); + obs_property_t *prop; + + if (!type || !description) { + blog(LOG_ERROR, "camera model '%s' has invalid property '%s'", ptz_http_id, name); + continue; + } + + std::string prop_name = "ptz.http."; + prop_name += ptz_http_id; + prop_name += "."; + prop_name += name; + + if (strcmp(type, "string") == 0) { + prop = obs_properties_add_text(group, prop_name.c_str(), description, OBS_TEXT_DEFAULT); + } + else { + blog(LOG_ERROR, "camera model '%s': property '%s': invalid type '%s'", ptz_http_id, name, type); + continue; + } + + if (long_description) + obs_property_set_long_description(prop, long_description); + } +} + +static bool id_modified(obs_properties_t *props, obs_property_t *, obs_data_t *settings) +{ + obs_property_t *group_prop = obs_properties_get(props, "output"); + obs_properties_t *group = obs_property_group_content(group_prop); + + remove_id_specific_props(group); + + add_id_specific_props(group, obs_data_get_string(settings, "ptz.http.id")); + + return true; +} + +static void init_ptz_http_id(obs_properties_t *group_output, obs_property_t *prop, obs_data_t *settings) +{ + obs_property_set_modified_callback(prop, id_modified); + + OBSDataArrayAutoRelease cameras = get_camera_models(); + for (size_t i = 0, n = obs_data_array_count(cameras); i < n; i++) { + OBSDataAutoRelease camera = obs_data_array_item(cameras, i); + + const char *id = obs_data_get_string(camera, "id"); + const char *name = obs_data_get_string(camera, "name"); + + obs_property_list_add_string(prop, name, id); + } + + if (obs_data_has_user_value(settings, "ptz.http.id")) + id_modified(obs_properties_get_parent(group_output), prop, settings); +} + +bool ptz_http_backend::ptz_type_modified(obs_properties_t *group_output, obs_data_t *settings) +{ + if (obs_properties_get(group_output, "ptz.http.id")) + return false; + + obs_property_t *prop = obs_properties_add_list(group_output, "ptz.http.id", obs_module_text("Camera Model"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + init_ptz_http_id(group_output, prop, settings); + + obs_properties_add_text(group_output, "ptz.http.host", obs_module_text("Host"), OBS_TEXT_DEFAULT); + // TODO: Also consider to add basic authentication, if necessary. + + return true; +} diff --git a/src/ptz-http-backend.hpp b/src/ptz-http-backend.hpp new file mode 100644 index 0000000..a578a4a --- /dev/null +++ b/src/ptz-http-backend.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include "ptz-backend.hpp" + +class ptz_http_backend : public ptz_backend +{ + struct ptz_http_backend_data_s *data; + + static void *thread_main(void *); + void thread_loop(); + +public: + ptz_http_backend(); + ~ptz_http_backend() override; + + void set_config(struct obs_data *data) override; + + void set_pantilt_speed(int, int) override { } + void set_zoom_speed(int) override { } + void set_pantiltzoom_speed(float pan, float tilt, float zoom) override; + void recall_preset(int) override { } + float get_zoom() override { + // TODO: Implement if available + return 1.0f; + } + +public: + static bool ptz_type_modified(obs_properties_t *group_output, obs_data_t *settings); +}; From 549613e4349f66679e1a859d2638b6c525ebeee5 Mon Sep 17 00:00:00 2001 From: Norihiro Kamae Date: Sun, 4 Aug 2024 09:00:21 +0900 Subject: [PATCH 2/5] ptz: Add libcurl as the dependency and call it --- .github/workflows/main.yml | 1 + CMakeLists.txt | 3 +++ ci/plugin.spec | 1 + src/ptz-http-backend.cpp | 22 +++++++++++++++++++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f979a0d..1d8b5c9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -64,6 +64,7 @@ jobs: set -ex sudo apt install -y \ libopenblas-dev libopenblas0 \ + libcurl4-openssl-dev \ || true export OPENBLAS_HOME=/lib/x86_64-linux-gnu/ cmake -S . -B build \ diff --git a/CMakeLists.txt b/CMakeLists.txt index d952940..c8fa980 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ find_package(libobs REQUIRED) find_package(obs-frontend-api REQUIRED) include(cmake/ObsPluginHelpers.cmake) find_qt(VERSION ${QT_VERSION} COMPONENTS Widgets Core Gui) +find_package(CURL REQUIRED) if (WITH_DLIB_SUBMODULE) set(CMAKE_POSITION_INDEPENDENT_CODE True) @@ -123,6 +124,7 @@ target_link_libraries(${CMAKE_PROJECT_NAME} OBS::libobs OBS::obs-frontend-api dlib + CURL::libcurl ${plugin_additional_libs} ) @@ -137,6 +139,7 @@ if(OS_WINDOWS) add_definitions("-D_USE_MATH_DEFINES") add_definitions("-D_CRT_SECURE_NO_WARNINGS") # to avoid a warning for `fopen` add_definitions("-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR") # TODO: Remove once requiring OBS 30.2 or later. + target_compile_definitions(${PROJECT_NAME} PRIVATE NOMINMAX) endif() target_link_libraries(${CMAKE_PROJECT_NAME} OBS::w32-pthreads) diff --git a/ci/plugin.spec b/ci/plugin.spec index 44ea9fa..198c9d7 100644 --- a/ci/plugin.spec +++ b/ci/plugin.spec @@ -13,6 +13,7 @@ BuildRequires: obs-studio-devel BuildRequires: qt6-qtbase-devel qt6-qtbase-private-devel BuildRequires: dlib-devel ffmpeg-free-devel sqlite-devel blas-devel lapack-devel BuildRequires: flexiblas-devel +BuildRequires: libcurl-devel # dlib-devel requires /usr/include/ffmpeg so that install ffmpeg-free-devel %package data diff --git a/src/ptz-http-backend.cpp b/src/ptz-http-backend.cpp index 8e32ea8..49429e9 100644 --- a/src/ptz-http-backend.cpp +++ b/src/ptz-http-backend.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "plugin-macros.generated.h" #include "ptz-http-backend.hpp" @@ -185,7 +186,26 @@ void *ptz_http_backend::thread_main(void *data) static void call_url(const char *method, const char *url, const char *payload) { blog(LOG_INFO, "call_url(method='%s', url='%s', payload='%s')", method, url, payload); - // TODO: Implement + + CURL *const c = curl_easy_init(); + if (!c) + return; + + curl_easy_setopt(c, CURLOPT_URL, url); + + // TODO: Implement method, payload, etc. + + char error[CURL_ERROR_SIZE]; + curl_easy_setopt(c, CURLOPT_ERRORBUFFER, error); + + CURLcode code = curl_easy_perform(c); + if (code != CURLE_OK) { + blog(LOG_WARNING, "Failed method='%s' url='%s' %s", + method, url, + strlen(error) ? error : curl_easy_strerror(code)); + } + + curl_easy_cleanup(c); } static bool send_ptz(obs_data_t *user_data, obs_data_t *camera_settings) From cc55709d648b6e046066c083351b565e99ce1ad3 Mon Sep 17 00:00:00 2001 From: Norihiro Kamae Date: Mon, 5 Aug 2024 22:45:16 +0900 Subject: [PATCH 3/5] ptz-http-backend: Open URL --- data/locale/en-US.ini | 5 ++ src/ptz-http-backend.cpp | 121 +++++++++++++++++++++++---------------- 2 files changed, 77 insertions(+), 49 deletions(-) diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 0148cfa..bd11b0f 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -1,2 +1,7 @@ Detector.dlib.hog="HOG, dlib" Detector.dlib.cnn="CNN, dlib" +Prop.ptz.http.host="Host name" +Prop.ptz.http.host.desc="Host name and optionally port number, eg. 192.0.2.101:8080" +Prop.ptz.http.user="User name" +Prop.ptz.http.user.desc="Set if authentication is required. Leave blank if necessary." +Prop.ptz.http.passwd="Password" diff --git a/src/ptz-http-backend.cpp b/src/ptz-http-backend.cpp index 49429e9..8aaf660 100644 --- a/src/ptz-http-backend.cpp +++ b/src/ptz-http-backend.cpp @@ -125,9 +125,6 @@ bool control_change_s::update(float u, obs_data_t *control_function, const char is_int = true; u_int = u_int_next; - blog(LOG_INFO, "control_change_s::update: k1=%f k0=%f u=%f max=%d u_int=%d", - k1, k0, u, max, u_int_next); - return true; } return false; @@ -183,9 +180,32 @@ void *ptz_http_backend::thread_main(void *data) return NULL; } -static void call_url(const char *method, const char *url, const char *payload) +struct read_cb_data +{ + const char *data; + size_t size; +}; + +static size_t read_cb(char *ptr, size_t size, size_t nmemb, void *userdata) +{ + auto *ctx = static_cast(userdata); + size_t ret = std::min(nmemb, ctx->size / size); + + memcpy(ptr, ctx->data, ret * size); + ctx->data += ret * size; + ctx->size -= ret * size; + + return ret; +} + +static void call_url(obs_data_t *data, const char *method, const char *url, const char *payload) { - blog(LOG_INFO, "call_url(method='%s', url='%s', payload='%s')", method, url, payload); + blog(LOG_DEBUG, "call_url(method='%s', url='%s', payload='%s')", method, url, payload); + + struct read_cb_data read_cb_data = { + .data = payload, + .size = strlen(payload), + }; CURL *const c = curl_easy_init(); if (!c) @@ -193,7 +213,23 @@ static void call_url(const char *method, const char *url, const char *payload) curl_easy_setopt(c, CURLOPT_URL, url); - // TODO: Implement method, payload, etc. + const char *user = obs_data_get_string(data, "user"); + const char *passwd = obs_data_get_string(data, "passwd"); + if (user && passwd && *user && *passwd) { + curl_easy_setopt(c, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + std::string up = user; + up += ":"; + up += passwd; + curl_easy_setopt(c, CURLOPT_USERPWD, up.c_str()); + } + + if (strcmp(method, "PUT") == 0) { + curl_easy_setopt(c, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(c, CURLOPT_INFILESIZE_LARGE, (curl_off_t)read_cb_data.size); + + curl_easy_setopt(c, CURLOPT_READFUNCTION, read_cb); + curl_easy_setopt(c, CURLOPT_READDATA, &read_cb_data); + } char error[CURL_ERROR_SIZE]; curl_easy_setopt(c, CURLOPT_ERRORBUFFER, error); @@ -219,10 +255,7 @@ static bool send_ptz(obs_data_t *user_data, obs_data_t *camera_settings) std::string url = replace_placeholder(url_t, user_data); std::string payload = replace_placeholder(payload_t, user_data); - blog(LOG_INFO, "send_ptz: url_t='%s' url='%s' payload_t='%s' payload='%s'", - url_t, url.c_str(), payload_t, payload.c_str()); - - call_url(method, url.c_str(), payload.c_str()); + call_url(user_data, method, url.c_str(), payload.c_str()); return true; } @@ -248,24 +281,10 @@ void ptz_http_backend::thread_loop() while (get_ref() > 1) { if (data->data_changed.exchange(false)) { - blog(LOG_INFO, "%s: data_changed was true", __func__); - { std::lock_guard lock(data->mutex); /* Assuming `data->user_data` won't be touched by the other thread. */ user_data = data->user_data.Get(); - blog(LOG_INFO, "got user_data=%p", user_data.Get()); - } - - for (obs_data_item_t *item = obs_data_first(user_data); item; obs_data_item_next(&item)) { - const char *name = obs_data_item_get_name(item); - switch (obs_data_item_gettype(item)) { - case OBS_DATA_STRING: - blog(LOG_INFO, "ptz_http_backend::thread_loop: name='%s' value='%s'", name, obs_data_item_get_string(item)); - break; - default: - blog(LOG_INFO, "ptz_http_backend::thread_loop: name='%s'", name); - } } const char *ptz_http_id_new = obs_data_get_string(user_data, "id"); @@ -281,14 +300,10 @@ void ptz_http_backend::thread_loop() camera_settings = obs_data_get_obj(camera, "settings"); control_function = obs_data_get_obj(camera, "control-function"); - - blog(LOG_INFO, "loaded camera model '%s' %p %p", ptz_http_id_new, camera_settings.Get(), control_function.Get()); } } if (!user_data || !ptz_http_id || !camera_settings || !control_function) { - blog(LOG_INFO, "%s: user_data=%p ptz_http_id=%p camera_settings=%p control_function=%p", - __func__, user_data.Get(), ptz_http_id.Get(), camera_settings.Get(), control_function.Get()); os_sleep_ms(500); continue; } @@ -313,18 +328,6 @@ void ptz_http_backend::thread_loop() void ptz_http_backend::set_config(struct obs_data *user_data) { - blog(LOG_INFO, "%s: got user_data=%p", __func__, user_data); - for (obs_data_item_t *item = obs_data_first(user_data); item; obs_data_item_next(&item)) { - const char *name = obs_data_item_get_name(item); - switch (obs_data_item_gettype(item)) { - case OBS_DATA_STRING: - blog(LOG_INFO, "ptz_http_backend::set_config: name='%s' value='%s'", name, obs_data_item_get_string(item)); - break; - default: - blog(LOG_INFO, "ptz_http_backend::set_config: name='%s'", name); - } - } - std::lock_guard lock(data->mutex); data->user_data = user_data; data->data_changed = true; @@ -337,7 +340,7 @@ void ptz_http_backend::set_pantiltzoom_speed(float pan, float tilt, float zoom) data->z_next = zoom; } -static void remove_id_specific_props(obs_properties_t *group) +static bool remove_id_specific_props(obs_properties_t *group) { std::vector names; @@ -347,25 +350,35 @@ static void remove_id_specific_props(obs_properties_t *group) continue; if (strcmp(name, "ptz.http.id") == 0) continue; + if (strcmp(name, "ptz.http.host") == 0) + continue; + if (strcmp(name, "ptz.http.user") == 0) + continue; + if (strcmp(name, "ptz.http.passwd") == 0) + continue; names.push_back(name); } for (const char *name : names) { obs_properties_remove_by_name(group, name); } + + return names.size() > 0; } -static void add_id_specific_props(obs_properties_t *group, const char *ptz_http_id) +static bool add_id_specific_props(obs_properties_t *group, const char *ptz_http_id) { if (!ptz_http_id) - return; + return false; OBSDataAutoRelease camera = get_camera_model(ptz_http_id); if (!camera) { blog(LOG_ERROR, "Cannot find camera model '%s'", ptz_http_id); - return; + return false; } + bool modified = false; + OBSDataAutoRelease properties = obs_data_get_obj(camera, "properties"); for (obs_data_item_t *item = obs_data_first(properties); item; obs_data_item_next(&item)) { const char *name = obs_data_item_get_name(item); @@ -393,21 +406,26 @@ static void add_id_specific_props(obs_properties_t *group, const char *ptz_http_ continue; } + modified = true; + if (long_description) obs_property_set_long_description(prop, long_description); } + + return modified; } static bool id_modified(obs_properties_t *props, obs_property_t *, obs_data_t *settings) { + bool modified = false; obs_property_t *group_prop = obs_properties_get(props, "output"); obs_properties_t *group = obs_property_group_content(group_prop); - remove_id_specific_props(group); + modified |= remove_id_specific_props(group); - add_id_specific_props(group, obs_data_get_string(settings, "ptz.http.id")); + modified |= add_id_specific_props(group, obs_data_get_string(settings, "ptz.http.id")); - return true; + return modified; } static void init_ptz_http_id(obs_properties_t *group_output, obs_property_t *prop, obs_data_t *settings) @@ -437,8 +455,13 @@ bool ptz_http_backend::ptz_type_modified(obs_properties_t *group_output, obs_dat OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); init_ptz_http_id(group_output, prop, settings); - obs_properties_add_text(group_output, "ptz.http.host", obs_module_text("Host"), OBS_TEXT_DEFAULT); - // TODO: Also consider to add basic authentication, if necessary. + prop = obs_properties_add_text(group_output, "ptz.http.host", obs_module_text("Prop.ptz.http.host"), OBS_TEXT_DEFAULT); + obs_property_set_long_description(prop, obs_module_text("Prop.ptz.http.host.desc")); + + prop = obs_properties_add_text(group_output, "ptz.http.user", obs_module_text("Prop.ptz.http.user"), OBS_TEXT_DEFAULT); + obs_property_set_long_description(prop, obs_module_text("Prop.ptz.http.user.desc")); + + obs_properties_add_text(group_output, "ptz.http.passwd", obs_module_text("Prop.ptz.http.passwd"), OBS_TEXT_PASSWORD); return true; } From 2ff4bf2d2546264f4a95bdcc20be800bca8a674d Mon Sep 17 00:00:00 2001 From: Norihiro Kamae Date: Sun, 1 Sep 2024 00:16:58 +0900 Subject: [PATCH 4/5] data: Add ptz-http-backend camera model definition file --- ci/plugin.spec | 1 + data/ptz/ptz-http-backend-camera-models.json | 25 ++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 data/ptz/ptz-http-backend-camera-models.json diff --git a/ci/plugin.spec b/ci/plugin.spec index 198c9d7..3f82886 100644 --- a/ci/plugin.spec +++ b/ci/plugin.spec @@ -69,6 +69,7 @@ mv %{buildroot}/%{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/LICENSE-shape_predicto %files %{_libdir}/obs-plugins/@PLUGIN_NAME@.so %{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/locale/ +%{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/ptz/ %{_datadir}/licenses/%{name}/* %files data diff --git a/data/ptz/ptz-http-backend-camera-models.json b/data/ptz/ptz-http-backend-camera-models.json new file mode 100644 index 0000000..1bf5822 --- /dev/null +++ b/data/ptz/ptz-http-backend-camera-models.json @@ -0,0 +1,25 @@ +{ + "camera-models": [ + { + "id": "hikvision", + "name": "HikVision PTZ (HTTP)", + "properties": { + }, + "settings": { + "ptz-method": "PUT", + "ptz-url": "http://{host}/ISAPI/PTZCtrl/channels/1/continuous", + "ptz-payload": "{p}{t}{z}" + }, + "control-function": { + "p": { "type": "linear-int", "k1": 1.0, "k0": 0, "max": 15 }, + "t": { "type": "linear-int", "k1": 1.0, "k0": 0, "max": 15 }, + "z": { "type": "linear-int", "k1": 1.0, "k0": 0, "max": 3 }, + "TODO": "\"max\" should be configurable." + } + }, + { + "id": "sony-srg300se", + "name": "Sony SRG-300SE (HTTP)" + } + ] +} From 6054e1011983d34202128619544492242e9e99bd Mon Sep 17 00:00:00 2001 From: Norihiro Kamae Date: Mon, 16 Sep 2024 18:49:33 +0900 Subject: [PATCH 5/5] ptz-http-backend: Implement seek method When communicating with a PTZ camera, libcurl left a error below. > necessary data rewind wasn't possible --- src/ptz-http-backend.cpp | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/ptz-http-backend.cpp b/src/ptz-http-backend.cpp index 8aaf660..b15644c 100644 --- a/src/ptz-http-backend.cpp +++ b/src/ptz-http-backend.cpp @@ -183,27 +183,52 @@ void *ptz_http_backend::thread_main(void *data) struct read_cb_data { const char *data; + size_t offset; size_t size; }; static size_t read_cb(char *ptr, size_t size, size_t nmemb, void *userdata) { auto *ctx = static_cast(userdata); - size_t ret = std::min(nmemb, ctx->size / size); - memcpy(ptr, ctx->data, ret * size); - ctx->data += ret * size; - ctx->size -= ret * size; + if (ctx->offset > ctx->size) + return 0; + size_t ret = std::min(nmemb, (ctx->size - ctx->offset) / size); + + memcpy(ptr, ctx->data + ctx->offset, ret * size); + ctx->offset += ret * size; return ret; } +static int seek_cb(void *userdata, curl_off_t offset, int origin) +{ + auto *ctx = static_cast(userdata); + + switch (origin) { + case SEEK_SET: + ctx->offset = offset; + break; + case SEEK_CUR: + ctx->offset += offset; + break; + case SEEK_END: + ctx->offset = ctx->size + offset; + break; + default: + return CURL_SEEKFUNC_FAIL; + } + + return CURL_SEEKFUNC_OK; +} + static void call_url(obs_data_t *data, const char *method, const char *url, const char *payload) { blog(LOG_DEBUG, "call_url(method='%s', url='%s', payload='%s')", method, url, payload); struct read_cb_data read_cb_data = { .data = payload, + .offset = 0, .size = strlen(payload), }; @@ -229,6 +254,8 @@ static void call_url(obs_data_t *data, const char *method, const char *url, cons curl_easy_setopt(c, CURLOPT_READFUNCTION, read_cb); curl_easy_setopt(c, CURLOPT_READDATA, &read_cb_data); + curl_easy_setopt(c, CURLOPT_SEEKFUNCTION, seek_cb); + curl_easy_setopt(c, CURLOPT_SEEKDATA, &read_cb_data); } char error[CURL_ERROR_SIZE];