diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 9c52da6..dccac7e 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -4,7 +4,11 @@ "name": "Linux", "includePath": [ "${workspaceFolder}/**", - "/usr/include" + "/usr/include", + "/usr/include/gstreamer-1.0", + "/usr/include/glib-2.0", + "/usr/lib/aarch64-linux-gnu/glib-2.0/include" + // (GStreamer 依赖 GLib, GLib的这个 .include 路径是必须的) ], "defines": [ "CROW_USE_BOOST" diff --git a/CMakeLists.txt b/CMakeLists.txt index 966cb6a..3a77298 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,6 @@ find_package(SQLite3 REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules(GST REQUIRED gstreamer-1.0 gstreamer-app-1.0) find_package(OpenCV REQUIRED) - # 在包含 Crow 子目录之前,设置一个 CMake 选项 # 这会告诉 Crow 的构建脚本去使用 Boost 库中包含的 Asio add_subdirectory(src/vendor/crow) @@ -117,42 +116,23 @@ target_link_libraries(test PRIVATE ) -# 1. 定义新的可执行文件 (我们将把代码放在 src/streamer/ 目录下) -# add_executable(edge_streamer -# src/streamer/main.cpp -# src/streamer/StreamManager.cpp -# ) +add_executable(edge_streamer + src/streamer/main.cpp +) -# # 2. 为新服务链接所有必需的库 -# target_link_libraries(edge_streamer PRIVATE -# # GStreamer (来自我们新增的 pkg_check_modules) -# ${GST_LIBRARIES} +# 链接新服务所需的所有库 +target_link_libraries(edge_streamer PRIVATE + # --- GStreamer (来自 pkg_check_modules) --- + ${GST_LIBRARIES} -# Crow -# nlohmann_json -# PahoMqttCpp::paho-mqttpp3 -# Boost::system -# Boost::thread -# rknn_api -# rknnrt -# pthread -# ssl -# crypto -# ) + pthread +) -# # 3. 为新服务设置头文件包含路径 -# target_include_directories(edge_streamer PRIVATE -# # 新服务的私有头文件 -# ${CMAKE_CURRENT_SOURCE_DIR}/src/streamer/include - -# ${CMAKE_CURRENT_SOURCE_DIR}/src/vendor - -# # GStreamer 的头文件 -# ${GST_INCLUDE_DIRS} - -# ${Boost_INCLUDE_DIRS} - -# # RKNN C-API 的头文件 (来自我们 Dockerfile 的 /usr/local/include) -# /usr/local/include -# ) +# 为新服务设置头文件包含路径 +target_include_directories(edge_streamer PRIVATE + # 新服务的私有头文件 + ${CMAKE_CURRENT_SOURCE_DIR}/src/streamer/include + # --- GStreamer (来自 pkg_check_modules) --- + ${GST_INCLUDE_DIRS} +) diff --git a/src/streamer/main.cpp b/src/streamer/main.cpp new file mode 100644 index 0000000..7fa432d --- /dev/null +++ b/src/streamer/main.cpp @@ -0,0 +1,133 @@ +/* + * 文件: src/streamer/main.cpp + * 目标: 阶段 0.1 - 最小硬件采集 + * 职责: 连接RTSP, 使用MPP硬件解码, 并在C++回调中接收原始帧。 + */ +#include +#include // 包含 appsink 的头文件 +#include + +// 这是一个简单的结构体,用于在回调间传递数据 +typedef struct { + GMainLoop *loop; + GstElement *pipeline; +} AppData; + +/** + * @brief 这是核心回调函数 + * * 当 GStreamer 的 'appsink' 元件成功从管线中拉出一个解码后的帧时, + * 此函数将被调用。 + * * @param appsink 触发信号的 appsink 元件 + * @param user_data 我们传递的自定义数据 + * @return GstFlowReturn 告诉管线上游是否继续发送数据 + */ +static GstFlowReturn +on_new_sample (GstAppSink * appsink, gpointer user_data) +{ + // 帧计数 + static guint frame_count = 0; + + // 从 appsink 中拉取 GstSample + // GstSample 是 GStreamer 中包含数据 (GstBuffer) 和元数据 (GstCaps) 的容器 + 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++; + + 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' + // 必须释放它,否则将导致内存泄漏。 + gst_sample_unref (sample); + + return GST_FLOW_OK; +} + +int +main (int argc, char *argv[]) +{ + AppData data; + GstElement *appsink; + + // 初始化 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 = + "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 ! " + "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"); + 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++ 回调函数 + // 这是整个程序最关键的“粘合”步骤 + 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() 被调用或程序被中断 + 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_main_loop_unref (data.loop); + + return 0; +} \ No newline at end of file