如何在C语言中使用GStreamer的Tee拆分H264摄像头输出?
GStreamer插入Tee实现多路输出的解决方案
核心逻辑
你需要为**视频(原生H264)和音频(ALSA输入)**分别创建Tee元素,每个Tee的输出分支必须搭配queue元素(强制要求,用于线程隔离避免流阻塞)。先实现双文件输出,后续动态添加/移除分支的逻辑,本质就是对Tee的动态pad进行链接/解绑操作。
双文件输出的C代码实现
下面是可直接运行的代码,实现摄像头H264视频和ALSA音频分别输出到两组文件(后续替换为tcpserversink只需修改对应元素即可):
#include <gst/gst.h> static gboolean bus_call(GstBus *bus, GstMessage *msg, gpointer data) { GMainLoop *loop = (GMainLoop *)data; switch (GST_MESSAGE_TYPE(msg)) { case GST_MESSAGE_EOS: g_print("流结束\n"); g_main_loop_quit(loop); break; case GST_MESSAGE_ERROR: { gchar *debug; GError *err; gst_message_parse_error(msg, &err, &debug); g_printerr("错误: %s\n", err->message); g_free(debug); g_error_free(err); g_main_loop_quit(loop); break; } default: break; } return TRUE; } int main(int argc, char *argv[]) { GMainLoop *loop; GstElement *pipeline, *v_src, *h264parse, *v_tee, *v_queue1, *v_queue2, *v_sink1, *v_sink2; GstElement *a_src, *a_convert, *a_resample, *a_enc, *a_tee, *a_queue1, *a_queue2, *a_sink1, *a_sink2; GstBus *bus; gst_init(&argc, &argv); loop = g_main_loop_new(NULL, FALSE); // 创建视频流元素 v_src = gst_element_factory_make("v4l2src", "video-source"); h264parse = gst_element_factory_make("h264parse", "h264-parser"); v_tee = gst_element_factory_make("tee", "video-tee"); v_queue1 = gst_element_factory_make("queue", "video-queue1"); v_queue2 = gst_element_factory_make("queue", "video-queue2"); v_sink1 = gst_element_factory_make("filesink", "video-sink1"); v_sink2 = gst_element_factory_make("filesink", "video-sink2"); // 创建音频流元素 a_src = gst_element_factory_make("alsasrc", "audio-source"); a_convert = gst_element_factory_make("audioconvert", "audio-convert"); a_resample = gst_element_factory_make("audioresample", "audio-resample"); a_enc = gst_element_factory_make("opusenc", "audio-encoder"); a_tee = gst_element_factory_make("tee", "audio-tee"); a_queue1 = gst_element_factory_make("queue", "audio-queue1"); a_queue2 = gst_element_factory_make("queue", "audio-queue2"); a_sink1 = gst_element_factory_make("filesink", "audio-sink1"); a_sink2 = gst_element_factory_make("filesink", "audio-sink2"); // 创建主管线 pipeline = gst_pipeline_new("cam-audio-tee-pipeline"); if (!pipeline || !v_src || !h264parse || !v_tee || !v_queue1 || !v_queue2 || !v_sink1 || !v_sink2 || !a_src || !a_convert || !a_resample || !a_enc || !a_tee || !a_queue1 || !a_queue2 || !a_sink1 || !a_sink2) { g_printerr("部分元素创建失败\n"); return -1; } // 配置元素参数 g_object_set(v_src, "device", "/dev/video2", NULL); g_object_set(v_sink1, "location", "video_out1.h264", NULL); g_object_set(v_sink2, "location", "video_out2.h264", NULL); g_object_set(a_sink1, "location", "audio_out1.opus", NULL); g_object_set(a_sink2, "location", "audio_out2.opus", NULL); // 将所有元素加入管线 gst_bin_add_many(GST_BIN(pipeline), v_src, h264parse, v_tee, v_queue1, v_queue2, v_sink1, v_sink2, a_src, a_convert, a_resample, a_enc, a_tee, a_queue1, a_queue2, a_sink1, a_sink2, NULL); // 链接视频主链路:v4l2src -> h264parse -> tee if (!gst_element_link_many(v_src, h264parse, v_tee, NULL)) { g_printerr("视频主链路链接失败\n"); return -1; } // 链接视频分支:queue -> filesink if (!gst_element_link(v_queue1, v_sink1) || !gst_element_link(v_queue2, v_sink2)) { g_printerr("视频分支链路链接失败\n"); return -1; } // 获取Tee动态pad并链接到队列 GstPad *v_tee_pad1 = gst_element_get_request_pad(v_tee, "src_%u"); GstPad *v_queue_pad1 = gst_element_get_static_pad(v_queue1, "sink"); GstPad *v_tee_pad2 = gst_element_get_request_pad(v_tee, "src_%u"); GstPad *v_queue_pad2 = gst_element_get_static_pad(v_queue2, "sink"); if (!gst_pad_link(v_tee_pad1, v_queue_pad1) || !gst_pad_link(v_tee_pad2, v_queue_pad2)) { g_printerr("视频Tee与队列链接失败\n"); return -1; } // 释放pad引用 gst_object_unref(v_tee_pad1); gst_object_unref(v_queue_pad1); gst_object_unref(v_tee_pad2); gst_object_unref(v_queue_pad2); // 链接音频主链路:alsasrc -> audioconvert -> audioresample -> opusenc -> tee if (!gst_element_link_many(a_src, a_convert, a_resample, a_enc, a_tee, NULL)) { g_printerr("音频主链路链接失败\n"); return -1; } // 链接音频分支:queue -> filesink if (!gst_element_link(a_queue1, a_sink1) || !gst_element_link(a_queue2, a_sink2)) { g_printerr("音频分支链路链接失败\n"); return -1; } // 获取音频Tee动态pad并链接到队列 GstPad *a_tee_pad1 = gst_element_get_request_pad(a_tee, "src_%u"); GstPad *a_queue_pad1 = gst_element_get_static_pad(a_queue1, "sink"); GstPad *a_tee_pad2 = gst_element_get_request_pad(a_tee, "src_%u"); GstPad *a_queue_pad2 = gst_element_get_static_pad(a_queue2, "sink"); if (!gst_pad_link(a_tee_pad1, a_queue_pad1) || !gst_pad_link(a_tee_pad2, a_queue_pad2)) { g_printerr("音频Tee与队列链接失败\n"); return -1; } gst_object_unref(a_tee_pad1); gst_object_unref(a_queue_pad1); gst_object_unref(a_tee_pad2); gst_object_unref(a_queue_pad2); // 总线监听错误与EOS事件 bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline)); gst_bus_add_watch(bus, bus_call, loop); gst_object_unref(bus); // 启动管线 gst_element_set_state(pipeline, GST_STATE_PLAYING); g_print("运行中...\n"); g_main_loop_run(loop); // 资源清理 gst_element_set_state(pipeline, GST_STATE_NULL); gst_object_unref(pipeline); g_main_loop_unref(loop); return 0; }
关键注意事项
- Queue是强制要求:Tee本身不处理线程同步,每个分支必须加
queue,否则会出现流阻塞、丢帧甚至管线崩溃的情况。 - 动态分支操作逻辑:
- 添加分支:创建
queue + 下游元素,将元素加入管线,链接队列与下游元素,通过gst_element_get_request_pad获取Tee的动态src pad,链接到队列的sink pad。 - 移除分支:先解绑pad链接,调用
gst_element_release_request_pad释放Tee的src pad,将队列和下游元素从管线中移除,最后销毁元素。
- 添加分支:创建
- 合流输出扩展:如果需要输出完整的音视频容器文件,可以在音视频Tee之前,先通过
matroskamux或mp4mux将音视频打包,再对容器流使用Tee。 - 替换为tcpserversink:只需将
filesink替换为tcpserversink,并设置端口参数,比如g_object_set(tcp_sink, "port", 5000, NULL),后续分支链接逻辑完全一致。
内容的提问来源于stack exchange,提问作者Paul Jon
相关产品推荐
相关产品推荐

