webrtc datachannel的协商

ZLMediaKit支持webrtc接入,按正常流程,第一步需要协商出音视频通道。
但方案是只协商出datachannel,sdp如下:

web 发起offer:

v=0^M
o=- 7161281774595815373 2 IN IP4 127.0.0.1^M
s=-^M
t=0 0^M
a=group:BUNDLE 0^M
a=extmap-allow-mixed^M
a=msid-semantic: WMS^M
m=application 9 UDP/DTLS/SCTP webrtc-datachannel^M
c=IN IP4 0.0.0.0^M
a=ice-ufrag:2rgo^M
a=ice-pwd:UpLdvvTbYzdPeYmgLBiCD15D^M
a=ice-options:trickle^M
a=fingerprint:sha-256 25:31:BD:29:3C:FB:50:4E:F5:32:A8:61:7A:D2:75:E3:D0:BA:8E:F7:B8:D9:63:8B:AA:31:26:81:94:A9:D1:B3^M
a=setup:actpass^M
a=mid:0^M
a=sctp-port:5000^M
a=max-message-size:262144^M

ZLMediaKit响应的answer:

v=0
o=- 7161281774595815373 2 IN IP4 172.16.63.52
s=-
t=0 0
a=group:BUNDLE 0
a=msid-semantic: WMS
a=ice-lite
m=application 8000 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 172.16.63.52
a=rtcp:8000 IN IP4 172.16.63.52
a=ice-ufrag:zlm_1
a=ice-pwd:QXOSSrdX7wKA6v6OZWWnEcFy
a=ice-options:trickle
a=fingerprint:sha-256 47:99:1F:A6:6F:B0:0C:E8:1F:A3:C0:E2:CD:D7:9B:C9:A3:33:67:50:E4:74:2A:73:68:98:DC:09:F3:78:71:AB
a=setup:passive
a=mid:0
a=ice-lite
a=sctp-port:5000
a=candidate:udpcandidate 1 udp 120 172.16.63.52 8000 typ host
a=candidate:tcpcandidate 1 tcp 115 172.16.63.52 8000 typ host tcptype passive

可以看到sdp中只带一个m行,类型为webrtc-datachannel,在offer的sdp中有一个表示可以发送最大发送消息长度的属性行 a=max-message-size:262144。rtp包的长度不会大于mute,一般是在1500个字节一下,远远小于这个大小。

转发视频流

只有webrtc datachannel通道,ZLMediaKit不会转发视频流。需要修改逻辑,使其也能转发视频流。

编译安装usrcsctp库

ZLMediaKit中的datachannel的实现也是基于usrsctp库,所以首先需要编译安装usrsctp,步骤比较简单这里就不列举。
安装完usrsctp后,再执行ZLMediaKit的cmake时,会打印 -- WebRTC datachannel 功能已打开

逻辑修改

在ZLMediaKit中,WebRtcTransport类负责处理webrtc接入的所有流程及媒体流的转发。

  1. sdp协商成功后,接下的流程就是进行DTLS的协商,void WebRtcTransport::OnDtlsTransportConnected就是DTLS协商成功后的回调。
  2. DTLS协商成功后,接下来就是收发媒体流(如果协商了音视频通道)和自定义消息(如果协商了datachannel通道)。
  3. 在前面一篇文章提到过,data channel通道基于DTLS,处理data channel消息的堆栈如下:

static int onRecvSctpData
SctpAssociation::ProcessSctpData
WebRtcTransport::OnDtlsTransportApplicationDataReceived
WebRtcTransportImp::OnDtlsTransportApplicationDataReceive
DtlsTransport::ProcessDtlsData

sctp消息都会到OnRecvSctpData处理。

  1. usrsctp也有一个sctp连接成功的回调,OnSctpAssociationConnected,在这个回调中就表示可以发送sctp消息了,那么在该回调中添加如下转流逻辑
void WebRtcPlayer::OnSctpAssociationConnected(RTC::SctpAssociation *sctpAssociation) {
    auto playSrc = _play_src.lock();
    if(!playSrc){
        onShutdown(SockException(Err_shutdown, "rtsp media source was shutdown"));
        return ;
    }

    if (isOnlyDatachannel()) {
        playSrc->pause(false);
        _reader = playSrc->getRing()->attach(getPoller(), true);
        weak_ptr<WebRtcPlayer> weak_self = static_pointer_cast<WebRtcPlayer>(shared_from_this());
        weak_ptr<Session> weak_session = static_pointer_cast<Session>(getSession());
        _reader->setGetInfoCB([weak_session]() { return weak_session.lock(); });
        _reader->setReadCB([weak_self](const RtspMediaSource::RingDataType &pkt) {
            auto strong_self = weak_self.lock();
            if (!strong_self) {
                return;
            }
            size_t i = 0;
            pkt->for_each([&](const RtpPacket::Ptr &rtp){
                if (TrackVideo == rtp->type) {
                    strong_self->SendSctpMessage((uint8_t*)rtp->data(),rtp->size());
                }
            });
                
    	});
        _reader->setDetachCB([weak_self]() {
            auto strong_self = weak_self.lock();
            if (!strong_self) {
                return;
            }
            strong_self->onShutdown(SockException(Err_shutdown, "rtsp ring buffer detached"));
            });
    }
}

在只有datachannel通道时,才从src的RingBuffer中获取一个RingReader,开始转流。

优化发送缓存区大小

usrsctp的IO模式被设置成非阻塞IO,默认的发送缓存的大小为262144。如果缓存区满了,那么就会报错Resource temporarily unavailable

在码率和分辨率比较高时,RingBuffer中缓存的GOP大小可能远远大于262144,在一开始转流时就填满了发送缓冲区,造成图像卡顿。

可以将缓存区的大小改大,在SctpAssociation::SctpAssociation构造函数中加入如下代码:

//设置发送缓存区
//默认的buffer大小为262144,将buffer size设置为512000*5
uint32_t bufferSize = 512000*5;
if (usrsctp_setsockopt(this->socket, SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(uint32_t))<0) {
    MS_THROW_ERROR("usrsctp_setsockopt(SO_SNDBUF) failed: %s", std::strerror(errno));
}

不过这样也还是避免不了缓存区被填满的情况,这也是使用webrtc data channel传输视频流的一个弊端。

具体代码看这里

在实际的测试中,如果网络环境好,一般不会出现Resource temporarily unavailable,我实测过1080P/8M H265的码流,在内网环境,用web播放比较流畅。

但是将服务放在外网,则时不时的出现Resource temporarily unavailable,说明可能还是丢包造成的该错误,应该是包丢了后,服务端等不到web端的ack,而没有将包从发送缓冲中删除(应该没有等到web端的重传请求,难道是web没又请求重传?web端的datachannel中没有配置重传参数?),导致包越积越多,而产生该错误。

Logo

音视频技术社区,一个全球开发者共同探讨、分享、学习音视频技术的平台,加入我们,与全球开发者一起创造更加优秀的音视频产品!

更多推荐