You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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;
}

关键注意事项

  1. Queue是强制要求:Tee本身不处理线程同步,每个分支必须加queue,否则会出现流阻塞、丢帧甚至管线崩溃的情况。
  2. 动态分支操作逻辑:
    • 添加分支:创建queue + 下游元素,将元素加入管线,链接队列与下游元素,通过gst_element_get_request_pad获取Tee的动态src pad,链接到队列的sink pad。
    • 移除分支:先解绑pad链接,调用gst_element_release_request_pad释放Tee的src pad,将队列和下游元素从管线中移除,最后销毁元素。
  3. 合流输出扩展:如果需要输出完整的音视频容器文件,可以在音视频Tee之前,先通过matroskamux或mp4mux将音视频打包,再对容器流使用Tee。
  4. 替换为tcpserversink:只需将filesink替换为tcpserversink,并设置端口参数,比如g_object_set(tcp_sink, "port", 5000, NULL),后续分支链接逻辑完全一致。

内容的提问来源于stack exchange,提问作者Paul Jon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 17:44:52