美文网首页音视频开发笔记
GStreamer 对于 WebRTC 的支持

GStreamer 对于 WebRTC 的支持

作者: 老瓦在霸都 | 来源:发表于2023-09-11 23:07 被阅读0次

    1. GStreamer 简介

    GStreamer 是与 FFmeg 齐名的多媒体框架,它最引人注目的就是它的 pipeline 和 plugin 机制,提供了高度可定制化的扩展性。

    有关 Gstreamer 的基础知识请参见

    这里不在赘述,只简单重温一下基本概念

    gstreamer

    什么是 Element?

    GStreamer应用中最重要的对象是GstElement对象,element是多媒体pipeline基本的构建组件,所有的高级组件都集成自GstElement。

    Gstreamer中主要有三种elements:sink element,src element,filter-like element,element的类型由其具备哪些pads决定

    什么 bin ?

    GstBin可以将一系列elements组合形成一个逻辑上的element,以便从整体上操控和管理elements。也就是说 Bin是一个可以启动的element的集合,包含source、filter和sink。

    • 最外层的bin即使pipeline。
    • GstBin管理它内部elements的状态。

    什么是 Bus

    GstBus是将stream线程消息转发给应用程序线程的系统。

    • GstBus本身运行在应用程序的上下文中,但能够自动监听GStreamer内的线程。
    • 每条pipeline都自带一条GstBus,开发人员仅需为其设定handler以便在接收到消息是能或者正确的处理。

    什么是Pad?

    Pad是一个element与外部交互的接口,数据从一个element的src-pad传递给另一个element的sink-pad。Pad的Capabilities表明element能处理的数据。

    什么是Capabilities?

    Capabilities是用于描述一个pad能够处理或正在处理的数据类型的机制。GStreamer使用GstCaps描述pads的capabilities,一个GstCaps将含有一个或多个GStructure来描述媒体类型,但对于已经完成negotiation的pad,其GstCaps的GStructure是唯一的,并且属性值是固定的。

    2. WebrtcBin 简介

    webrtcbin 是由 Matthew Waters 编写的 GStreamer 插件,使用此插件,您可以连接到网络浏览器或者其他 WebRTC 端点或服务器进行音视频的实时传输。 它实现点对点连接握手(使用 ICE 和外部 STUN 服务器)、或者在无法直连时重新路由数据包(使用外部 TURN 服务器中转),然后维护传输音视频数据包的会话(DTLS, SCTP 和 SRTP)

    不过需要设计自己的信令协议, 实现自己的信令服务器(这里有一个简单的信令服务的例子)来交换 SDP 和 ICE candidate,并处理数据包丢失和重传,管理网络拥塞并调整编码比特率,以在不同质量的网络上保持可接受的用户体验。例如常用的 NACK/PLI, FEC, RTX, Congestion Control method(REMB and TWCC)

    参见 https://gstreamer.freedesktop.org/documentation/webrtc/index.html?gi-language=c

    GObject
        ╰──GInitiallyUnowned
            ╰──GstObject
                ╰──GstElement
                    ╰──GstBin
                        ╰──webrtcbin
    

    它有一个sink pad 和一个 src pad:

    • GstWebRTCBinSinkPad
    GObject
        ╰──GInitiallyUnowned
            ╰──GstObject
                ╰──GstPad
                    ╰──GstProxyPad
                        ╰──GstGhostPad
                            ╰──GstWebRTCBinPad
                                ╰──GstWebRTCBinSinkPad
    
    • GstWebRTCBinSrcPad
    GObject
        ╰──GInitiallyUnowned
            ╰──GstObject
                ╰──GstPad
                    ╰──GstProxyPad
                        ╰──GstGhostPad
                            ╰──GstWebRTCBinPad
                                ╰──GstWebRTCBinSrcPad
    

    3. WebrtcBin 使用实例

    -- 注:这里的例子编译自 https://blog.nirbheek.in/2018/02/gstreamer-webrtc.html

    • 首先我们从摄像头中捕获视频,并将视频流发送至 webrtc 的对端,并从对端接收视频流,第一步是构建视频流水线, 由插件 v4l2src 捕获视频流,放在 queue 中,由 vp8enc 来进行 vp8 编码,由 rtpvp8pay 将编码过的视频流封装成 rtp 包,再由 webrtcbin 插件通过 RTCPeerConnection 发送出去(DTLS/SRTP), 这其中要经过 SDP 协商和 ICE Candidate 协商
    GstElement *pipe;
    
    pipe = gst_parse_launch ("v4l2src ! queue ! vp8enc ! rtpvp8pay ! "
        "application/x-rtp,media=video,encoding-name=VP8,payload=96 !"
        " webrtcbin name=sendrecv", NULL);
    
    
    • 获取一个 webrtcbin 的引用,并设置它的一些回调.
    GstElement *webrtc;
    
    webrtc = gst_bin_get_by_name (GST_BIN (pipe), "sendrecv");
    g_assert (webrtc != NULL);
    
    /* This is the gstwebrtc entry point where we create the offer.
     * It will be called when the pipeline goes to PLAYING. */
    g_signal_connect (webrtc, "on-negotiation-needed",
        G_CALLBACK (on_negotiation_needed), NULL);
    /* We will transmit this ICE candidate to the remote using some
     * signalling. Incoming ICE candidates from the remote need to be
     * added by us too. */
    g_signal_connect (webrtc, "on-ice-candidate",
        G_CALLBACK (send_ice_candidate_message), NULL);
    /* Incoming streams will be exposed via this signal */
    g_signal_connect (webrtc, "pad-added",
        G_CALLBACK (on_incoming_stream), pipe);
    /* Lifetime is the same as the pipeline itself */
    gst_object_unref (webrtc);
    
    • 当 pipeline 状态变成 PLAYING, on_negotiation_needed() 回调函数将被调用, 我们将请求 webrtcbin 去创建一个匹配以上 pipeline 的 offer
    static void
    on_negotiation_needed (GstElement * webrtc, gpointer user_data)
    {
      GstPromise *promise;
    
      promise = gst_promise_new_with_change_func (on_offer_created,
          user_data, NULL);
      g_signal_emit_by_name (webrtc, "create-offer", NULL,
          promise);
    }
    
    • 当 webrtcbin 创建 SDP Offer 后,其会调用 on_offer_created() 回调函数
    static void
    on_offer_created (GstPromise * promise, GstElement * webrtc)
    {
      GstWebRTCSessionDescription *offer = NULL;
      const GstStructure *reply;
      gchar *desc;
    
      reply = gst_promise_get_reply (promise);
      gst_structure_get (reply, "offer",
          GST_TYPE_WEBRTC_SESSION_DESCRIPTION, 
          &offer, NULL);
      gst_promise_unref (promise);
    
      /* We can edit this offer before setting and sending */
      g_signal_emit_by_name (webrtc,
          "set-local-description", offer, NULL);
    
      /* Implement this and send offer to peer using signalling */
      send_sdp_offer (offer);
      gst_webrtc_session_description_free (offer);
    }
    
    • 类似的,当我们从远端接收到 SDP answer 时,我们必须调用 webrtcbin 的 set-remote-description 方法.
    answer = gst_webrtc_session_description_new (
        GST_WEBRTC_SDP_TYPE_ANSWER, sdp);
    g_assert (answer);
    
    /* Set remote description on our pipeline */
    g_signal_emit_by_name (webrtc, "set-remote-description",  answer, NULL);
    

    ICE 的处理也是类似的,当"on-ice-candidate" 信号触发时,我们会获得一个 local ICE candidate, 并需要发送到远端,而当我们从远端收到一个 ICE candidate 时,我们需要调用 webrtcbin 的 "add-ice-candidate" 方法。

    现在拼图只剩下一块了,那就是处理从远端收到的媒体流。为此,我们要将 on_incoming_stream() 附加到 webrtcbin 有 "pad-added" 信号。

    static void
    on_incoming_stream (GstElement * webrtc, GstPad * pad,
        GstElement * pipe)
    {
      GstElement *play;
    
      play = gst_parse_bin_from_description (
          "queue ! vp8dec ! videoconvert ! autovideosink",
          TRUE, NULL);
      gst_bin_add (GST_BIN (pipe), play);
    
      /* Start displaying video */
      gst_element_sync_state_with_parent (play);
      gst_element_link (webrtc, play);
    }
    

    这就是基本的 webrtc 工作流程。 那些以前使用过 PeerConnection API 的人会很高兴看到这与调用 Web JS API 的流程非常接近。

    1. SDP 协商
    sdp negotiation
    1. ICE 检查
    ice candidate exchange

    快速测试

    参考 https://github.com/centricular/gstwebrtc-demos 的示例 sendrecv: 发送和接收音视频

    • 复制示例代码
    git clone https://github.com/centricular/gstwebrtc-demos.git
    
    • js/ 目录放到网站的根目录下,或者打开 https://webrtc.nirbheek.in

    • 此段 JS 代码假设信令服务在 Web 服务器的端口 8443 侦听

    • 打开网站,确保状态为 "Registered with server, waiting for call", 并记下 id

    • 编译 C 代码

    $ cd gst
    $ gcc webrtc-sendrecv.c $(pkg-config --cflags --libs gstreamer-webrtc-1.0 gstreamer-sdp-1.0 libsoup-2.4 json-glib-1.0) -o webrtc-sendrecv
    
    • 运行 webrtc-sendrecv --peer-id=IDid 即在浏览器上显示的 peerId.

    例如:

    1. 打开 https://webrtc.nirbheek.in/, 显示的 peer id 为 5323
    2. 运行 ./webrtc-sendrecv --peer-id=5323
    3. 在浏览器上可以看到跳动的小球视频
    > GET  HTTP/1.1
    > Soup-Debug-Timestamp: 1692837597
    > Soup-Debug: SoupSession 1 (0x557842e85100), SoupMessage 1 (0x5578431cf8e0), SoupSocket 1 (0x557842d51c40)
    > Host: webrtc.nirbheek.in:8443
    > Upgrade: websocket
    > Connection: Upgrade
    > Sec-WebSocket-Key: e/WoYP6Gr85M4iqaZYTd3g==
    > Sec-WebSocket-Version: 13
    > Accept-Encoding: gzip, deflate
    
    < HTTP/1.1 101 Switching Protocols
    < Soup-Debug-Timestamp: 1692837597
    < Soup-Debug: SoupMessage 1 (0x5578431cf8e0)
    < Upgrade: websocket
    < Connection: Upgrade
    < Sec-WebSocket-Accept: jnGmBhPRHb9szcxQnGoSZAe5WMc=
    < Date: Thu, 24 Aug 2023 00:39:08 GMT
    < Server: Python/3.11 websockets/11.0.2
    
    Connected to signalling server
    Registering id 8634 with server
    Registered with server
    Setting up signalling server call with 5323
    Created data channel
    Starting pipeline
    Sending offer:
    v=0
    o=- 4050401179331307259 0 IN IP4 0.0.0.0
    s=-
    t=0 0
    a=ice-options:trickle
    a=group:BUNDLE video0 audio1 application2
    m=video 9 UDP/TLS/RTP/SAVPF 96
    c=IN IP4 0.0.0.0
    a=setup:actpass
    a=ice-ufrag:SSUHGjApzvSrB6fACYzhFG6WFQpL4qrh
    a=ice-pwd:scHMe5CJ57NpqnHp70OzrGSmAI1kJoCB
    a=rtcp-mux
    a=rtcp-rsize
    a=sendrecv
    a=rtpmap:96 VP8/90000
    a=rtcp-fb:96 nack pli
    a=rtcp-fb:96 transport-cc
    a=framerate:30
    a=ssrc:199344106 msid:user3365093335@host-d6d503be webrtctransceiver0
    a=ssrc:199344106 cname:user3365093335@host-d6d503be
    a=mid:video0
    a=fingerprint:sha-256 3B:D3:71:E5:B4:AA:A1:A3:D9:6A:22:8E:D3:49:27:6F:55:83:AC:A7:A2:E2:6B:88:3C:53:F7:E0:1D:8D:A7:B0
    a=rtcp-mux-only
    m=audio 0 UDP/TLS/RTP/SAVPF 97
    c=IN IP4 0.0.0.0
    a=setup:actpass
    a=ice-ufrag:SSUHGjApzvSrB6fACYzhFG6WFQpL4qrh
    a=ice-pwd:scHMe5CJ57NpqnHp70OzrGSmAI1kJoCB
    a=bundle-only
    a=rtcp-mux
    a=rtcp-rsize
    a=sendrecv
    a=rtpmap:97 OPUS/48000/2
    a=rtcp-fb:97 transport-cc
    a=fmtp:97 sprop-stereo=0;sprop-maxcapturerate=48000
    a=ssrc:1019736707 msid:user3365093335@host-d6d503be webrtctransceiver1
    a=ssrc:1019736707 cname:user3365093335@host-d6d503be
    a=mid:audio1
    a=fingerprint:sha-256 3B:D3:71:E5:B4:AA:A1:A3:D9:6A:22:8E:D3:49:27:6F:55:83:AC:A7:A2:E2:6B:88:3C:53:F7:E0:1D:8D:A7:B0
    a=rtcp-mux-only
    m=application 0 UDP/DTLS/SCTP webrtc-datachannel
    c=IN IP4 0.0.0.0
    a=setup:actpass
    a=ice-ufrag:SSUHGjApzvSrB6fACYzhFG6WFQpL4qrh
    a=ice-pwd:scHMe5CJ57NpqnHp70OzrGSmAI1kJoCB
    a=bundle-only
    a=mid:application2
    a=sctp-port:5000
    a=fingerprint:sha-256 3B:D3:71:E5:B4:AA:A1:A3:D9:6A:22:8E:D3:49:27:6F:55:83:AC:A7:A2:E2:6B:88:3C:53:F7:E0:1D:8D:A7:B0
    
    ICE gathering state changed to gathering
    Received answer:
    v=0
    o=- 5928460106182311303 2 IN IP4 127.0.0.1
    s=-
    t=0 0
    a=group:BUNDLE video0 audio1 application2
    a=msid-semantic: WMS 80f0e9a6-a2ba-434e-94f0-4d8d0d0e18fe
    m=video 9 UDP/TLS/RTP/SAVPF 96
    c=IN IP4 0.0.0.0
    a=rtcp:9 IN IP4 0.0.0.0
    a=ice-ufrag:aV9x
    a=ice-pwd:1hBp2fLGi7bW+QZvEs9C58Du
    a=ice-options:trickle
    a=fingerprint:sha-256 59:8C:C4:46:92:99:D6:7B:71:D9:7C:AD:CB:8C:5D:50:96:B6:70:37:CC:F9:EF:25:FF:34:F2:6E:34:52:8F:CA
    a=setup:active
    a=mid:video0
    a=sendrecv
    a=rtcp-mux
    a=rtcp-rsize
    a=rtpmap:96 VP8/90000
    a=rtcp-fb:96 transport-cc
    a=rtcp-fb:96 nack pli
    a=ssrc:1621313799 cname:C+l5r9FJua3urdCO
    a=ssrc:1621313799 msid:80f0e9a6-a2ba-434e-94f0-4d8d0d0e18fe 19830fc8-a2d5-4d54-8aa7-cfe22b1720d3
    m=audio 9 UDP/TLS/RTP/SAVPF 97
    c=IN IP4 0.0.0.0
    a=rtcp:9 IN IP4 0.0.0.0
    a=ice-ufrag:aV9x
    a=ice-pwd:1hBp2fLGi7bW+QZvEs9C58Du
    a=ice-options:trickle
    a=fingerprint:sha-256 59:8C:C4:46:92:99:D6:7B:71:D9:7C:AD:CB:8C:5D:50:96:B6:70:37:CC:F9:EF:25:FF:34:F2:6E:34:52:8F:CA
    a=setup:active
    a=mid:audio1
    a=sendrecv
    a=rtcp-mux
    a=rtpmap:97 OPUS/48000/2
    a=rtcp-fb:97 transport-cc
    a=fmtp:97 minptime=10;useinbandfec=1
    a=ssrc:1489599020 cname:C+l5r9FJua3urdCO
    a=ssrc:1489599020 msid:80f0e9a6-a2ba-434e-94f0-4d8d0d0e18fe c64ee897-790a-497c-8edd-06d08d1691c3
    m=application 9 UDP/DTLS/SCTP webrtc-datachannel
    c=IN IP4 0.0.0.0
    a=ice-ufrag:aV9x
    a=ice-pwd:1hBp2fLGi7bW+QZvEs9C58Du
    a=ice-options:trickle
    a=fingerprint:sha-256 59:8C:C4:46:92:99:D6:7B:71:D9:7C:AD:CB:8C:5D:50:96:B6:70:37:CC:F9:EF:25:FF:34:F2:6E:34:52:8F:CA
    a=setup:active
    a=mid:application2
    a=sctp-port:5000
    
    data channel opened
    data channel opened
    Received data channel message: Hi! (from browser)
    

    参考资料

    相关文章

      网友评论

        本文标题:GStreamer 对于 WebRTC 的支持

        本文链接:https://www.haomeiwen.com/subject/ihtimdtx.html