增加HLS转发协议
This commit is contained in:
parent
f42ee41573
commit
337a02a9ec
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"modbus_rtu_devices": [
|
"modbus_rtu_devices": [
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"device_id": "rtu_temp_sensor_lab",
|
"device_id": "rtu_temp_sensor_lab",
|
||||||
"port_path": "/dev/ttyS7",
|
"port_path": "/dev/ttyS7",
|
||||||
"baud_rate": 9600,
|
"baud_rate": 9600,
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"device_id": "rotary encoder",
|
"device_id": "rotary encoder",
|
||||||
"port_path": "/dev/ttyS7",
|
"port_path": "/dev/ttyS7",
|
||||||
"baud_rate": 9600,
|
"baud_rate": 9600,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,132 +1,154 @@
|
||||||
/*
|
/*
|
||||||
* 文件: src/streamer/main.cpp
|
* 文件: src/streamer/main.cpp
|
||||||
* 目标: 阶段 0.1 - 最小硬件采集
|
* 目标: 阶段 1.0 - HLS 网页播放
|
||||||
* 职责: 连接RTSP, 使用MPP硬件解码, 并在C++回调中接收原始帧。
|
* 职责: 1. 运行采集管线 (rtspsrc -> ... -> appsink)
|
||||||
|
* 2. 运行 HLS 编码管线 (appsrc -> ... -> hlssink)
|
||||||
|
* 3. 在 C++ 回调中,将 (1) 的样本推送给 (2)
|
||||||
*/
|
*/
|
||||||
#include <gst/gst.h>
|
#include <gst/gst.h>
|
||||||
#include <gst/app/gstappsink.h> // 包含 appsink 的头文件
|
#include <gst/app/gstappsink.h>
|
||||||
|
#include <gst/app/gstappsrc.h>
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
// 这是一个简单的结构体,用于在回调间传递数据
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
GMainLoop *loop;
|
GMainLoop *loop;
|
||||||
GstElement *pipeline;
|
GstElement *ingest_pipeline; // 采集管线
|
||||||
|
GstElement *hls_pipeline; // HLS 编码管线
|
||||||
|
GstElement *hls_appsrc; // HLS 管线的 "appsrc" 元件
|
||||||
} AppData;
|
} AppData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 这是核心回调函数
|
* @brief 这是核心回调函数 (Callback)
|
||||||
* * 当 GStreamer 的 'appsink' 元件成功从管线中拉出一个解码后的帧时,
|
* 当 'appsink' 成功拉出一个解码后的帧时,此函数被调用
|
||||||
* 此函数将被调用。
|
|
||||||
* * @param appsink 触发信号的 appsink 元件
|
|
||||||
* @param user_data 我们传递的自定义数据
|
|
||||||
* @return GstFlowReturn 告诉管线上游是否继续发送数据
|
|
||||||
*/
|
*/
|
||||||
static GstFlowReturn
|
static GstFlowReturn on_new_sample (GstAppSink * appsink, gpointer user_data)
|
||||||
on_new_sample (GstAppSink * appsink, gpointer user_data)
|
|
||||||
{
|
{
|
||||||
// 帧计数
|
AppData *data = (AppData *) user_data;
|
||||||
static guint frame_count = 0;
|
static guint frame_count = 0;
|
||||||
|
// 从 appsink 拉取样本 (包含解码后的原始帧)
|
||||||
// 从 appsink 中拉取 GstSample
|
|
||||||
// GstSample 是 GStreamer 中包含数据 (GstBuffer) 和元数据 (GstCaps) 的容器
|
|
||||||
GstSample *sample = gst_app_sink_pull_sample (appsink);
|
GstSample *sample = gst_app_sink_pull_sample (appsink);
|
||||||
|
|
||||||
if (sample == NULL) {
|
if (sample == NULL) {
|
||||||
g_warning ("Failed to pull sample.");
|
|
||||||
return GST_FLOW_ERROR;
|
return GST_FLOW_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 核心采集点 ---
|
|
||||||
// 此时,'sample' 变量中就装着解码后的原始视频帧 (NV12 格式)
|
|
||||||
// 这是我们未来插入 ONNX/RGA/编码 的地方。
|
|
||||||
|
|
||||||
frame_count++;
|
// 检查 HLS 编码管线的 appsrc 是否已准备好
|
||||||
|
if (data->hls_appsrc) {
|
||||||
if (frame_count % 100 == 0) {
|
// 将样本(sample)直接推送到 hls_appsrc
|
||||||
g_print ("Hardware decoder acquired frame %u\n", frame_count);
|
// GStreamer 会自动处理缓冲和线程
|
||||||
|
GstFlowReturn ret = gst_app_src_push_sample (GST_APP_SRC (data->hls_appsrc), sample);
|
||||||
// (可选的调试: 打印帧的详细信息)
|
if (ret != GST_FLOW_OK) {
|
||||||
// GstBuffer *buffer = gst_sample_get_buffer(sample);
|
g_warning ("Failed to push sample to HLS appsrc");
|
||||||
// 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'
|
// 打印日志
|
||||||
// 必须释放它,否则将导致内存泄漏。
|
frame_count++;
|
||||||
|
if (frame_count % 100 == 0) {
|
||||||
|
g_print ("Hardware decoder acquired frame %u\n", frame_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 【重要】释放样本
|
||||||
|
// 无论推送成功与否,我们都必须释放 sample,hls_appsrc 已持有它
|
||||||
gst_sample_unref (sample);
|
gst_sample_unref (sample);
|
||||||
|
|
||||||
return GST_FLOW_OK;
|
return GST_FLOW_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
int main (int argc, char *argv[])
|
||||||
main (int argc, char *argv[])
|
|
||||||
{
|
{
|
||||||
AppData data;
|
AppData data = {0}; // 初始化数据结构
|
||||||
GstElement *appsink;
|
GstElement *appsink;
|
||||||
|
GError *error = NULL;
|
||||||
|
|
||||||
// 初始化 GStreamer 和 GMainLoop
|
|
||||||
gst_init (&argc, &argv);
|
gst_init (&argc, &argv);
|
||||||
data.loop = g_main_loop_new (NULL, FALSE);
|
data.loop = g_main_loop_new (NULL, FALSE);
|
||||||
|
|
||||||
// 构建采集管线 (Pipeline String)
|
// --- 2. 【新增】HLS 路径和目录准备 ---
|
||||||
// **注意**: 您的摄像头可能不是H.264,如果是H.265,请使用 rtph265depay ! h265parse
|
const char *hls_output_dir = "/app/hls_streams/live";
|
||||||
// **关键**: appsink 的配置
|
const char *playlist_location = "/app/hls_streams/live/playlist.m3u8";
|
||||||
// name=sink: 给它一个名字 "sink",方便我们后面找到它
|
|
||||||
// emit-signals=true: **必须**设置为 true,它才会触发 'new-sample' 信号
|
g_print("Creating HLS directory: %s\n", hls_output_dir);
|
||||||
// max-buffers=5: 设置一个小的内部缓冲区
|
g_autofree gchar *mkdir_cmd = g_strdup_printf("mkdir -p %s", hls_output_dir);
|
||||||
// drop=true: 如果C++处理不过来 (缓冲区满了),就丢弃旧的帧 (防止内存溢出)
|
system(mkdir_cmd);
|
||||||
const gchar *pipeline_str =
|
|
||||||
|
const gchar *ingest_pipeline_str =
|
||||||
"rtspsrc location=rtsp://admin:hzx12345@192.168.1.10:554/Streaming/Channels/1301 latency=300 protocols=tcp ! "
|
"rtspsrc location=rtsp://admin:hzx12345@192.168.1.10:554/Streaming/Channels/1301 latency=300 protocols=tcp ! "
|
||||||
"application/x-rtp, media=(string)video ! "
|
"application/x-rtp, media=(string)video ! "
|
||||||
"rtpjitterbuffer latency=100 ! "
|
"rtpjitterbuffer latency=100 ! "
|
||||||
|
"rtph265depay ! h265parse ! queue ! mppvideodec ! "
|
||||||
"rtph265depay ! h265parse ! "
|
|
||||||
|
|
||||||
"queue ! "
|
|
||||||
|
|
||||||
"mppvideodec ! "
|
|
||||||
"video/x-raw,format=NV12 ! "
|
"video/x-raw,format=NV12 ! "
|
||||||
"appsink name=sink emit-signals=true max-buffers=5 drop=true";
|
"appsink name=sink emit-signals=true max-buffers=5 drop=true";
|
||||||
|
|
||||||
g_print ("Using pipeline:\n%s\n", pipeline_str);
|
g_print ("Using Ingest pipeline:\n%s\n", ingest_pipeline_str);
|
||||||
|
data.ingest_pipeline = gst_parse_launch (ingest_pipeline_str, &error);
|
||||||
data.pipeline = gst_parse_launch (pipeline_str, NULL);
|
if (error) {
|
||||||
if (!data.pipeline) {
|
g_printerr ("Failed to parse ingest pipeline: %s\n", error->message);
|
||||||
g_printerr ("Failed to parse ingest pipeline.\n");
|
g_error_free (error);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 'appsink' 元件
|
appsink = gst_bin_get_by_name (GST_BIN (data.ingest_pipeline), "sink");
|
||||||
// 我们通过在管线字符串中设置的 '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++ 回调函数
|
|
||||||
// 这是整个程序最关键的“粘合”步骤
|
|
||||||
g_signal_connect (appsink, "new-sample", G_CALLBACK (on_new_sample), &data);
|
g_signal_connect (appsink, "new-sample", G_CALLBACK (on_new_sample), &data);
|
||||||
|
|
||||||
// appsink 已经设置好,我们可以释放对它的临时引用
|
|
||||||
g_object_unref (appsink);
|
g_object_unref (appsink);
|
||||||
|
|
||||||
// 启动管线
|
|
||||||
gst_element_set_state (data.pipeline, GST_STATE_PLAYING);
|
|
||||||
g_print ("Acquisition pipeline started. Waiting for frames...\n");
|
|
||||||
|
|
||||||
// 运行主循环
|
// --- 配置 HLS 编码管线 (HLS Pipeline) ---
|
||||||
// GStreamer 的所有工作都在后台线程中进行
|
g_autofree gchar *hls_pipeline_str = g_strdup_printf(
|
||||||
// 主循环会阻塞在这里,直到 g_main_loop_quit() 被调用或程序被中断
|
"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);
|
g_main_loop_run (data.loop);
|
||||||
|
|
||||||
// 7. (程序退出时) 清理
|
g_print ("Exiting...\n");
|
||||||
g_print ("Exiting acquisition service...\n");
|
gst_element_set_state (data.ingest_pipeline, GST_STATE_NULL);
|
||||||
gst_element_set_state (data.pipeline, GST_STATE_NULL);
|
gst_element_set_state (data.hls_pipeline, GST_STATE_NULL);
|
||||||
gst_object_unref (data.pipeline);
|
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);
|
g_main_loop_unref (data.loop);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// 文件名: src/web/web_server.cc
|
// 文件名: src/web/web_server.cc
|
||||||
#include "web_server.h"
|
#include "web_server.h"
|
||||||
#include "spdlog/spdlog.h"
|
#include "spdlog/spdlog.h"
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
// 构造函数现在需要调用基类的构造函数
|
// 构造函数现在需要调用基类的构造函数
|
||||||
WebServer::WebServer(SystemMonitor::SystemMonitor& monitor, DeviceManager& deviceManager, LiveDataCache& liveDataCache,uint16_t port)
|
WebServer::WebServer(SystemMonitor::SystemMonitor& monitor, DeviceManager& deviceManager, LiveDataCache& liveDataCache,uint16_t port)
|
||||||
|
|
@ -109,4 +111,44 @@ void WebServer::setup_routes() {
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CROW_ROUTE((*this), "/streams/<string>/<string>")
|
||||||
|
.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();
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue