diff --git a/config/devices.json b/config/devices.json index 62ba2d1..c462e9c 100644 --- a/config/devices.json +++ b/config/devices.json @@ -1,7 +1,7 @@ { "modbus_rtu_devices": [ { - "enabled": true, + "enabled": false, "device_id": "rtu_temp_sensor_lab", "port_path": "/dev/ttyS7", "baud_rate": 9600, @@ -13,7 +13,7 @@ ] }, { - "enabled": true, + "enabled": false, "device_id": "rotary encoder", "port_path": "/dev/ttyS7", "baud_rate": 9600, diff --git a/hls_streams/live/playlist.m3u8 b/hls_streams/live/playlist.m3u8 new file mode 100644 index 0000000..6091fd1 --- /dev/null +++ b/hls_streams/live/playlist.m3u8 @@ -0,0 +1,15 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:305 +#EXT-X-TARGETDURATION:1 + +#EXTINF:1.0002298355102539, +http://192.168.0.108:8080/streams/live/segment00304.ts +#EXTINF:1.0001586675643921, +http://192.168.0.108:8080/streams/live/segment00305.ts +#EXTINF:1.0011043548583984, +http://192.168.0.108:8080/streams/live/segment00306.ts +#EXTINF:1.0000612735748291, +http://192.168.0.108:8080/streams/live/segment00307.ts +#EXTINF:1.0000095367431641, +http://192.168.0.108:8080/streams/live/segment00308.ts diff --git a/hls_streams/live/segment00307.ts b/hls_streams/live/segment00307.ts new file mode 100644 index 0000000..79da7a8 Binary files /dev/null and b/hls_streams/live/segment00307.ts differ diff --git a/hls_streams/live/segment00308.ts b/hls_streams/live/segment00308.ts new file mode 100644 index 0000000..b0d70a9 Binary files /dev/null and b/hls_streams/live/segment00308.ts differ diff --git a/hls_streams/live/segment00309.ts b/hls_streams/live/segment00309.ts new file mode 100644 index 0000000..c965eda Binary files /dev/null and b/hls_streams/live/segment00309.ts differ diff --git a/src/streamer/main.cpp b/src/streamer/main.cpp index 7fa432d..0054759 100644 --- a/src/streamer/main.cpp +++ b/src/streamer/main.cpp @@ -1,132 +1,154 @@ /* * 文件: src/streamer/main.cpp - * 目标: 阶段 0.1 - 最小硬件采集 - * 职责: 连接RTSP, 使用MPP硬件解码, 并在C++回调中接收原始帧。 + * 目标: 阶段 1.0 - HLS 网页播放 + * 职责: 1. 运行采集管线 (rtspsrc -> ... -> appsink) + * 2. 运行 HLS 编码管线 (appsrc -> ... -> hlssink) + * 3. 在 C++ 回调中,将 (1) 的样本推送给 (2) */ #include -#include // 包含 appsink 的头文件 +#include +#include #include +#include -// 这是一个简单的结构体,用于在回调间传递数据 typedef struct { GMainLoop *loop; - GstElement *pipeline; + GstElement *ingest_pipeline; // 采集管线 + GstElement *hls_pipeline; // HLS 编码管线 + GstElement *hls_appsrc; // HLS 管线的 "appsrc" 元件 } AppData; /** - * @brief 这是核心回调函数 - * * 当 GStreamer 的 'appsink' 元件成功从管线中拉出一个解码后的帧时, - * 此函数将被调用。 - * * @param appsink 触发信号的 appsink 元件 - * @param user_data 我们传递的自定义数据 - * @return GstFlowReturn 告诉管线上游是否继续发送数据 + * @brief 这是核心回调函数 (Callback) + * 当 'appsink' 成功拉出一个解码后的帧时,此函数被调用 */ -static GstFlowReturn -on_new_sample (GstAppSink * appsink, gpointer user_data) +static GstFlowReturn on_new_sample (GstAppSink * appsink, gpointer user_data) { - // 帧计数 + AppData *data = (AppData *) user_data; static guint frame_count = 0; - - // 从 appsink 中拉取 GstSample - // GstSample 是 GStreamer 中包含数据 (GstBuffer) 和元数据 (GstCaps) 的容器 + // 从 appsink 拉取样本 (包含解码后的原始帧) GstSample *sample = gst_app_sink_pull_sample (appsink); - if (sample == NULL) { - g_warning ("Failed to pull sample."); return GST_FLOW_ERROR; } - // --- 核心采集点 --- - // 此时,'sample' 变量中就装着解码后的原始视频帧 (NV12 格式) - // 这是我们未来插入 ONNX/RGA/编码 的地方。 - - frame_count++; + + // 检查 HLS 编码管线的 appsrc 是否已准备好 + if (data->hls_appsrc) { + // 将样本(sample)直接推送到 hls_appsrc + // GStreamer 会自动处理缓冲和线程 + GstFlowReturn ret = gst_app_src_push_sample (GST_APP_SRC (data->hls_appsrc), sample); + if (ret != GST_FLOW_OK) { + g_warning ("Failed to push sample to HLS appsrc"); + } + } + // 打印日志 + frame_count++; if (frame_count % 100 == 0) { g_print ("Hardware decoder acquired frame %u\n", frame_count); - - // (可选的调试: 打印帧的详细信息) - // GstBuffer *buffer = gst_sample_get_buffer(sample); - // GstCaps *caps = gst_sample_get_caps(sample); - // g_print(" Buffer size: %lu, Caps: %s\n", - // gst_buffer_get_size(buffer), - // gst_caps_to_string(caps)); } - // **重要**: 释放 'sample' - // 必须释放它,否则将导致内存泄漏。 + // 【重要】释放样本 + // 无论推送成功与否,我们都必须释放 sample,hls_appsrc 已持有它 gst_sample_unref (sample); - return GST_FLOW_OK; + return GST_FLOW_OK; } -int -main (int argc, char *argv[]) +int main (int argc, char *argv[]) { - AppData data; + AppData data = {0}; // 初始化数据结构 GstElement *appsink; + GError *error = NULL; - // 初始化 GStreamer 和 GMainLoop gst_init (&argc, &argv); data.loop = g_main_loop_new (NULL, FALSE); - // 构建采集管线 (Pipeline String) - // **注意**: 您的摄像头可能不是H.264,如果是H.265,请使用 rtph265depay ! h265parse - // **关键**: appsink 的配置 - // name=sink: 给它一个名字 "sink",方便我们后面找到它 - // emit-signals=true: **必须**设置为 true,它才会触发 'new-sample' 信号 - // max-buffers=5: 设置一个小的内部缓冲区 - // drop=true: 如果C++处理不过来 (缓冲区满了),就丢弃旧的帧 (防止内存溢出) - const gchar *pipeline_str = + // --- 2. 【新增】HLS 路径和目录准备 --- + const char *hls_output_dir = "/app/hls_streams/live"; + const char *playlist_location = "/app/hls_streams/live/playlist.m3u8"; + + g_print("Creating HLS directory: %s\n", hls_output_dir); + g_autofree gchar *mkdir_cmd = g_strdup_printf("mkdir -p %s", hls_output_dir); + system(mkdir_cmd); + + const gchar *ingest_pipeline_str = "rtspsrc location=rtsp://admin:hzx12345@192.168.1.10:554/Streaming/Channels/1301 latency=300 protocols=tcp ! " "application/x-rtp, media=(string)video ! " "rtpjitterbuffer latency=100 ! " - - "rtph265depay ! h265parse ! " - - "queue ! " - - "mppvideodec ! " + "rtph265depay ! h265parse ! queue ! mppvideodec ! " "video/x-raw,format=NV12 ! " "appsink name=sink emit-signals=true max-buffers=5 drop=true"; - g_print ("Using pipeline:\n%s\n", pipeline_str); - - data.pipeline = gst_parse_launch (pipeline_str, NULL); - if (!data.pipeline) { - g_printerr ("Failed to parse ingest pipeline.\n"); + g_print ("Using Ingest pipeline:\n%s\n", ingest_pipeline_str); + data.ingest_pipeline = gst_parse_launch (ingest_pipeline_str, &error); + if (error) { + g_printerr ("Failed to parse ingest pipeline: %s\n", error->message); + g_error_free (error); return -1; } - // 获取 'appsink' 元件 - // 我们通过在管线字符串中设置的 'name=sink' 来找到它 - appsink = gst_bin_get_by_name (GST_BIN (data.pipeline), "sink"); - if (!appsink) { - g_printerr("Failed to get appsink element.\n"); - gst_object_unref(data.pipeline); - return -1; - } - - // 连接 'new-sample' 信号到我们的 C++ 回调函数 - // 这是整个程序最关键的“粘合”步骤 + appsink = gst_bin_get_by_name (GST_BIN (data.ingest_pipeline), "sink"); g_signal_connect (appsink, "new-sample", G_CALLBACK (on_new_sample), &data); - - // appsink 已经设置好,我们可以释放对它的临时引用 g_object_unref (appsink); - // 启动管线 - gst_element_set_state (data.pipeline, GST_STATE_PLAYING); - g_print ("Acquisition pipeline started. Waiting for frames...\n"); - // 运行主循环 - // GStreamer 的所有工作都在后台线程中进行 - // 主循环会阻塞在这里,直到 g_main_loop_quit() 被调用或程序被中断 + // --- 配置 HLS 编码管线 (HLS Pipeline) --- + g_autofree gchar *hls_pipeline_str = g_strdup_printf( + "appsrc name=source ! " // (您的) appsrc,C++代码已为其设置了 1920x1080 NV12 Caps + "mpph264enc ! " // (您的) H.264 硬件编码器 + "h264parse ! " // (您的) H.264 解析器 + "mpegtsmux ! " // [关键修复] 将 H.264 裸流打包成 MPEG-TS 流 + "hlssink " + " playlist_root=http://192.168.0.108:8080/streams/live " + " playlist_location=%s " + " location=%s/segment%%05d.ts " + " target-duration=1 " + " max-files=3", + playlist_location, + hls_output_dir + ); + + g_print ("Using HLS pipeline:\n%s\n", hls_pipeline_str); + data.hls_pipeline = gst_parse_launch (hls_pipeline_str, &error); + if (error) { + g_printerr ("Failed to parse HLS pipeline: %s\n", error->message); + g_error_free (error); + return -1; + } + + data.hls_appsrc = gst_bin_get_by_name (GST_BIN (data.hls_pipeline), "source"); + if (!data.hls_appsrc) { + g_printerr("Failed to get HLS appsrc element.\n"); + return -1; + } + + GstCaps *caps = gst_caps_new_simple ("video/x-raw", + "format", G_TYPE_STRING, "NV12", + "width", G_TYPE_INT, 1920, + "height", G_TYPE_INT, 1080, + "framerate", GST_TYPE_FRACTION, 25, 1, + NULL); + g_object_set (data.hls_appsrc, "caps", caps, NULL); + g_object_set (data.hls_appsrc, "format", GST_FORMAT_TIME, NULL); + gst_caps_unref (caps); + + g_print ("Starting Ingest pipeline...\n"); + gst_element_set_state (data.ingest_pipeline, GST_STATE_PLAYING); + + g_print ("Starting HLS pipeline...\n"); + gst_element_set_state (data.hls_pipeline, GST_STATE_PLAYING); + + g_print ("All pipelines started. Waiting for frames...\n"); g_main_loop_run (data.loop); - // 7. (程序退出时) 清理 - g_print ("Exiting acquisition service...\n"); - gst_element_set_state (data.pipeline, GST_STATE_NULL); - gst_object_unref (data.pipeline); + g_print ("Exiting...\n"); + gst_element_set_state (data.ingest_pipeline, GST_STATE_NULL); + gst_element_set_state (data.hls_pipeline, GST_STATE_NULL); + gst_object_unref (data.ingest_pipeline); + gst_object_unref (data.hls_pipeline); + if (data.hls_appsrc) g_object_unref (data.hls_appsrc); g_main_loop_unref (data.loop); return 0; diff --git a/src/web/web_server.cc b/src/web/web_server.cc index 5e921b7..d6c8320 100644 --- a/src/web/web_server.cc +++ b/src/web/web_server.cc @@ -1,6 +1,8 @@ // 文件名: src/web/web_server.cc #include "web_server.h" #include "spdlog/spdlog.h" +#include +#include // 构造函数现在需要调用基类的构造函数 WebServer::WebServer(SystemMonitor::SystemMonitor& monitor, DeviceManager& deviceManager, LiveDataCache& liveDataCache,uint16_t port) @@ -109,4 +111,44 @@ void WebServer::setup_routes() { return response; }); + CROW_ROUTE((*this), "/streams//") + .methods("GET"_method) + ([this](const crow::request& req, crow::response& res, std::string stream_id, std::string filename) { + + // **安全检查**: 防止路径遍历攻击 + if (filename.find("..") != std::string::npos || stream_id.find("..") != std::string::npos) { + res.code = 400; + res.end("Bad Request"); + return; + } + + // HLS 文件在磁盘上的真实路径 + std::string file_path = "/app/hls_streams/" + stream_id + "/" + filename; + + std::ifstream file(file_path, std::ios::binary); + if (!file.is_open()) { + // 文件未找到 + res.code = 404; + res.end("Not Found"); + return; + } + // 将文件内容读入字符串流 + std::ostringstream contents; + contents << file.rdbuf(); + file.close(); + res.body = contents.str(); + + if (filename.find(".m3u8") != std::string::npos) { + res.set_header("Content-Type", "application/vnd.apple.mpegurl"); + res.set_header("Cache-Control", "no-cache, no-store, must-revalidate"); + res.set_header("Pragma", "no-cache"); + res.set_header("Expires", "0"); + } else if (filename.find(".ts") != std::string::npos) { + res.set_header("Content-Type", "video/MP2T"); + } else { + res.set_header("Content-Type", "application/octet-stream"); + } + res.end(); + }); + } \ No newline at end of file