初级调研得知kurento作为一个较为成熟的流媒体服务器具有了一定的错误处理和恢复机制。同时gstreamer也确认有相应的重传插件可供使用。根据一段时间的调试,现在基本实现rtp和srtp下模拟30%丢包率也能保证的正常传输。下面来说说具体实现的要点
sdp sdp是一种回话描述协议格式,用于描述流媒体的初始化参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 sdpOffer.append("v=0\r\n" ); sdpOffer.append("o=- 0 0 IN IP4 " + senderIp + "\r\n" ); sdpOffer.append("s=VideoHub----Client\r\n" ); sdpOffer.append("c=IN IP4 " + senderIp + "\r\n" ); sdpOffer.append("t=0 0\r\n" ); if (acodec != Structure::acodec_t ::NONE && !useSrtp){ metadata["useAudio" ] = "true" ; sdpOffer.append("m=audio " + senderPortA +" RTP/AVPF 96\r\n" ); switch (acodec){ case Structure::acodec_t ::OPUS: sdpOffer.append("a=rtpmap:96 opus/48000/2\r\n" ); break ; default : break ; } sdpOffer.append("a=sendonly\r\n" ); sdpOffer.append("a=direction:active\r\n" ); sdpOffer.append("a=ssrc:" + senderSSRCA +" cname:user@example.com\r\n" ); } else { metadata["useAudio" ] = "false" ; } if (vcodec != Structure::vcodec_t ::NONE){ metadata["useVideo" ] = "true" ; if (this ->useSrtp){ sdpOffer.append("m=video " + senderPortV +" RTP/SAVPF 101\r\n" ); sdpOffer.append("a=crypto:2 AES_CM_128_HMAC_SHA1_80 inline:QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVoxMjM0|2^31|1:1\r\n" ); } else { sdpOffer.append("m=video " + senderPortV +" RTP/AVPF 101\r\n" ); } switch (vcodec){ case Structure::vcodec_t ::H264: sdpOffer.append("a=rtpmap:101 H264/90000\r\n" ); break ; case Structure::vcodec_t ::H265: sdpOffer.append("a=rtpmap:103 H265/90000\r\n" ); break ; default : break ; } sdpOffer.append("a=rtcp-fb:101 nack\r\n" ); sdpOffer.append("a=rtcp-fb:101 goog-remb\r\n" ); sdpOffer.append("a=sendonly\r\n" ); sdpOffer.append("a=direction:active\r\n" ); sdpOffer.append("a=ssrc:" + senderSSRCV +" cname:user@example.com\r\n" );
这是一段构造sdp的例子,我们通过这个sdp和kurento流媒体服务器进行协商。针对重传机制我们需要关心的是sdpOffer.append("a=rtcp-fb:101 nack\r\n");
这一行,这一行中确定了kurento将通过rtcp返回nack,同时注意的是载荷类型也是后面的101要保持和你要传输的h264流的载荷类型相同。
NACK 我们把推流程序跑起来,然后通过wireshark抓包。
这就是在丢包的时候收到的nack,成功抓到以后就可以确定kurento确实分析出来丢包的情况,并且成功返回了相应的信息。
RTCP nack的信息走的是RTCP协议,我们必须确定rtcp用到的插件的正确连接和设置。 这里可以参考https://swapx.space/2019/04/05/gst-launch/ 里的例子连接rtpbin中的pad与udpsink/udpsrc等
rtprtxqueue 因为gstreamer的版本大于1.2,所以可以选用rtprtxqueue来实现重传。rtprtxqueue维护一个传输的RTP数据包队列,并根据下游rtpsession(GstRTPRetransmissionRequest事件)的请求重新发送它们。 其次需要注意来自rtprtxqueue的重传不符合RFC 4588。 重传的分组具有与原始流相同的ssrc和有效载荷类型。 使用rtxqueue的方法比较简单,将rtprtxqueue插入payloader和rtpbin的中间,rtpbin会向上游发送重传请求,rtxqueue收到以后完成重传的动作。chong
效果
在下午1:45:30左右的时间将模拟丢包从30%调回0%,可以观察到在30%丢包的情况下接收帧率明显有抖动,但是并没对输出帧率造成很大的影响,码率也是正常没有因为丢包而出现十分严重的问题。
还可以看到的是在丢包率高的时候,JitterBuffer的值也别高,这应该是webrtc在处理这种类型事件的时候进行的一种辅助错误恢复的机制。
SRTP srtp下的重传原理其实与rtp完全相同。都是使用rtprtxqueue接收上游的重传信号。 对于实现srtp功能,我们需要在正常的rtp pipeline上增加相应的srtpenc和srtpdec。
在这里我们需要关注两个点,第一个是rtcp信息的读取与解码。
解码key的设置 rtcp信息进入udpsrc后会顺着我们的安排流入srtpdec。在这个时候,strpdec会因为接收到了新的ssrc流而发出信号reques-key
。因为ssrc信息难以提前获知。所以提前建立cap的方法并不能实现。我们要在这里通过信号,注册一个回调函数,提供相应的解码srtcp的key。在这里有一个经验教训,之前一直是直接对srtpdec在连接的时候设置caps,一直没有发现ssrc的值其实和想象的不一样。直到后来打开了gstreamer的log输出,才发现srtcp的信息在进入strpdec后发出了request-key的信号然后因为没有响应,所以没获得正确的key进行解码,导致信息直接被srtpdec丢弃,进而导致重传失败。
1 g_signal_connect (srtpSession->srtpdec, "request-key" ,G_CALLBACK(connectionRemoteKey), &sessionInfo)
回调函数connectionRemoteKey的返回类型是GstCaps *型,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static GstCaps * connectionRemoteKey (GstElement * srtpdec, guint ssrc, Info sessioninfo) { GstCaps *caps = NULL ; GstBuffer *buff_key; guint8 *bin_buffer; gchar *key = (char *)"4321ZYXWVUTSRQPONMLKJIHGFEDCBA" ; gsize len = strlen (key); buff_key = gst_buffer_new_wrapped(key, len); caps = gst_caps_new_simple ("application/x-srtp" , "srtp-key" , GST_TYPE_BUFFER, buff_key, "srtp-cipher" , G_TYPE_STRING, "aes-128-icm" , "srtp-auth" , G_TYPE_STRING, "hmac-sha1-80" , "srtcp-cipher" , G_TYPE_STRING, "aes-128-icm" , "srtcp-auth" , G_TYPE_STRING, "hmac-sha1-80" , NULL ); gst_buffer_unref(buff_key); return caps; }
这里需要注意的是,caps中的”srtp-key“字段要是GST_TYPE_BUFFER类型。所以我们要用到gst_buffer_new_wrapped函数对key进行包装。经过对ASCII,HEX,Base64三种key的格式的尝试,最终确定kurento发回的srtcp信号进行解码的key的格式是ASCII。将caps返回,这个回调函数就完成了。再次观察log,发现不再有因为key导致解码失败丢弃信息的log。
strpenc的设置 在确定srtcp信息正确传入后,重传依然失败。经过探索和对文档的阅读,我们再看下面这句话。
自rtprtxqueue的重传不符合RFC 4588。 重传的分组具有与原始流相同的ssrc和有效载荷类型。
然后打开srtpenc的源码发现了如下的描述
1 2 3 4 5 6 g_param_spec_boolean ("allow-repeat-tx" , "Allow repeat packets transmission" , "Whether retransmissions of packets with the same sequence number are allowed" "(Note that such repeated transmissions must have the same RTP payload, " "or a severe security weakness is introduced!)" , DEFAULT_ALLOW_REPEAT_TX, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
可以发现,srtpenc在重传同一个序列号的包的时候,需要开启allow-repeat-tx,不然重传包会被丢弃。
1 g_object_set(srtpSession->srtpenc, "allow-repeat-tx" , TRUE, NULL );
如上设置完后,srtp下的重传功能成功实现。效果与rtp基本上没有差别。