diff --git a/.gitmodules b/.gitmodules index c6211ba1..af38aadf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "www/webassist"] path = www/webassist url = https://gitee.com/victor1002/zlm_webassist +[submodule "3rdpart/pybind11"] + path = 3rdpart/pybind11 + url = https://gitee.com/mirrors/pybind11.git diff --git a/.gitmodules_github b/.gitmodules_github index 87b576ee..f3b18b57 100644 --- a/.gitmodules_github +++ b/.gitmodules_github @@ -9,4 +9,7 @@ url = https://github.com/open-source-parsers/jsoncpp.git [submodule "www/webassist"] path = www/webassist - url = https://github.com/1002victor/zlm_webassist \ No newline at end of file + url = https://github.com/1002victor/zlm_webassist +[submodule "3rdpart/pybind11"] + path = 3rdpart/pybind11 + url = https://github.com/pybind/pybind11.git \ No newline at end of file diff --git a/3rdpart/CMakeLists.txt b/3rdpart/CMakeLists.txt index 845984f6..8a5a534d 100644 --- a/3rdpart/CMakeLists.txt +++ b/3rdpart/CMakeLists.txt @@ -120,4 +120,14 @@ add_subdirectory(ZLToolKit) # 添加库别名 add_library(ZLMediaKit::ToolKit ALIAS ZLToolKit) # 添加依赖 -update_cached_list(MK_LINK_LIBRARIES ZLMediaKit::ToolKit) \ No newline at end of file +update_cached_list(MK_LINK_LIBRARIES ZLMediaKit::ToolKit) + +############################################################################## + +if (ENABLE_PYTHON) + # ============ pybind11 lib ============ + add_subdirectory(pybind11) + update_cached_list(MK_LINK_LIBRARIES pybind11::embed) + include_directories(${CMAKE_CURRENT_SOURCE_DIR}/pybind11/include) + update_cached_list(MK_COMPILE_DEFINITIONS ENABLE_PYTHON) +endif () \ No newline at end of file diff --git a/3rdpart/pybind11 b/3rdpart/pybind11 new file mode 160000 index 00000000..ed5057de --- /dev/null +++ b/3rdpart/pybind11 @@ -0,0 +1 @@ +Subproject commit ed5057ded698e305210269dafa57574ecf964483 diff --git a/CMakeLists.txt b/CMakeLists.txt index 57d01be7..6de5d94b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,7 @@ option(USE_SOLUTION_FOLDERS "Enable solution dir supported" ON) option(ENABLE_OBJCOPY "Enable use objcopy to generate debug info file" ON) # 编译静态库 option(BUILD_SHARED_LIBS "Build shared instead of static" OFF) +option(ENABLE_PYTHON "Enable python plugin" OFF) ############################################################################## # 设置socket默认缓冲区大小为256k.如果设置为0则不设置socket的默认缓冲区大小,使用系统内核默认值(设置为0仅对linux有效) @@ -603,6 +604,10 @@ endif () file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/www" DESTINATION ${EXECUTABLE_OUTPUT_PATH}) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/conf/config.ini" DESTINATION ${EXECUTABLE_OUTPUT_PATH}) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/default.pem" DESTINATION ${EXECUTABLE_OUTPUT_PATH}) +if (ENABLE_PYTHON) + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/python" DESTINATION ${EXECUTABLE_OUTPUT_PATH}) +endif () + if (ENABLE_FFMPEG) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/DejaVuSans.ttf" DESTINATION ${EXECUTABLE_OUTPUT_PATH}) endif () diff --git a/python/mk_logger.py b/python/mk_logger.py new file mode 100644 index 00000000..a0b32328 --- /dev/null +++ b/python/mk_logger.py @@ -0,0 +1,27 @@ +import inspect + +try: + import mk_loader + USE_PLUGIN_LOGGER = True +except ImportError: + USE_PLUGIN_LOGGER = False + +def _do_log(level: int, *args): + frame_info = inspect.stack()[2] + filename = frame_info.filename + lineno = frame_info.lineno + funcname = frame_info.function + + # 把所有参数转成字符串后用空格拼接 + msg = " ".join(str(arg) for arg in args) + + if USE_PLUGIN_LOGGER: + mk_loader.log(level, filename, lineno, funcname, msg) + else: + print(f"[{filename}:{lineno}] {funcname} | {msg}") + +def log_trace(*args): _do_log(0, *args) +def log_debug(*args): _do_log(1, *args) +def log_info(*args): _do_log(2, *args) +def log_warn(*args): _do_log(3, *args) +def log_error(*args): _do_log(4, *args) diff --git a/python/mk_plugin.py b/python/mk_plugin.py new file mode 100644 index 00000000..d7b45032 --- /dev/null +++ b/python/mk_plugin.py @@ -0,0 +1,21 @@ +import mk_logger +import mk_loader + +def on_start(): + mk_logger.log_info("on_start") + + +def on_exit(): + mk_logger.log_info("on_exit") + + +def on_publish(type: str, info, invoker, sender) -> bool: + mk_logger.log_info(f"on_publish: {type}") + # opt 控制转协议,请参考配置文件[protocol]下字段 + opt = { + "enable_rtmp": "1" + } + # 响应推流鉴权结果 + mk_loader.mk_publish_auth_invoker_do(invoker, "", opt); + # 返回True代表此事件被python拦截 + return True diff --git a/server/WebHook.cpp b/server/WebHook.cpp index 5475df8f..236cd1c7 100755 --- a/server/WebHook.cpp +++ b/server/WebHook.cpp @@ -21,6 +21,10 @@ #include "WebHook.h" #include "WebApi.h" +#if defined(ENABLE_PYTHON) +#include "pyinvoker.h" +#endif + using namespace std; using namespace Json; using namespace toolkit; @@ -358,6 +362,12 @@ void installWebHook() { GET_CONFIG(bool, hook_enable, Hook::kEnable); NoticeCenter::Instance().addListener(&web_hook_tag, Broadcast::kBroadcastMediaPublish, [](BroadcastMediaPublishArgs) { +#if defined(ENABLE_PYTHON) + if (PythonInvoker::Instance().on_publish(type, args, invoker, sender)) { + return; + } +#endif + GET_CONFIG(string, hook_publish, Hook::kOnPublish); if (!hook_enable || hook_publish.empty()) { invoker("", ProtocolOption()); diff --git a/server/main.cpp b/server/main.cpp index 3591dcff..a6013bc9 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -43,6 +43,10 @@ #include "ZLMVersion.h" #endif +#if defined(ENABLE_PYTHON) +#include "pyinvoker.h" +#endif + #include "System.h" using namespace std; @@ -107,6 +111,14 @@ onceToken token1([](){ },nullptr); } //namespace RtpProxy +namespace Python { +#define Python_FIELD "python." +const string kPlugin = Python_FIELD"plugin"; +onceToken token1([](){ + mINI::Instance()[kPlugin] = "mk_plugin"; +},nullptr); +} //namespace RtpProxy + } // namespace mediakit @@ -494,6 +506,13 @@ int start_main(int argc,char *argv[]) { g_reload_certificates(); }); #endif + +#if defined(ENABLE_PYTHON) + auto py_plugin = mINI::Instance()[Python::kPlugin]; + if (!py_plugin.empty()) { + PythonInvoker::Instance().load(py_plugin); + } +#endif sem.wait(); } unInstallWebApi(); diff --git a/server/pyinvoker.cpp b/server/pyinvoker.cpp new file mode 100644 index 00000000..f40b4c31 --- /dev/null +++ b/server/pyinvoker.cpp @@ -0,0 +1,155 @@ +#if defined(ENABLE_PYTHON) + +#include "pyinvoker.h" +#include +#include +#include +#include +#include + +using namespace toolkit; +using namespace mediakit; + +template +auto to_python(const T &obj) -> typename std::enable_if::value, py::capsule>::type { + static auto name_str = toolkit::demangle(typeid(T).name()); + auto p = new toolkit::Any(std::make_shared(obj)); + return py::capsule(p, name_str.data(), [](PyObject *capsule) { + auto p = reinterpret_cast(PyCapsule_GetPointer(capsule, name_str.data())); + delete p; + TraceL << "delete " << name_str << "(" << p << ")"; + }); +} + +template +auto to_python(const T &obj) -> typename std::enable_if::value, py::capsule>::type { + static auto name_str = toolkit::demangle(typeid(T).name()); + auto p = new toolkit::Any(std::shared_ptr(const_cast(&obj), [](T *) {})); + return py::capsule(p, name_str.data(), [](PyObject *capsule) { + auto p = reinterpret_cast(PyCapsule_GetPointer(capsule, name_str.data())); + delete p; + TraceL << "unref " << name_str << "(" << p << ")"; + }); +} + +template +T &to_native(const py::capsule &cap) { + static auto name_str = toolkit::demangle(typeid(T).name()); + if (std::string(cap.name()) != name_str) { + throw std::runtime_error("Invalid capsule name!"); + } + auto any = reinterpret_cast(cap.get_pointer()); + return any->get(); +} + +mINI to_native(const py::dict &opt) { + mINI ret; + for (auto &item : opt) { + // 转换为字符串(允许 int/float/bool 等) + ret.emplace(py::str(item.first).cast(), py::str(item.second).cast()); + } + return ret; +} + +PYBIND11_EMBEDDED_MODULE(mk_loader, m) { + m.def("log", [](int lev, const char *file, int line, const char *func, const char *content) { + py::gil_scoped_release release; + LoggerWrapper::printLog(::toolkit::getLogger(), lev, file, func, line, content); + }); + m.def("mk_publish_auth_invoker_do", [](const py::capsule &cap, const std::string &err, const py::dict &opt) { + ProtocolOption option; + option.load(to_native(opt)); + // 执行c++代码时释放gil锁 + py::gil_scoped_release release; + auto &invoker = to_native(cap); + invoker(err, option); + }); +} + +namespace mediakit { + +inline bool set_env(const char *name, const char *value) { +#if defined(_WIN32) + std::string env_str = std::string(name) + "=" + value; + return _putenv(env_str.c_str()) == 0; +#else + return setenv(name, value, 1) == 0; // overwrite = 1 +#endif +} + +bool set_python_path() { + const char *env_var = std::getenv("PYTHONPATH"); + if (env_var && *env_var) { + PrintI("PYTHONPATH is already set to: %s", env_var); + return false; + } + auto default_path = exeDir() + "/python"; + // 1 表示覆盖已存在的值 + if (!set_env("PYTHONPATH", default_path.data())) { + PrintW("Failed to set PYTHONPATH"); + return false; + } + PrintI("PYTHONPATH was not set. Set to default: %s", default_path.data()); + return true; +} + +PythonInvoker &PythonInvoker::Instance() { + static std::shared_ptr instance(new PythonInvoker); + return *instance; +} + +PythonInvoker::PythonInvoker() { + // 确保日志一直可用 + _logger = Logger::Instance().shared_from_this(); + set_python_path(); // 确保 PYTHONPATH 在第一次调用时设置 + _interpreter = new py::scoped_interpreter; + _rel = new py::gil_scoped_release; +} + +PythonInvoker::~PythonInvoker() { + { + py::gil_scoped_acquire gil; // 加锁 + if (_on_exit) { + _on_exit(); + } + _on_exit = py::object(); + _on_publish = py::object(); + _module = py::module(); + } + + delete _rel; + delete _interpreter; +} + +void PythonInvoker::load(const std::string &module_name) { + try { + py::gil_scoped_acquire gil; // 加锁 + _module = py::module::import(module_name.c_str()); + if (hasattr(_module, "on_start")) { + py::object on_start = _module.attr("on_start"); + if (on_start) { + on_start(); + } + } + if (hasattr(_module, "on_exit")) { + _on_exit = _module.attr("on_exit"); + } + if (hasattr(_module, "on_publish")) { + _on_publish = _module.attr("on_publish"); + } + } catch (py::error_already_set &e) { + PrintE("Python exception:%s", e.what()); + } +} + +bool PythonInvoker::on_publish(BroadcastMediaPublishArgs) { + py::gil_scoped_acquire gil; // 确保在 Python 调用期间持有 GIL + if (!_on_publish) { + return false; + } + return _on_publish(getOriginTypeString(type), to_python(args), to_python(invoker), to_python(sender)).cast(); +} + +} // namespace mediakit + +#endif diff --git a/server/pyinvoker.h b/server/pyinvoker.h new file mode 100644 index 00000000..04f266b7 --- /dev/null +++ b/server/pyinvoker.h @@ -0,0 +1,46 @@ + +#ifndef PYINVOKER_H +#define PYINVOKER_H + +#if defined(ENABLE_PYTHON) + +#include +#include +#include +#include +#include "Util/logger.h" +#include "Common/config.h" +#include "Common/MediaSource.h" + +namespace py = pybind11; + +namespace mediakit { + +class PythonInvoker : public std::enable_shared_from_this{ +public: + ~PythonInvoker(); + + static PythonInvoker& Instance(); + + void load(const std::string &module_name); + bool on_publish(BroadcastMediaPublishArgs); + +private: + PythonInvoker(); + +private: + py::gil_scoped_release *_rel; + py::scoped_interpreter *_interpreter; + std::shared_ptr _logger; + py::module _module; + + // 程序退出 + py::object _on_exit; + // 推流鉴权 + py::object _on_publish; +}; + +} // namespace mediakit + +#endif +#endif // PYINVOKER_H \ No newline at end of file