继解决了Android Mediacodec编码H264码流下降的问题后,码流上来了,WHIP推流也一如既往的奔溃了,毕竟是刚发布几个月的“试验性”功能……
话说FFMPEG为了抢市场,这个试验功能推出的也够仓促的,好多参数全部写死,视频只支持H264,音频只支持OPUS并且强制2声道48000Hz采样,啥VP8,VP9,711编码都不带给你的,而且随8.0主版本发布的还有ICE协商BUG导致许多流媒体服务器在ICE协商这步就出错,然后连接超时,根本推不了流,许多人可能尝试到这步这放弃了。
经过抓包才发现,STUN消息里少了关键字段服务器不认。然后想自己手写补丁的,上Git仓库一看,8.1 dev中有更新修正了,打上补丁在桌面系统上可以正常使用mp4文件进行推流了,以为一切ok就在Android上尝试一下吧,一试就出现了上面的编码码流问题,然后就是反复的错误,反复的ret返回-11错误。
1 | E Failed to write packet=1194B, ret=-11 |
可以看到,mediacodec上了高码率后,产出的nal包特别大,远超MTU大小,rtp肯定需要分片才能正常发送(rtp muxer负责在ff_rtp_send_h264_hevc中,进行nal分片,最后是nal_send发送的,所以并不是网上说的ffmpeg不直接提供H264裸包的nal分片功能,而是它不对外提供接口,只是自己内部使用)。
实际测试,同样10000+和20000+的包有时能发送,有时发送错误,再根据错误码-11是EAGIN,即重试,一般都是因为网络堵塞或服务器性能太差容量造成,但这个错误一般是发生在刚开始推送的前几秒,也不太象。
对比了桌面环境下的使用mp4文件的推流并调高码率,或Android下直接推流rtp_mpegts都没有这个问题,网络资源和AI都说不出个所以然,只能继续源码分析了。
来分析一下whip.c的源码吧,跟随错误到达这里:
1 | ret = ff_write_chained(rtp_ctx, 0, pkt, s, 0); |
这代码可以说相当简陋了,就是利用了ffmpeg的链式写入功能,whip是实现成一个MUXER,在主程序逻辑中,将编码好的H264,OPUS AVPacket包通过av_interleaved_write_frame(ofmt_ctx, aPacket)喂给whip时,它反手就是调用ff_write_chained将packet扔给了rtp_ctx(往上翻代码可以看到是通过create_rtp_muxer创建的一个rtp muxer)。
创建rtp muxer过程中,还贴心的给了一个:
1 | buffer_size = MAX_UDP_BUFFER_SIZE; |
将写给whip的原始packet,最后不是直接使用的rtp muxer的rtp_write_packet,而是先给on_rtp_write_packet进行srtp加密再发送,上面的错误也正是这里产生:
1 | /* Encrypt by SRTP and send out. */ |
所以回到上面的mp4文件和rtp_mpegts为什么正常呢?
mp4文件中包含的mp4数据不像硬件编码器是实时吐出的数据,文件不会突然产生例如37952这么大的nal包,而是规则大小的,有兴趣的可以在解码前打印frame的大小。
而rtp_mpegts是使用的rtp_mpegts muxer,再链式调用rtp muxer(行为类似whip),但它写数据时,是rtp_mpegts_write_packet直接调用av_write_frame(chain->rtp_ctx, local_pkt),这里与whip处理逻辑不同,不能简单对比了。
问题再次聚焦到上面的ffurl_write上,代码中有一条TODO:
1 | /* TODO: Use AVIOContext instead of URLContext */ |
作者暗示了这么写的确可能有些问题,进一步分析下代码,看udp是啥时候分配初始化的。
ice_dtls_handshake是webrtc逻辑中,ice协商完毕,建立dtls即媒体srtp通信时调用的,这时候应该就是rtp相关的udp初始化位置,定位到whip->state = WHIP_STATE_ICE_CONNECTED;即ice成功协商到可用的媒体地址:
1 | ff_url_join(buf, sizeof(buf), "dtls", NULL, whip->ice_host, whip->ice_port, NULL); |
注意这里ffurl_open_whitelist又是ffmpeg特色的链式调用,它会层层调用下一层的协议,通过ff_url_join就知道它构建了新的下级url格式并进行调用,例如这里,dtls是在tls.c中定义,并且根据协议类型,调用了下级udp协议。
通过跟踪一个典型的巨大包的发送和报错逻辑:
1 | 0: write len=112624 |
可知它经过了rtp协议处理(主要是h264 nal分片和打包成rtp),再经过srtp封包成dtls,再由openssl通过udp协议最终发送出去。
rtp_send_data size=
没有任何错误,表示rtp过程正常,问题还是出在最后的openssl发送关节上,即udp协议。
核心代码是:
1 | if (!(h->flags & AVIO_FLAG_NONBLOCK)) { |
因为whip有一次git更新,就是加的AVIO_FLAG_NONBLOCK,所以先看这里。
进入函数,果然看到了EAGIN错误的来源:
1 | #define UDP_TX_BUF_SIZE 32768 |
注意看UDP_TX_BUF_SIZE值,当mediacodec的单个包大小超过它时,它一下子堵塞了udp发送缓冲区,而现在的whip实现,也并没有对EAGIN进行任何处理,导致继续向里面不停扔数据,最终导致整个程序垮掉。
而这个值本来是可以通过av_dict_set进来的,udp.c中也会读取这个值,如果这个值没有才会使用上面的默认值:
1 | if (s->buffer_size < 0) |
理论上,传递参数给最上层的MUXER并层层传递到最底层的udp即可,例如:
1 | av_dict_set(&format_opts, "buffer_size", "8M", 0); //rtp buffer size |
但现实是, WHIP.c在打开dtls的:
1 | ffurl_open_whitelist(&whip->dtls_uc, buf, AVIO_FLAG_READ_WRITE, &s->interrupt_callback, |
就没有提供任何接口或方法来传递这个值,难怪网上之前看到过一个留言,信誓旦旦地说,FFMPEG只适合干2M以下的流媒体工作,一旦超过2M就各种不稳定的异常,想来也是这方面的原因吧。
知道的原理和原因,解决办法就看你自己了,要么手工硬编码,把上面那个UDP_TX_BUF_SIZE值改大,要么按上面说的原理,把av_dict_set的buffer_size值一层层传递进去吧,祝好运。