Gstreamer的webrtcbin插件

1、输入参数
static GOptionEntry entries[] = {{"peer-id", 0, 0, G_OPTION_ARG_STRING, &peer_id, "String ID of the peer to connect to", "ID"},{"server", 0, 0, G_OPTION_ARG_STRING, &server_url, "Signalling server to connect to", "URL"},{"disable-ssl", 0, 0, G_OPTION_ARG_NONE, &disable_ssl, "Disable ssl", NULL},{"remote-offerer", 0, 0, G_OPTION_ARG_NONE, &remote_is_offerer, "Request that the peer generate the offer and we'll answer", NULL},{NULL},
};
2、解析参数并连接到信令服务器
int main(int argc, char *argv[])
{GOptionContext *context;GError *error = NULL;sigint_setup();context = g_option_context_new("- gstreamer webrtc sendrecv demo");g_option_context_add_main_entries(context, entries, NULL);g_option_context_add_group(context, gst_init_get_option_group());if (!g_option_context_parse(context, &argc, &argv, &error)){g_printerr("Error initializing: %s\n", error->message);return -1;}if (!check_plugins())return -1;if (!peer_id){g_printerr("--peer-id is a required argument\n");return -1;}//运行 localhost 服务器时禁用 ssl,因为它可能是具有自签名证书的测试服务器{GstUri *uri = gst_uri_from_string(server_url);if (g_strcmp0("localhost", gst_uri_get_host(uri)) == 0 ||g_strcmp0("127.0.0.1", gst_uri_get_host(uri)) == 0)disable_ssl = TRUE;gst_uri_unref(uri);}loop = g_main_loop_new(NULL, FALSE);connect_to_websocket_server_async();g_main_loop_run(loop);g_main_loop_unref(loop);if (pipe1) {gst_element_set_state(GST_ELEMENT(pipe1), GST_STATE_NULL);g_print("Pipeline stopped\n");gst_object_unref(pipe1);}return 0;
}
3、连接到 wss://webrtc.nirbheek.in:8443信令服务器
/* 连接到信令服务器。这是其他一切的入口点 */
static const gchar *server_url = "wss://webrtc.nirbheek.in:8443";static void
connect_to_websocket_server_async(void)
{SoupLogger *logger;SoupMessage *message;SoupSession *session;const char *https_aliases[] = {"wss", NULL};session = soup_session_new_with_options(SOUP_SESSION_SSL_STRICT, !disable_ssl,SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,//SOUP_SESSION_SSL_CA_FILE, "/etc/ssl/certs/ca-bundle.crt",SOUP_SESSION_HTTPS_ALIASES, https_aliases, NULL);logger = soup_logger_new(SOUP_LOGGER_LOG_BODY, -1);soup_session_add_feature(session, SOUP_SESSION_FEATURE(logger));g_object_unref(logger);message = soup_message_new(SOUP_METHOD_GET, server_url);g_print("Connecting to server...\n");/* Once connected, we will register */soup_session_websocket_connect_async(session, message, NULL, NULL, NULL, (GAsyncReadyCallback)on_server_connected, message);app_state = SERVER_CONNECTING;
}
4、连接成功后监听消息和关闭事件并注册服务器
static void
on_server_connected(SoupSession *session, GAsyncResult *res,SoupMessage *msg)
{GError *error = NULL;ws_conn = soup_session_websocket_connect_finish(session, res, &error);if (error){cleanup_and_quit_loop(error->message, SERVER_CONNECTION_ERROR);g_error_free(error);return;}g_assert_nonnull(ws_conn);app_state = SERVER_CONNECTED;g_print("Connected to signalling server\n");g_signal_connect(ws_conn, "closed", G_CALLBACK(on_server_closed), NULL);g_signal_connect(ws_conn, "message", G_CALLBACK(on_server_message), NULL);/* Register with the server so it knows about us and can accept commands */register_with_server();
}
5、向信令服务器注册
static gboolean
register_with_server(void)
{gchar *hello;gint32 our_id;if (soup_websocket_connection_get_state(ws_conn) !=SOUP_WEBSOCKET_STATE_OPEN)return FALSE;our_id = g_random_int_range(10, 10000);g_print("Registering id %i with server\n", our_id);app_state = SERVER_REGISTERING;// 使用随机整数 ID 向服务器注册。回复将通过 on_server_message() 接收hello = g_strdup_printf("HELLO %i", our_id);soup_websocket_connection_send_text(ws_conn, hello);g_free(hello);return TRUE;
}
6、告诉信令服务器连接对端peer_id
static gboolean
setup_call(void)
{gchar *msg;if (soup_websocket_connection_get_state(ws_conn) != SOUP_WEBSOCKET_STATE_OPEN)return FALSE;if (!peer_id)return FALSE;g_print("Setting up signalling server call with %s\n", peer_id);app_state = PEER_CONNECTING;msg = g_strdup_printf("SESSION %s", peer_id);soup_websocket_connection_send_text(ws_conn, msg);g_free(msg);return TRUE;
}
7、处理回复消息
/* One mega message handler for our asynchronous calling mechanism */
static void
on_server_message(SoupWebsocketConnection *conn, SoupWebsocketDataType type,GBytes *message, gpointer user_data)
{// 检测数据是否为文本类型gchar *text;switch (type) {case SOUP_WEBSOCKET_DATA_BINARY:g_printerr("Received unknown binary message, ignoring\n");return;case SOUP_WEBSOCKET_DATA_TEXT: {gsize size;const gchar *data = g_bytes_get_data(message, &size);/* Convert to NULL-terminated string */text = g_strndup(data, size);break;}default:g_assert_not_reached();}// 服务器已接受我们的注册,我们已准备好发送命令if (g_strcmp0(text, "HELLO") == 0) {if (app_state != SERVER_REGISTERING) {cleanup_and_quit_loop("ERROR: Received HELLO when not registering", APP_STATE_ERROR);goto out;}app_state = SERVER_REGISTERED;g_print("Registered with server\n");// 要求信令服务器将我们与特定对等点连接起来if (!setup_call()) {cleanup_and_quit_loop("ERROR: Failed to setup call", PEER_CALL_ERROR);goto out;}/* Call has been setup by the server, now we can start negotiation */} else if (g_strcmp0(text, "SESSION_OK") == 0) {if (app_state != PEER_CONNECTING) {cleanup_and_quit_loop("ERROR: Received SESSION_OK when not calling", PEER_CONNECTION_ERROR);goto out;}app_state = PEER_CONNECTED;// 开始谈判(交换 SDP 和 ICE 候选人)if (!start_pipeline())cleanup_and_quit_loop("ERROR: failed to start pipeline", PEER_CALL_ERROR);} else if (g_str_has_prefix(text, "ERROR")) {  //处理错误消息switch (app_state) {case SERVER_CONNECTING:app_state = SERVER_CONNECTION_ERROR;break;case SERVER_REGISTERING:app_state = SERVER_REGISTRATION_ERROR;break;case PEER_CONNECTING:app_state = PEER_CONNECTION_ERROR;break;case PEER_CONNECTED:case PEER_CALL_NEGOTIATING:app_state = PEER_CALL_ERROR;break;default:app_state = APP_STATE_ERROR;}cleanup_and_quit_loop(text, 0);/* Look for JSON messages containing SDP and ICE candidates */} else {JsonNode *root;JsonObject *object, *child;JsonParser *parser = json_parser_new();if (!json_parser_load_from_data(parser, text, -1, NULL)){g_printerr("Unknown message '%s', ignoring", text);g_object_unref(parser);goto out;}root = json_parser_get_root(parser);if (!JSON_NODE_HOLDS_OBJECT(root)){g_printerr("Unknown json message '%s', ignoring", text);g_object_unref(parser);goto out;}object = json_node_get_object(root);if (json_object_has_member(object, "sdp")) { //远端的sdp消息int ret;GstSDPMessage *sdp;const gchar *text, *sdptype;GstWebRTCSessionDescription *answer;g_assert_cmphex(app_state, ==, PEER_CALL_NEGOTIATING);child = json_object_get_object_member(object, "sdp");if (!json_object_has_member(child, "type")) {cleanup_and_quit_loop("ERROR: received SDP without 'type'", PEER_CALL_ERROR);goto out;}sdptype = json_object_get_string_member(child, "type");// 在这个例子中,我们默认创建了要约并收到一个答案,但可以注释掉要约创建并等待要约,因此我们在这里处理。// 请参阅 gst-plugins-bad 中的 tests/examples/webrtcbidirectional.c 以获取另一个示例,了解如何处理来自同行的要约并使用 webrtcbin 回复答案。 text = json_object_get_string_member(child, "sdp");ret = gst_sdp_message_new(&sdp);g_assert_cmphex(ret, ==, GST_SDP_OK);ret = gst_sdp_message_parse_buffer((guint8 *)text, strlen(text), sdp);g_assert_cmphex(ret, ==, GST_SDP_OK);if (g_str_equal(sdptype, "answer")){ // answer sdp消息g_print("Received answer:\n%s\n", text);answer = gst_webrtc_session_description_new(GST_WEBRTC_SDP_TYPE_ANSWER, sdp);g_assert_nonnull(answer);// 设置远端的sdp到pipeline{GstPromise *promise = gst_promise_new();g_signal_emit_by_name(webrtc1, "set-remote-description", answer, promise);gst_promise_interrupt(promise);gst_promise_unref(promise);}app_state = PEER_CALL_STARTED;} else { // offer sdp消息g_print("Received offer:\n%s\n", text);on_offer_received(sdp);}} else if (json_object_has_member(object, "ice")) { // ICE候选人信息const gchar *candidate;gint sdpmlineindex;child = json_object_get_object_member(object, "ice");candidate = json_object_get_string_member(child, "candidate");sdpmlineindex = json_object_get_int_member(child, "sdpMLineIndex");/* Add ice candidate sent by remote peer */g_signal_emit_by_name(webrtc1, "add-ice-candidate", sdpmlineindex, candidate);} else {g_printerr("Ignoring unknown JSON message:\n%s\n", text);}g_object_unref(parser);}
out:g_free(text);
}
8、创建webrtcbin的Pipeline
#define STUN_SERVER " stun-server=stun://stun.l.google.com:19302 "
#define RTP_CAPS_OPUS "application/x-rtp,media=audio,encoding-name=OPUS,payload="
#define RTP_CAPS_VP8 "application/x-rtp,media=video,encoding-name=VP8,payload="static gboolean
start_pipeline(void)
{GstStateChangeReturn ret;GError *error = NULL;pipe1 = gst_parse_launch("webrtcbin bundle-policy=max-bundle name=sendrecv " STUN_SERVER"videotestsrc is-live=true pattern=ball ! videoconvert ! queue ! vp8enc deadline=1 ! rtpvp8pay ! " "queue ! " RTP_CAPS_VP8 "96 ! sendrecv. " //发送视频"audiotestsrc is-live=true wave=red-noise ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay ! ""queue ! " RTP_CAPS_OPUS "97 ! sendrecv. ", //发送音频&error);if (error) {g_printerr("Failed to parse launch: %s\n", error->message);g_error_free(error);goto err;}// 获取webrtcbin插件对象webrtc1 = gst_bin_get_by_name(GST_BIN(pipe1), "sendrecv");g_assert_nonnull(webrtc1);// 这是我们create offer sdp的 gstwebrtc 入口点。当管道进入播放状态时,它将被调用。g_signal_connect(webrtc1, "on-negotiation-needed", G_CALLBACK(on_negotiation_needed), NULL);// 我们需要通过 websockets 信令服务器将此 ICE 候选传输到浏览器。来自浏览器的传入 ice 候选也需要我们添加,请参阅 on_server_message()g_signal_connect(webrtc1, "on-ice-candidate", G_CALLBACK(send_ice_candidate_message), NULL);g_signal_connect(webrtc1, "notify::ice-gathering-state", G_CALLBACK(on_ice_gathering_state_notify), NULL);gst_element_set_state(pipe1, GST_STATE_READY);g_signal_emit_by_name(webrtc1, "create-data-channel", "channel", NULL, &send_channel);if (send_channel) {g_print("Created data channel\n");connect_data_channel_signals(send_channel);} else {g_print("Could not create data channel, is usrsctp available?\n");}g_signal_connect(webrtc1, "on-data-channel", G_CALLBACK(on_data_channel), NULL);// 接收到音视频数据on_incoming_stream回调函数处理g_signal_connect(webrtc1, "pad-added", G_CALLBACK(on_incoming_stream), pipe1);// 生命周期与管道本身相同, 启动管道gst_object_unref(webrtc1);g_print("Starting pipeline\n");ret = gst_element_set_state(GST_ELEMENT(pipe1), GST_STATE_PLAYING);if (ret == GST_STATE_CHANGE_FAILURE)goto err;return TRUE;
err:if (pipe1)g_clear_object(&pipe1);if (webrtc1)webrtc1 = NULL;return FALSE;
}
9、data-channel数据通道数据通信
static void
connect_data_channel_signals(GObject *data_channel)
{g_signal_connect(data_channel, "on-error", G_CALLBACK(data_channel_on_error), NULL);g_signal_connect(data_channel, "on-open", G_CALLBACK(data_channel_on_open), NULL);g_signal_connect(data_channel, "on-close", G_CALLBACK(data_channel_on_close), NULL);g_signal_connect(data_channel, "on-message-string", G_CALLBACK(data_channel_on_message_string), NULL);
}
10、协商处理
static void
on_negotiation_needed(GstElement *element, gpointer user_data)
{app_state = PEER_CALL_NEGOTIATING;// 是否接收远端的sdp,音视频流if (remote_is_offerer) { //发送指令获取sdpgchar *msg = g_strdup_printf("OFFER_REQUEST");soup_websocket_connection_send_text(ws_conn, msg);g_free(msg);} else {//只发送模式GArray *transceivers;g_signal_emit_by_name(element, "get-transceivers", &transceivers);GstWebRTCRTPTransceiver *transceiver = g_array_index(transceivers, GstWebRTCRTPTransceiver *, 0);transceiver->direction = GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY;g_object_set(transceiver, "fec-type", GST_WEBRTC_FEC_TYPE_ULP_RED, NULL);g_object_set(transceiver, "do-nack", TRUE, NULL);//创建本地offer sdp,获取本地音视频信息GstPromise *promise;promise = gst_promise_new_with_change_func(on_offer_created, user_data, NULL);g_signal_emit_by_name(webrtc1, "create-offer", NULL, promise);}
}
11、获取offer sdp后设置本地并且发送给对端
/* Offer created by our pipeline, to be sent to the peer */
static void
on_offer_created(GstPromise *promise, gpointer user_data)
{GstWebRTCSessionDescription *offer = NULL;const GstStructure *reply;g_assert_cmphex(app_state, ==, PEER_CALL_NEGOTIATING);g_assert_cmphex(gst_promise_wait(promise), ==, GST_PROMISE_RESULT_REPLIED);reply = gst_promise_get_reply(promise);gst_structure_get(reply, "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &offer, NULL);gst_promise_unref(promise);promise = gst_promise_new();g_signal_emit_by_name(webrtc1, "set-local-description", offer, promise);gst_promise_interrupt(promise);gst_promise_unref(promise);/* Send offer to peer */send_sdp_to_peer(offer);gst_webrtc_session_description_free(offer);
}
12、接收音视频数据并解码后调用 on_incoming_decodebin_stream
static void
on_incoming_stream(GstElement *webrtc, GstPad *pad, GstElement *pipe)
{GstElement *decodebin;GstPad *sinkpad;if (GST_PAD_DIRECTION(pad) != GST_PAD_SRC)return;decodebin = gst_element_factory_make("decodebin", NULL);g_signal_connect(decodebin, "pad-added", G_CALLBACK(on_incoming_decodebin_stream), pipe);gst_bin_add(GST_BIN(pipe), decodebin);gst_element_sync_state_with_parent(decodebin);sinkpad = gst_element_get_static_pad(decodebin, "sink");gst_pad_link(pad, sinkpad);gst_object_unref(sinkpad);
}
13、接收解码后的音视频数据并播放
static void
on_incoming_decodebin_stream(GstElement *decodebin, GstPad *pad,GstElement *pipe)
{GstCaps *caps;const gchar *name;if (!gst_pad_has_current_caps(pad)) {g_printerr("Pad '%s' has no caps, can't do anything, ignoring\n", GST_PAD_NAME(pad));return;}caps = gst_pad_get_current_caps(pad);name = gst_structure_get_name(gst_caps_get_structure(caps, 0));if (g_str_has_prefix(name, "video")){handle_media_stream(pad, pipe, "videoconvert", "autovideosink");} else if (g_str_has_prefix(name, "audio")) {handle_media_stream(pad, pipe, "audioconvert", "autoaudiosink");} else {g_printerr("Unknown pad %s, ignoring", GST_PAD_NAME(pad));}
}

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

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

相关文章

Android15音频进阶之组音量调试(九十)

简介: CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布:《Android系统多媒体进阶实战》🚀 优质专栏: Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏: 多媒体系统工程师系列【原创干货持续更新中……】🚀 优质视频课程:AAOS车载系统+…

输出时间序列中的时区是什么Series.dt.tz_convert(tz)

【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 输出时间序列中的时区是什么 Series.dt.tz_convert(tz) [太阳]选择题 关于以下代码的输出结果的说法中正确的是? import pandas as pd t pd.date_range(start2014-08-01 09:00, freq…

【Java】常用方法合集

以 DemoVo 为实体 import lombok.Data; import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;Data ExcelIgnoreUnannotated public class ExportPromoteUnitResult {private String id;ExcelProperty(value &qu…

学习莫烦python---神经网络

一、卷积神经网络区别 1、“卷积” 和 “神经网络”. 卷积也就是说神经网络不再是对每个像素的输入信息做处理了,而是图片上每一小块像素区域进行处理, 这种做法加强了图片信息的连续性. 使得神经网络能看到图形, 而非一个点. 这种做法同时也加深了神经网络对图片的理解 –翻译…

项目结构(后端+前端)(若依)

项目结构(后端前端) 文章目录 项目结构(后端前端)前言一、后端结构1.若依 二、前端结构1. 总结 前言 方便了解项目结构 提示:以下是本篇文章正文内容: 一、后端结构 1.若依 com.ruoyi ├── ruoyi-adm…

SQL语句高级查询(适用于新手)

SQL查询语句的下载脚本链接!!! 【免费】SQL练习资源-具体练习操作可以查看我发布的文章资源-CSDN文库https://download.csdn.net/download/Z0412_J0103/89908378 本文旨在为那些编程基础相对薄弱的朋友们提供一份详尽的指南,特别聚…

【C++篇】栈的层叠与队列的流动:在 STL 的节奏中聆听算法的静谧旋律

文章目录 C 栈与队列详解:基础与进阶应用前言第一章:栈的介绍与使用1.1 栈的介绍1.2 栈的使用1.2.1 最小栈1.2.2 示例与输出 1.3 栈的模拟实现 第二章:队列的介绍与使用2.1 队列的介绍2.2 队列的使用2.2.1 示例与输出 2.3 队列的模拟实现2.3.…

Java项目-基于spingboot框架的篮球论坛系统项目实战(附源码+文档)

作者:计算机学长阿伟 开发技术:SpringBoot、SSM、Vue、MySQL、ElementUI等,“文末源码”。 开发运行环境 开发语言:Java数据库:MySQL技术:SpringBoot、Vue、Mybaits Plus、ELementUI工具:IDEA/…

【计算机网络 - 基础问题】每日 3 题(四十九)

✍个人博客:https://blog.csdn.net/Newin2020?typeblog 📣专栏地址:http://t.csdnimg.cn/fYaBd 📚专栏简介:在这个专栏中,我将会分享 C 面试中常见的面试题给大家~ ❤️如果有收获的话,欢迎点赞…

从Excel文件中提取HTTP链接并在浏览器中打开

本文介绍了一段Python代码,使用openpyxl库从Excel文件中提取包含HTTP或HTTPS链接的单元格,并在默认浏览器中打开这些链接。 使用说明 确保安装openpyxl库,可以使用以下命令安装: pip install openpyxl选择包含链接的Excel文件&…

帝国CMS7.0 7.2 7.5微信登录插件

这款帝国cms微信登录插件,非常方便,新手式设置。 使用方法: 1、将文件夹拷贝到 /e/memberconnect 2、访问 /e/memberconnect/weixin/install/ 进行安装 3、登陆后台 用户–>外部接口–>管理外部登陆接口 输入 appid、appkey 4、前台链…

MATLAB人脸考勤系统

MATLAB人脸考勤系统课题介绍 该课题为基于MATLAB平台的人脸识别系统。传统的人脸识别都是直接人头的比对,现实意义不大,没有一定的新意。该课题识别原理为:先采集待识别人员的人脸,进行训练,得到人脸特征值。测试的时…

【计网笔记】应用层

应用层服务传输层协议端口号DNSTCP或UDP53FTPTCP20或21TELNETTCP23HTTPTCP80HTTPSTCP443SMTPTCP25POP3TCP110IMAPTCP143MIME//DHCPUDP67SNMPUDP161 域名系统DNS 使用具有一定语义的域名来助记IP地址 一个域名可能有多个IP地址 服务器农场 前端调配请求去不同的服务器不同服务…

机器学习面试笔试知识点之K近邻算法(KNN)、最大期望算法(EM)

机器学习面试笔试知识点之K近邻算法KNN、最大期望算法EM 一、K近邻算法(KNN)(监督学习算法)1. 什么是KNN1.1 KNN的通俗解释1.2 近邻的距离度量1.3 K值选择1.4 KNN最近邻分类算法的过程 2.KDD的实现:KD树2.1 构建KD树2.2 KD树的插入2.3 KD树的…

vue 项目i18n国际化,快速抽离中文,快速翻译

国际化大家都知道vue-i18n 实现的,但是有个问题,就是繁杂的抽离中文字符的过程,以及翻译中文字符的过程,关于这个有些小工具可以希望可以帮到大家 1.安装vue-i18n npm i vue-i18n8.22.22.ElementUI多语言配置 在src目录下创建…

【UE5】将2D切片图渲染为体积纹理,最终实现使用RT实时绘制体积纹理【第六篇-阶段总结篇】

因为马上就要进入下一个阶段,制作动态编辑体积纹理的模块。 但在这之前,要在这一章做最后一些整理。 首先,我们完成没完成的部分。其次,最后整理一下图表。最后,本文附上正在用的贴图 完善Shader 还记得我们之前注…

联想ThinkPad笔记本怎么开启vt_ThinkPad笔记本开启vt虚拟化教程

最近使用联想ThinkPad笔记本的小伙伴们问我,联想ThinkPad笔记本怎么开启vt虚拟。大多数可以在Bios中开启vt虚拟化技术,当CPU支持VT-x虚拟化技术,有些电脑会自动开启VT-x虚拟化技术功能。而大部分的电脑则需要在Bios Setup界面中,手…

Florence-2视觉语言模型简明教程

近年来,计算机视觉领域见证了基础模型的兴起,这些模型无需训练自定义模型即可进行图像注释。我们已经看到了用于分类的 CLIP [2]、用于对象检测的 GroundingDINO [3] 和用于分割的 SAM [4] 等模型——每个模型都在其领域表现出色。但是,如果我…

将VSCode界面的显示语言改为简体中文,切换VScode界面的显示语言

VSCode界面默认的语言为英语,需要安装简体中文语言包,语言包为插件 (Extension)。 Step1: 点击左侧扩展按钮 Step2: 在搜索栏 ,输入chinese Step3: 选择第一个简体中文,点击右下角install 按钮 Step4: 安装…

【K8S系列】Kubernetes node节点NotReady问题及解决方案详解【已解决】

Kubernetes 集群中的每个节点都是运行容器化应用的基础。当节点状态显示为 NotReady 时,意味着该节点无法正常工作,这可能会导致 Pod 无法调度,从而影响整个应用的可用性。本文将深入分析节点不健康的各种原因、详细的排查步骤以及有效的解决方案。 一、节点不健康的原因 节…