From 0cc31ca9b96fecb6a6585669cf8d5b5ea483d6a5 Mon Sep 17 00:00:00 2001 From: sladro Date: Tue, 6 Jan 2026 10:01:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0hls=E7=9A=84http=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/CMakeLists.txt | 23 +++ plugins/zlm_http/zlm_http_node.cpp | 250 +++++++++++++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 plugins/zlm_http/zlm_http_node.cpp diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 26ce6a5..747be42 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -305,6 +305,29 @@ set_target_properties(storage PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR} ) +# zlm_http plugin (embedded HTTP file server via ZLMediaKit) +if(RK3588_ENABLE_ZLMEDIAKIT AND RK_ZLMK_API_LIB) + add_library(zlm_http SHARED zlm_http/zlm_http_node.cpp) + target_include_directories(zlm_http PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/third_party) + target_compile_definitions(zlm_http PRIVATE RK3588_ENABLE_ZLMEDIAKIT) + target_include_directories(zlm_http PRIVATE ${RK_ZLMEDIAKIT_INCLUDE_DIR}) + target_link_libraries(zlm_http PRIVATE project_options Threads::Threads ${RK_ZLMK_API_LIB}) + set_target_properties(zlm_http PROPERTIES + OUTPUT_NAME "zlm_http" + LIBRARY_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR} + RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR} + BUILD_RPATH "\$ORIGIN" + INSTALL_RPATH "\$ORIGIN" + ) + add_custom_command(TARGET zlm_http POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${RK_ZLMK_API_LIB}" $ + ) + install(TARGETS zlm_http + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins + RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins + ) +endif() + install(TARGETS input_rtsp input_file publish preprocess ai_yolo osd alarm storage ai_scheduler LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins diff --git a/plugins/zlm_http/zlm_http_node.cpp b/plugins/zlm_http/zlm_http_node.cpp new file mode 100644 index 0000000..f68123f --- /dev/null +++ b/plugins/zlm_http/zlm_http_node.cpp @@ -0,0 +1,250 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "node.h" + +#if defined(RK3588_ENABLE_ZLMEDIAKIT) +extern "C" { +#include "mk_events.h" +#include "mk_mediakit.h" +} +#endif + +namespace rk3588 { + +#if defined(RK3588_ENABLE_ZLMEDIAKIT) +namespace { + +struct HttpServeState { + std::mutex mu; + std::string root; + std::string prefix; + uint16_t port = 0; + bool started = false; +}; + +HttpServeState& GState() { + static HttpServeState st; + return st; +} + +static inline bool StartsWith(const std::string& s, const std::string& prefix) { + return s.size() >= prefix.size() && std::equal(prefix.begin(), prefix.end(), s.begin()); +} + +static inline std::string TrimLeadingSlash(std::string s) { + while (!s.empty() && s.front() == '/') s.erase(s.begin()); + return s; +} + +static inline bool HasPathTraversal(const std::string& rel) { + // Minimal hardening: reject obvious traversal and Windows drive/UNC paths. + if (rel.find("..") != std::string::npos) return true; + if (rel.find('\\') != std::string::npos) return true; + if (rel.find(':') != std::string::npos) return true; + return false; +} + +static inline const char* ContentTypeForPath(const std::filesystem::path& p) { + auto ext = p.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return (char)std::tolower(c); }); + if (ext == ".m3u8") return "application/vnd.apple.mpegurl"; + if (ext == ".ts") return "video/mp2t"; + if (ext == ".mp4") return "video/mp4"; + if (ext == ".m4s") return "video/iso.segment"; + if (ext == ".mpd") return "application/dash+xml"; + if (ext == ".vtt") return "text/vtt"; + return "application/octet-stream"; +} + +void API_CALL OnHttpRequest(const mk_parser parser, + const mk_http_response_invoker invoker, + int* consumed, + const mk_sock_info /*sender*/) { + if (!parser || !invoker || !consumed) return; + + const char* method_c = mk_parser_get_method(parser); + const char* url_c = mk_parser_get_url(parser); + if (!method_c || !url_c) return; + + const std::string method(method_c); + const std::string url(url_c); + + std::string root; + std::string prefix; + { + auto& st = GState(); + std::lock_guard lock(st.mu); + root = st.root; + prefix = st.prefix; + } + if (root.empty()) return; + + std::string rel_url = url; + if (!prefix.empty() && prefix != "/") { + if (!StartsWith(rel_url, prefix)) return; + rel_url.erase(0, prefix.size()); + if (rel_url.empty()) rel_url = "/"; + } + rel_url = TrimLeadingSlash(rel_url); + if (rel_url.empty()) return; + if (HasPathTraversal(rel_url)) { + *consumed = 1; + const char* hdr[] = { + "Access-Control-Allow-Origin", "*", + "Cache-Control", "no-cache", + nullptr + }; + mk_http_response_invoker_do_string(invoker, 403, hdr, "Forbidden"); + return; + } + + std::filesystem::path full = std::filesystem::path(root) / std::filesystem::path(rel_url); + std::error_code ec; + if (std::filesystem::is_directory(full, ec)) { + full /= "index.m3u8"; + } + + if (ec || !std::filesystem::exists(full, ec) || ec || !std::filesystem::is_regular_file(full, ec) || ec) { + return; // fallthrough to ZLMediaKit default handlers + } + + const char* ct = ContentTypeForPath(full); + if (method == "OPTIONS") { + *consumed = 1; + const char* hdr[] = { + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "GET, OPTIONS", + "Access-Control-Allow-Headers", "Range, Origin, Accept, Content-Type", + "Access-Control-Max-Age", "86400", + "Cache-Control", "no-cache", + nullptr + }; + mk_http_response_invoker_do_string(invoker, 204, hdr, ""); + return; + } + + if (method != "GET") return; + + *consumed = 1; + const char* hdr[] = { + "Content-Type", ct, + "Access-Control-Allow-Origin", "*", + "Cache-Control", "no-cache", + nullptr + }; + const std::string file_path = full.string(); + mk_http_response_invoker_do_file(invoker, parser, hdr, file_path.c_str()); +} + +void EnsureZlmEnvOnce() { + static std::once_flag once; + std::call_once(once, [] { + mk_config cfg; + std::memset(&cfg, 0, sizeof(cfg)); + cfg.thread_num = 0; + cfg.log_level = 2; + cfg.log_mask = LOG_CONSOLE; + mk_env_init(&cfg); + }); +} + +void EnsureHttpEventsOnce() { + static std::once_flag once; + std::call_once(once, [] { + mk_events ev; + std::memset(&ev, 0, sizeof(ev)); + ev.on_mk_http_request = &OnHttpRequest; + mk_events_listen(&ev); + }); +} + +uint16_t StartHttpServerOnce(uint16_t port, bool ssl) { + auto& st = GState(); + std::lock_guard lock(st.mu); + if (st.started) return st.port; + auto actual = mk_http_server_start(port, ssl ? 1 : 0); + if (actual == 0) return 0; + st.started = true; + st.port = actual; + return actual; +} + +} // namespace +#endif + +class ZlmHttpNode final : public INode { +public: + std::string Id() const override { return id_; } + std::string Type() const override { return "zlm_http"; } + + bool Init(const SimpleJson& config, const NodeContext& /*ctx*/) override { + id_ = config.ValueOr("id", "zlm_http"); + +#if !defined(RK3588_ENABLE_ZLMEDIAKIT) + (void)config; + std::cerr << "[zlm_http] RK3588_ENABLE_ZLMEDIAKIT is OFF at build time\n"; + return false; +#else + root_ = config.ValueOr("root", ""); + if (root_.empty()) root_ = config.ValueOr("path", ""); + prefix_ = config.ValueOr("prefix", "/"); + port_ = static_cast(std::max(0, config.ValueOr("port", 8080))); + ssl_ = config.ValueOr("ssl", false); + + if (root_.empty()) { + std::cerr << "[zlm_http] missing config: root (or path)\n"; + return false; + } + if (prefix_.empty()) prefix_ = "/"; + if (prefix_.front() != '/') prefix_ = "/" + prefix_; + return true; +#endif + } + + bool Start() override { +#if !defined(RK3588_ENABLE_ZLMEDIAKIT) + return false; +#else + EnsureZlmEnvOnce(); + EnsureHttpEventsOnce(); + + { + auto& st = GState(); + std::lock_guard lock(st.mu); + st.root = root_; + st.prefix = prefix_; + } + + auto actual = StartHttpServerOnce(port_, ssl_); + if (actual == 0) { + std::cerr << "[zlm_http] failed to start HTTP server on port " << port_ << "\n"; + return false; + } + std::cout << "[zlm_http] http file server started, port=" << actual + << " root=" << root_ << " prefix=" << prefix_ << "\n"; + return true; +#endif + } + + void Stop() override { + // ZLMediaKit mk_api doesn't expose per-server stop; keep it running for process lifetime. + } + +private: + std::string id_; + std::string root_; + std::string prefix_ = "/"; + uint16_t port_ = 8080; + bool ssl_ = false; +}; + +REGISTER_NODE(ZlmHttpNode, "zlm_http"); + +} // namespace rk3588