添加python插件

This commit is contained in:
xia-chu 2025-12-02 11:12:43 +08:00
parent a54a0b35c7
commit 9420f25b73
11 changed files with 302 additions and 2 deletions

3
.gitmodules vendored
View File

@ -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

View File

@ -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
url = https://github.com/1002victor/zlm_webassist
[submodule "3rdpart/pybind11"]
path = 3rdpart/pybind11
url = https://github.com/pybind/pybind11.git

View File

@ -120,4 +120,14 @@ add_subdirectory(ZLToolKit)
#
add_library(ZLMediaKit::ToolKit ALIAS ZLToolKit)
#
update_cached_list(MK_LINK_LIBRARIES ZLMediaKit::ToolKit)
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 ()

1
3rdpart/pybind11 Submodule

@ -0,0 +1 @@
Subproject commit ed5057ded698e305210269dafa57574ecf964483

View File

@ -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)
##############################################################################
# socket256k.0socket,使用系统内核默认值(设置为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 ()

27
python/mk_logger.py Normal file
View File

@ -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)

21
python/mk_plugin.py Normal file
View File

@ -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

View File

@ -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());

View File

@ -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();

155
server/pyinvoker.cpp Normal file
View File

@ -0,0 +1,155 @@
#if defined(ENABLE_PYTHON)
#include "pyinvoker.h"
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <string>
#include <type_traits>
using namespace toolkit;
using namespace mediakit;
template <typename T>
auto to_python(const T &obj) -> typename std::enable_if<std::is_copy_constructible<T>::value, py::capsule>::type {
static auto name_str = toolkit::demangle(typeid(T).name());
auto p = new toolkit::Any(std::make_shared<T>(obj));
return py::capsule(p, name_str.data(), [](PyObject *capsule) {
auto p = reinterpret_cast<toolkit::Any *>(PyCapsule_GetPointer(capsule, name_str.data()));
delete p;
TraceL << "delete " << name_str << "(" << p << ")";
});
}
template <typename T>
auto to_python(const T &obj) -> typename std::enable_if<!std::is_copy_constructible<T>::value, py::capsule>::type {
static auto name_str = toolkit::demangle(typeid(T).name());
auto p = new toolkit::Any(std::shared_ptr<T>(const_cast<T *>(&obj), [](T *) {}));
return py::capsule(p, name_str.data(), [](PyObject *capsule) {
auto p = reinterpret_cast<toolkit::Any *>(PyCapsule_GetPointer(capsule, name_str.data()));
delete p;
TraceL << "unref " << name_str << "(" << p << ")";
});
}
template <typename T>
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<toolkit::Any *>(cap.get_pointer());
return any->get<T>();
}
mINI to_native(const py::dict &opt) {
mINI ret;
for (auto &item : opt) {
// 转换为字符串(允许 int/float/bool 等)
ret.emplace(py::str(item.first).cast<std::string>(), py::str(item.second).cast<std::string>());
}
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<Broadcast::PublishAuthInvoker>(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<PythonInvoker> 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<bool>();
}
} // namespace mediakit
#endif

46
server/pyinvoker.h Normal file
View File

@ -0,0 +1,46 @@

#ifndef PYINVOKER_H
#define PYINVOKER_H
#if defined(ENABLE_PYTHON)
#include <map>
#include <string>
#include <pybind11/embed.h>
#include <pybind11/numpy.h>
#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<PythonInvoker>{
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<toolkit::Logger> _logger;
py::module _module;
// 程序退出
py::object _on_exit;
// 推流鉴权
py::object _on_publish;
};
} // namespace mediakit
#endif
#endif // PYINVOKER_H