继解决了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
2
E  Failed to write packet=1194B, ret=-11
E Failed to write packet, size=37952 Try again

可以看到,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
2
3
4
5
6
7
8
9
ret = ff_write_chained(rtp_ctx, 0, pkt, s, 0);
if (ret < 0) {
if (ret == AVERROR(EINVAL)) {
av_log(whip, AV_LOG_WARNING, "Ignore failed to write packet=%dB, ret=%d\n", pkt->size, ret);
ret = 0;
} else
av_log(whip, AV_LOG_ERROR, "Failed to write packet, size=%d %s\n", pkt->size, av_err2str(ret));
goto end;
}

这代码可以说相当简陋了,就是利用了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
2
buffer_size = MAX_UDP_BUFFER_SIZE;
rtp_ctx->pb = avio_alloc_context(buffer, buffer_size, 1, s, NULL, on_rtp_write_packet, NULL);

将写给whip的原始packet,最后不是直接使用的rtp muxer的rtp_write_packet,而是先给on_rtp_write_packet进行srtp加密再发送,上面的错误也正是这里产生:

1
2
3
4
5
6
7
8
9
10
11
12
/* Encrypt by SRTP and send out. */
cipher_size = ff_srtp_encrypt(srtp, buf, buf_size, whip->buf, sizeof(whip->buf));
if (cipher_size <= 0 || cipher_size < buf_size) {
av_log(whip, AV_LOG_WARNING, "Failed to encrypt packet=%dB, cipher=%dB\n", buf_size, cipher_size);
return 0;
}

rtp_write_packetret = ffurl_write(whip->udp, whip->buf, cipher_size);
if (ret < 0) {
av_log(whip, AV_LOG_ERROR, "Failed to write packet=%dB, ret=%d\n", cipher_size, ret);
return ret;
}

所以回到上面的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
2
3
4
/* TODO: Use AVIOContext instead of URLContext */
URLContext *dtls_uc;
/* The UDP transport is used for delivering ICE, DTLS and SRTP packets. */
URLContext *udp;

作者暗示了这么写的确可能有些问题,进一步分析下代码,看udp是啥时候分配初始化的。

ice_dtls_handshake是webrtc逻辑中,ice协商完毕,建立dtls即媒体srtp通信时调用的,这时候应该就是rtp相关的udp初始化位置,定位到whip->state = WHIP_STATE_ICE_CONNECTED;即ice成功协商到可用的媒体地址:

1
2
3
ff_url_join(buf, sizeof(buf), "dtls", NULL, whip->ice_host, whip->ice_port, NULL);
ret = ffurl_open_whitelist(&whip->dtls_uc, buf, AVIO_FLAG_READ_WRITE, &s->interrupt_callback,
&opts, s->protocol_whitelist, s->protocol_blacklist, NULL);

注意这里ffurl_open_whitelist又是ffmpeg特色的链式调用,它会层层调用下一层的协议,通过ff_url_join就知道它构建了新的下级url格式并进行调用,例如这里,dtls是在tls.c中定义,并且根据协议类型,调用了下级udp协议。

通过跟踪一个典型的巨大包的发送和报错逻辑:

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
0: write len=112624
2025-09-25 11:27:03.174 16269-16407 ffmpeg lib cn.mbstudio.mewhip D Sending NAL 1 of len 112620 M=1
2025-09-25 11:27:03.174 16269-16407 ffmpeg lib cn.mbstudio.mewhip D NAL size 112620 > 1172
2025-09-25 11:27:03.174 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.174 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.174 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.174 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.175 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.175 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.175 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.175 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.175 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.175 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.175 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.175 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.176 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.176 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.176 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.176 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.176 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.176 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.176 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.176 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.177 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.177 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.177 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.177 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.177 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.177 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.177 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.177 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.178 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.178 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.178 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.178 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.178 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.178 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.178 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.178 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.179 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.179 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.179 16269-16407 ffmpeg lib cn.mbstudio.mewhip D rtp_send_data size=1172
2025-09-25 11:27:03.179 16269-16407 ffmpeg lib cn.mbstudio.mewhip E Failed to write packet=1194B, ret=-11

可知它经过了rtp协议处理(主要是h264 nal分片和打包成rtp),再经过srtp封包成dtls,再由openssl通过udp协议最终发送出去。

rtp_send_data size=

没有任何错误,表示rtp过程正常,问题还是出在最后的openssl发送关节上,即udp协议。

核心代码是:

1
2
3
4
5
6
7
8
9
10
11
12
if (!(h->flags & AVIO_FLAG_NONBLOCK)) {
ret = ff_network_wait_fd(s->udp_fd, 1);
if (ret < 0)
return ret;
}

if (!s->is_connected) {
ret = sendto (s->udp_fd, buf, size, 0,
(struct sockaddr *) &s->dest_addr,
s->dest_addr_len);
} else
ret = send(s->udp_fd, buf, size, 0);

因为whip有一次git更新,就是加的AVIO_FLAG_NONBLOCK,所以先看这里。
进入函数,果然看到了EAGIN错误的来源:

1
2
3
4
5
6
7
8
9
10
#define UDP_TX_BUF_SIZE 32768

int ff_network_wait_fd(int fd, int write)
{
int ev = write ? POLLOUT : POLLIN;
struct pollfd p = { .fd = fd, .events = ev, .revents = 0 };
int ret;
ret = poll(&p, 1, POLLING_TIME);
return ret < 0 ? ff_neterrno() : p.revents & (ev | POLLERR | POLLHUP) ? 0 : AVERROR(EAGAIN);
}

注意看UDP_TX_BUF_SIZE值,当mediacodec的单个包大小超过它时,它一下子堵塞了udp发送缓冲区,而现在的whip实现,也并没有对EAGIN进行任何处理,导致继续向里面不停扔数据,最终导致整个程序垮掉。

而这个值本来是可以通过av_dict_set进来的,udp.c中也会读取这个值,如果这个值没有才会使用上面的默认值:

1
2
3
4
5
6
7
8
if (s->buffer_size < 0)
s->buffer_size = is_output ? UDP_TX_BUF_SIZE : UDP_RX_BUF_SIZE;

//...

if (av_find_info_tag(buf, sizeof(buf), "buffer_size", p)) {
s->buffer_size = strtol(buf, NULL, 10);
}

理论上,传递参数给最上层的MUXER并层层传递到最底层的udp即可,例如:

1
2
3
av_dict_set(&format_opts, "buffer_size",  "8M", 0); //rtp buffer size

参数从:WHIP MUXER -> RTP -> SRTP -> DTLS -> UDP依次传递。

但现实是, WHIP.c在打开dtls的:

1
2
ffurl_open_whitelist(&whip->dtls_uc, buf, AVIO_FLAG_READ_WRITE, &s->interrupt_callback,
&opts, s->protocol_whitelist, s->protocol_blacklist, NULL);

就没有提供任何接口或方法来传递这个值,难怪网上之前看到过一个留言,信誓旦旦地说,FFMPEG只适合干2M以下的流媒体工作,一旦超过2M就各种不稳定的异常,想来也是这方面的原因吧。

知道的原理和原因,解决办法就看你自己了,要么手工硬编码,把上面那个UDP_TX_BUF_SIZE值改大,要么按上面说的原理,把av_dict_set的buffer_size值一层层传递进去吧,祝好运。