嵌入式上gst rtsp server opencv mat

0 安装gstreamer

sudo apt install libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio

0.1 查看所有插件

gst-inspect-1.0 -a

0.2安装server

注意一定要加上dev

sudo apt install libgstreamer1.0-dev libgstrtspserver-1.0-dev
git clone https://github.com/GStreamer/gst-rtsp-server.git
cd gst-rtsp-server/
git checkout 1.18
cd examples/
gcc test-launch.c -o test-launch $(pkg-config --cflags --libs gstreamer-rtsp-server-1.0)

0.3查看库装在了何处

ldconfig -p | grep libgstrtspserver

0.4查看某个摄像头

gst-launch-1.0 v4l2src device=/dev/video0 ! video/x-raw, format=NV12, width=640, height=480, framerate=30/1 ! autovideosink

1 基础测试视频

#include <gst/gst.h>
#include <gst/rtsp-server/rtsp-server.h>int main(int argc, char *argv[]) {// 初始化GStreamergst_init(&argc, &argv);// 创建RTSP服务器实例GstRTSPServer *server = gst_rtsp_server_new();// 设置服务器监听端口gst_rtsp_server_set_service(server, "8554");// 创建媒体映射与工厂GstRTSPMountPoints *mounts = gst_rtsp_server_get_mount_points(server);GstRTSPMediaFactory *factory = gst_rtsp_media_factory_new();// 创建GStreamer管道gst_rtsp_media_factory_set_launch(factory,"( videotestsrc ! video/x-raw,format=(string)I420,width=640,height=480,framerate=(fraction)30/1 ! x264enc ! rtph264pay name=pay0 pt=96 )");// 将媒体工厂添加到媒体映射gst_rtsp_mount_points_add_factory(mounts, "/test", factory);g_object_unref(mounts);// 启动RTSP服务器gst_rtsp_server_attach(server, NULL);// 进入主循环GMainLoop *loop = g_main_loop_new(NULL, FALSE);g_main_loop_run(loop);// 清理资源g_main_loop_unref(loop);g_object_unref(server);return 0;
}

1.1 makefile

cmake_minimum_required(VERSION 3.0)
project(photo_get_project)
set(CMAKE_CXX_STANDARD 11)#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -pthread -O3 -DNDEBUG")SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -Wall -g -ggdb")
SET(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall -DNDEBUG")
set(CMAKE_C_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall -DNDEBUG")set(EXECUTABLE_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/bin)
# 设置代码构建级别为 Debug 方式 Debug Release
#set(CMAKE_BUILD_TYPE Release)
message(STATUS "yangshao: ${CMAKE_CXX_FLAGS}" )
message(STATUS "OpenCV Libraries: ${OpenCV_LIBS}")
find_package(OpenCV REQUIRED)include_directories(/usr/local/opencv4.8/include/opencv4)
include_directories(/usr/include/gstreamer-1.0)
include_directories(/usr/include/glib-2.0)
include_directories(/usr/lib/aarch64-linux-gnu/glib-2.0/include/)
add_executable(test rtspserver.cpp)
target_link_libraries(test PRIVATE  -lgobject-2.0 glib-2.0 gstreamer-1.0 gstapp-1.0 gstrtspserver-1.0)

1.2 结果

打开两个vlc测试

在这里插入图片描述

2 发送到rtspserver

#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
#include <opencv2/opencv.hpp>class VideoSource {
public:VideoSource(const char* pipeline_str) {pipeline = gst_parse_launch(pipeline_str, NULL);appsrc = GST_APP_SRC(gst_bin_get_by_name(GST_BIN(pipeline), "appsrc0"));// 设置appsrc属性g_object_set(appsrc, "is-live", TRUE, "format", GST_FORMAT_TIME, NULL);// 连接信号g_signal_connect(appsrc, "need-data", G_CALLBACK(OnNeedData), this);g_signal_connect(appsrc, "enough-data", G_CALLBACK(OnEnoughData), this);}~VideoSource() {gst_object_unref(pipeline);}void Start() {gst_element_set_state(pipeline, GST_STATE_PLAYING);}void Stop() {gst_element_set_state(pipeline, GST_STATE_NULL);}private:static gboolean OnNeedData(GstElement *element, guint unused_size, gpointer user_data) {VideoSource *self = static_cast<VideoSource*>(user_data);return self->PushFrame();}static gboolean OnEnoughData(GstElement *element, gpointer user_data) {VideoSource *self = static_cast<VideoSource*>(user_data);self->Stop();return TRUE;}gboolean PushFrame() {// 从cv::Mat中获取帧cv::Mat frame = GetNextFrame();// 将cv::Mat转换为GstBufferGstBuffer *buffer = ConvertMatToGstBuffer(frame);// 将GstBuffer推送到appsrcgst_app_src_push_buffer(GST_APP_SRC(appsrc), buffer);// 如果没有更多的帧,停止管道if (!HasMoreFrames()) {Stop();return FALSE;}return TRUE;}cv::Mat GetNextFrame() {// 在这里实现获取下一帧的逻辑// 例如,从文件、网络流或摄像头读取// 返回一个cv::Mat对象}gboolean HasMoreFrames() {// 实现检查是否还有更多帧的逻辑// 如果没有更多帧,返回FALSE,否则返回TRUE}GstBuffer *ConvertMatToGstBuffer(cv::Mat &frame) {// 将cv::Mat转换为GstBuffer// 注意:这只是一个示例实现,你可能需要根据你的帧格式进行调整GstBuffer *buffer = gst_buffer_new_wrapped(frame.data, frame.total() * frame.elemSize(), 0, frame.total() * frame.elemSize());GstVideoInfo info;gst_video_info_set_format(&info, GST_VIDEO_FORMAT_RGBA, frame.cols, frame.rows, frame.step);gst_buffer_add_video_meta(buffer, &info, 0, frame.cols * frame.rows * frame.channels());return buffer;}GstElement *pipeline;GstElement *appsrc;
};int main(int argc, char *argv[]) {gst_init(&argc, &argv);// RTSP Server的GStreamer管道描述const char* pipeline_str = "appsrc name=appsrc0 is-live=true format=time ""! videoconvert ""! x264enc tune=zerolatency bitrate=500 speed-preset=superfast ""! rtph264pay config-interval=1 name=pay0 pt=96 ""! rtspclientsink location=\"rtsp://localhost:8554/test\"";VideoSource source(pipeline_str);source.Start();// 主循环GMainLoop *loop = g_main_loop_new(NULL, FALSE);g_main_loop_run(loop);g_main_loop_unref(loop);return 0;
}

3 需要cv::mat 作为输入源

#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
#include <opencv2/opencv.hpp>class VideoSource {
public:VideoSource(const char* pipeline_str) {pipeline = gst_parse_launch(pipeline_str, NULL);appsrc = GST_APP_SRC(gst_bin_get_by_name(GST_BIN(pipeline), "appsrc0"));// 设置appsrc属性g_object_set(appsrc, "is-live", TRUE, "format", GST_FORMAT_TIME, NULL);// 连接信号g_signal_connect(appsrc, "need-data", G_CALLBACK(OnNeedData), this);g_signal_connect(appsrc, "enough-data", G_CALLBACK(OnEnoughData), this);}~VideoSource() {gst_object_unref(pipeline);}void Start() {gst_element_set_state(pipeline, GST_STATE_PLAYING);}void Stop() {gst_element_set_state(pipeline, GST_STATE_NULL);}private:static gboolean OnNeedData(GstElement *element, guint unused_size, gpointer user_data) {VideoSource *self = static_cast<VideoSource*>(user_data);return self->PushFrame();}static gboolean OnEnoughData(GstElement *element, gpointer user_data) {VideoSource *self = static_cast<VideoSource*>(user_data);self->Stop();return TRUE;}gboolean PushFrame() {// 从cv::Mat中获取帧cv::Mat frame = GetNextFrame();// 将cv::Mat转换为GstBufferGstBuffer *buffer = ConvertMatToGstBuffer(frame);// 将GstBuffer推送到appsrcgst_app_src_push_buffer(GST_APP_SRC(appsrc), buffer);// 如果没有更多的帧,停止管道if (!HasMoreFrames()) {Stop();return FALSE;}return TRUE;}cv::Mat GetNextFrame() {// 在这里实现获取下一帧的逻辑// 例如,从文件、网络流或摄像头读取// 返回一个cv::Mat对象}gboolean HasMoreFrames() {// 实现检查是否还有更多帧的逻辑// 如果没有更多帧,返回FALSE,否则返回TRUE}GstBuffer *ConvertMatToGstBuffer(cv::Mat &frame) {// 将cv::Mat转换为GstBuffer// 注意:这只是一个示例实现,你可能需要根据你的帧格式进行调整GstBuffer *buffer = gst_buffer_new_wrapped(frame.data, frame.total() * frame.elemSize(), 0, frame.total() * frame.elemSize());GstVideoInfo info;gst_video_info_set_format(&info, GST_VIDEO_FORMAT_RGBA, frame.cols, frame.rows, frame.step);gst_buffer_add_video_meta(buffer, &info, 0, frame.cols * frame.rows * frame.channels());return buffer;}GstElement *pipeline;GstElement *appsrc;
};int main(int argc, char *argv[]) {gst_init(&argc, &argv);// RTSP Server的GStreamer管道描述const char* pipeline_str = "appsrc name=appsrc0 is-live=true format=time ""! videoconvert ""! x264enc tune=zerolatency bitrate=500 speed-preset=superfast ""! rtph264pay config-interval=1 name=pay0 pt=96 ""! rtspclientsink location=\"rtsp://localhost:8554/test\"";VideoSource source(pipeline_str);source.Start();// 主循环GMainLoop *loop = g_main_loop_new(NULL, FALSE);g_main_loop_run(loop);g_main_loop_unref(loop);return 0;
}

4 自身需要作为rtspserver


#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
#include <gst/rtsp-server/gst-rtsp-server.h>
#include <opencv2/opencv.hpp>class RtspsServerApp {
public:RtspsServerApp() {gst_init(NULL, NULL);pipeline = gst_pipeline_new("pipeline");appsrc = gst_element_factory_make("appsrc", "appsrc0");g_object_set(appsrc, "is-live", TRUE, "format", GST_FORMAT_TIME, NULL);g_signal_connect(appsrc, "need-data", G_CALLBACK(OnNeedData), this);g_signal_connect(appsrc, "enough-data", G_CALLBACK(OnEnoughData), this);videoconvert = gst_element_factory_make("videoconvert", "videoconvert");x264enc = gst_element_factory_make("x264enc", "x264enc");rtph264pay = gst_element_factory_make("rtph264pay", "rtph264pay");queue = gst_element_factory_make("queue", "queue");gst_bin_add_many(GST_BIN(pipeline), appsrc, videoconvert, x264enc, rtph264pay, queue, NULL);gst_element_link_many(appsrc, videoconvert, x264enc, rtph264pay, queue, NULL);// Create the RTSP server and mount pointsserver = gst_rtsp_server_new();mounts = gst_rtsp_server_get_mount_points(server);factory = gst_rtsp_media_factory_new();g_object_set(factory, "media-type", "video", NULL);g_object_set(factory, "caps", gst_pad_get_pad_template_caps(GST_ELEMENT(queue)->sinkpad), NULL);// Set up the launch string for the factorygchar *launch = g_strdup_printf("( appsrc name=source ! videoconvert ! x264enc ! rtph264pay name=pay0 pt=96 )");gst_rtsp_media_factory_set_launch(factory, launch);g_free(launch);gst_rtsp_mount_points_add_factory(mounts, "/stream", factory);// Attach the servergst_rtsp_server_attach(server, NULL);}~RtspsServerApp() {gst_rtsp_server_detach(server);gst_object_unref(mounts);gst_object_unref(factory);gst_object_unref(server);gst_object_unref(pipeline);}void Start() {gst_element_set_state(pipeline, GST_STATE_PLAYING);}void Stop() {gst_element_set_state(pipeline, GST_STATE_NULL);}private:static gboolean OnNeedData(GstElement *element, guint unused_size, gpointer user_data) {RtspsServerApp *self = static_cast<RtspsServerApp*>(user_data);return self->PushFrame();}static gboolean OnEnoughData(GstElement *element, gpointer user_data) {RtspsServerApp *self = static_cast<RtspsServerApp*>(user_data);self->Stop();return TRUE;}gboolean PushFrame() {// 假设这里你已经有了一个cv::Mat对象cv::Mat frame = GetNextFrame();// 将cv::Mat转换为GstBufferGstBuffer *buffer = ConvertMatToGstBuffer(frame);// 将GstBuffer推送到appsrcgst_app_src_push_buffer(GST_APP_SRC(appsrc), buffer);return TRUE;}cv::Mat GetNextFrame() {// 实现获取下一帧的逻辑// 返回一个cv::Mat对象}GstBuffer *ConvertMatToGstBuffer(cv::Mat &frame) {// 将cv::Mat转换为GstBuffer// 注意:这只是一个示例实现,你可能需要根据你的帧格式进行调整GstBuffer *buffer = gst_buffer_new_wrapped(frame.data, frame.total() * frame.elemSize(), 0, frame.total() * frame.elemSize());GstVideoInfo info;gst_video_info_set_format(&info, GST_VIDEO_FORMAT_I420, frame.cols, frame.rows, frame.step);gst_buffer_add_video_meta(buffer, &info, 0, frame.cols * frame.rows * frame.channels());return buffer;}GstElement *pipeline;GstElement *appsrc;GstElement *videoconvert;GstElement *x264enc;GstElement *rtph264pay;GstElement *queue;GstRTSPServer *server;GstRTSPMountPoints *mounts;GstRTSPMediaFactory *factory;
};int main(int argc, char *argv[]) {RtspsServerApp app;app.Start();// 主循环GMainLoop *loop = g_main_loop_new(NULL, FALSE);g_main_loop_run(loop);g_main_loop_unref(loop);return 0;
}

直接使用v4l来作为源并且作为rtspserver

经测试是可以多路打开流的,注意这里是软件编码,请自行修改

class RtspServerApp
{
public:RtspServerApp(){// Initialize GStreamergst_init(NULL, NULL);// Create the RTSP server and mount pointsserver = gst_rtsp_server_new();gst_rtsp_server_set_service(server, "8554");mounts = gst_rtsp_server_get_mount_points(server);// Create a media factory for the V4L2 sourcefactory = gst_rtsp_media_factory_new();// g_object_set(factory, "media-type", "video", NULL);g_object_set(factory, "latency", 0, NULL);g_object_set(factory, "shared", TRUE, NULL);// Set up the pipeline for the V4L2 sourcegchar *launch = g_strdup_printf("( v4l2src islive=1 device=/dev/video53 ! video/x-raw,width=640,height=480,framerate=30/1 ! videoconvert ! x264enc ! rtph264pay name=pay0 pt=96 )");gst_rtsp_media_factory_set_launch(factory, launch);gst_rtsp_mount_points_add_factory(mounts, "/test", factory);g_object_unref(mounts);// g_free(launch);// Add the factory to the mount points// gst_rtsp_mount_points_add_factory(mounts, "/test", factory);// Attach the servergst_rtsp_server_attach(server, NULL);}~RtspServerApp(){// gst_rtsp_server_detach(server);gst_object_unref(mounts);gst_object_unref(factory);gst_object_unref(server);}void Start(){// Server will start automatically when attached}private:GstRTSPServer *server;GstRTSPMountPoints *mounts;GstRTSPMediaFactory *factory;
};int main(int argc, char *argv[])
{RtspServerApp app;app.Start();// Main loopGMainLoop *loop = g_main_loop_new(NULL, FALSE);g_main_loop_run(loop);g_main_loop_unref(loop);return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/44315.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

怎么提高音频声音大小?提高音频声音大小的四种方法

怎么提高音频声音大小&#xff1f;在音频处理和编辑中&#xff0c;增加声音的音量是一个常见的需求&#xff0c;尤其是在确保音频清晰度和听觉效果的同时。调整音频的音量不仅仅是简单地提高音频的响度&#xff0c;它也涉及到如何保持音质的高标准&#xff0c;确保没有失真或削…

后端登录校验——Filter过滤器和Interceptor拦截器

一、Filter过滤器 前面我们学会了最先进的会话跟踪技术jwt令牌&#xff0c;那么我们要让用户使用某些功能时就要根据jwt令牌来验证用户身份&#xff0c;来决定他是否登陆了、让不让用户访问这个页面&#xff08;或功能&#xff09; 但是这样一来&#xff0c;没发一个请求&…

绝区零启动遇到的问题

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 ​ 绝区零》作为米哈游的一款全新都…

【机器学习】(基础篇三) —— 线性回归

线性回归 本文介绍最经典的监督学习问题——线性回归&#xff0c;包括单变量线性回归和多变量线性回归 线性回归是回归任务&#xff0c;输入是带有标签的数据&#xff0c;根据数据关系&#xff0c;拟合出一个线性函数&#xff0c;并利用该函数进行预测等操作。线性回归分为单…

Python爬虫:BeautifulSoup的基本使用方法!

1.简介 Beautiful Soup提供一些简单的、python式的函数用来处理导航、搜索、修改分析“标签树”等功能。它是一个工具箱&#xff0c;通过解析文档为用户提供需要抓取的数据&#xff0c;因为简单&#xff0c;所以不需要多少代码就可以写出一个完整的应用程序。 Beautiful Soup…

回收站删除了是不是彻底删除了 回收站删除了怎么找回 回收站删除了还能找回来吗

电脑删除的数据文件一般不会直接被彻底删除掉&#xff0c;而是会暂存在回收站中&#xff0c;这样设计主要是为了防止误删除等操作&#xff0c;如果不小心删除了很重要的文件&#xff0c;只需要在回收站对文件进行还原即可。为了让大家更了解回收站&#xff0c;下面给大家详细讲…

MIT机器人运动控制原理浅析-人形机器人

MIT人形机器人基于开发改进的执行器全新设计&#xff0c;通过可感知执行器运动动力学移动规划器(Actuator-Aware Kino-Dynamic Motion Planner)及着地控制器(Landing Controller)等实现机器人的运动控制。 机器人设计 机器人高0.7米&#xff0c;21KG(四肢重量 25%)&#xff0c;…

生物打印后的生物力学过程

生物打印后的生物力学过程 3D生物打印技术在组织工程领域展现出巨大的潜力&#xff0c;但打印后组织的生物力学特性对其最终成功至关重要。本文将详细介绍打印后组织的生物力学特性及其在组织工程中的应用。 1. 打印后水凝胶交联 原位交联可以在生物打印过程中提供足够的机械…

Python 爬虫与 Java 爬虫:相似之处、不同之处和选项

在信息时代&#xff0c;网络上可用的数据量巨大且不断增长。为了从这些数据中提取有用的信息&#xff0c;爬虫已成为一种重要的技术。Python 和 Java 都是流行的编程语言&#xff0c;都具有强大的爬虫功能。本文将深入探讨 Python 爬虫和 Java 爬虫之间的差异&#xff0c;以帮助…

【RIP实验-熟悉基础配置】

实验拓扑 实验要求 根据实验拓扑的IP地址分配&#xff0c;为所有设备配置对应的IP地址和环回地址。全网运行RIPv2&#xff0c;将R1、R2、R3和R4的物理端口、Loopback地址和10.1.00网段进行宣告。并在rip协议下配置路由自动汇总&#xff0c;观察R1/R2是否能够收到10.0.0.0的详细…

Python虚拟环境:Virtualenv和Pipenv的安装理解与使用

Python虚拟环境&#xff1a;Virtualenv和Pipenv的安装理解与使用 引言 在Python开发中&#xff0c;一个常见的问题是不同项目依赖不同版本的库&#xff0c;这可能导致版本冲突。为解决这个问题&#xff0c;Python社区创造了虚拟环境工具&#xff0c;如Virtualenv和Pipenv。本…

门墙柜加工中心是做什么的?

门墙柜加工中心&#xff0c;带有六工序自动换刀&#xff0c;是一款主要针对门板、衣柜门板、墙板扣件等工件的加工设备。 它可以实现多种加工工艺&#xff0c;如侧孔、三合一、隐性件等连接件&#xff0c;铰链孔&#xff0c;天地铰链槽、门锁孔槽、免拉手槽、海棠槽、灯槽、拉…

Linux系统学习 —— 计算机基础(笔记篇)

一、电脑硬件 电脑硬件由输入&#xff0c;控制计算&#xff0c;输出三部分组成。 输入部分包括键鼠&#xff0c;读卡器&#xff08;外部接口&#xff09;&#xff0c;扫描仪&#xff08;打印机的扫描仪&#xff09;。计算控制部分包括CPU &#xff0c; 内存&#xff0c;硬盘&…

MATLAB数据统计描述和分析

描述性统计就是搜集、整理、加工和分析统计数据&#xff0c; 使之系统化、条理化&#xff0c;以显示出数据资料的趋势、特征和数量关系。它是统计推断的基础&#xff0c;实用性较强&#xff0c;在数学建模的数据描述部分经常使用。 目录 1.频数表和直方图 2 .统计量 3.统计…

Apache Doris:下一代实时数据仓库

Apache Doris&#xff1a;下一代实时数据仓库 概念架构设计快速的原因——其性能的架构设计、特性和机制基于成本的优化器面向列的数据库的快速点查询数据摄取数据更新服务可用性和数据可靠性跨集群复制多租户管理便于使用半结构化数据分析据仓一体分层存储 词条诞生 概念 Apa…

Security认证要点速记

登录校验流程 springSecurity已经为我们默认实现了一个用不着的登录功能&#xff0c;我们需要自己实现个符合我们需求的登录功能&#xff0c;所以我们需要去了解默认登录功能的流程&#xff0c;对其中的部分进行替换 SpringSecurity底层就是过滤器链&#xff0c;包含实现了各种…

HarmonyOS Next应用开发之系统概述

一、鸿蒙系统概述 鸿蒙系统可以分为华为鸿蒙系统&#xff08;HUAWEI HarmonyOS&#xff09;和开源鸿蒙系统&#xff08;OpenHarmony&#xff09;&#xff0c;华为鸿蒙系统是基于OpenHarmony基础之上开发的商业版操作系统。他们二者的关系可以用下图来表示&#xff1a; 1.1、…

使用Go编写的持续下行测速脚本,快速消耗流量且不伤硬盘

介绍 使用go语言编写的持续下行测速脚本,可用于任意平台使用,通过指定URL清单文本文件自动遍历测速,支持多线程,支持多平台 特性 轻量级,无依赖采用内存进行缓存数据,不占用磁盘(如果内存较小请使用gcd项目)&#xff0c;最大程度减少磁盘IO,保护硬盘寿命可自定义最大下载文件…

30米全国地表覆盖数据分享

我们在《136G全国1m土地覆盖数据》一文中&#xff0c;为你分享过全国1米土地覆盖数据。 现在再为你分享30米全国地表覆盖数据&#xff0c;你可以在文末查看该数据的领取方法。 30米全国地表覆盖数据 土地覆盖数据是各项研究中经常使用的数据。 它不仅可以帮助我们快速进行用…

开发个人Go-ChatGPT--6 OpenUI

开发个人Go-ChatGPT–6 OpenUI Open-webui Open WebUI 是一种可扩展、功能丰富且用户友好的自托管 WebUI&#xff0c;旨在完全离线运行。它支持各种 LLM 运行器&#xff0c;包括 Ollama 和 OpenAI 兼容的 API。 功能 由于总所周知的原由&#xff0c;OpenAI 的接口需要密钥才…