导读:直播行业的竞争越来越激烈,进过18年这波洗牌后,已经度过了蛮荒暴力期,剩下的都是在不断追求体验。最近在帮做直播优化首开,通过多种方案并行,把首开降到500ms以下,希望能对大家有借鉴。
背景:基于FFmpeg的ijkplayer,最新版本0.88版本。
拉流协议基于http-flv,http-flv更稳定些,国内大部分直播公司基本都是使用http-flv了,从我们实际数据来看,http-flv确实稍微更快些。但是考虑到会有rtmp源,这块也加了些优化。
IP直通车
简单理解就是,把域名替换成IP。比如https://www.baidu.com/,你可以直接换成14.215.177.39,这样做的目的是,省去了DNS解析的耗时,尤其在网络不好时,访问域名,域名要去解析,再给你返回。不仅仅有时间解析过长的问题,还有小运营商DNS劫持的问题。一般就是在启动应用时,就开始对拉流的域名进行预解析好,存到本地,然后在真正拉流时,直接用就行。典型的案列,就是很多人使用HTTPDNS,这个github上也有开源,可以自行去研究下。
需要注意的是,这种方案在使用 HTTPS 时,是会失败的。因为 HTTPS 在证书验证的过程,会出现 domain 不匹配导致 SSL/TLS 握手不成功。
服务端 GOP 缓存
除了客户端业务侧的优化外,我们还可以从流媒体服务器侧进行优化。我们都知道直播流中的图像帧分为:I 帧、P 帧、B 帧,其中只有 I 帧是能不依赖其他帧独立完成解码的,这就意味着当播放器接收到 I 帧它能马上渲染出来,而接收到 P 帧、B 帧则需要等待依赖的帧而不能立即完成解码和渲染,这个期间就是「黑屏」了。
所以,在服务器端可以通过缓存 GOP(在 H.264 中,GOP 是封闭的,是以 I 帧开头的一组图像帧序列),保证播放端在接入直播时能先获取到 I 帧马上渲染出画面来,从而优化首屏加载的体验。
这里有一个 IDR 帧的概念需要讲一下,所有的 IDR 帧都是 I 帧,但是并不是所有 I 帧都是 IDR 帧,IDR 帧是 I 帧的子集。I 帧严格定义是帧内编码帧,由于是一个全帧压缩编码帧,通常用 I 帧表示「关键帧」。IDR 是基于 I 帧的一个扩展,带了控制逻辑,IDR 图像都是 I 帧图像,当解码器解码到 IDR 图像时,会立即将参考帧队列清空,将已解码的数据全部输出或抛弃。重新查找参数集,开始一个新的序列。这样如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR 图像之后的图像永远不会使用 IDR 之前的图像的数据来解码。在 H.264 编码中,GOP 是封闭式的,一个 GOP 的第一帧都是 IDR 帧。
推流端设置
一般播放器需要拿到一个完整的GOP,才能记性播放。GOP是在推流端可以设置,比如下面这个图,是我dump一个流,看到的GOP情况。GOP大小是50,推流过来的fps设置是25,也就是1s内会显示25个Frame,50个Frame,刚好直播设置GOP 2S,但是直播一般fps不用设置这么高,可以随便dump任何一家直播公司的推流,设置fps在15-18之间就够了。
播放器相关耗时
当set一个源给播放器后,播放器需要open这个流,然后和服务端建立长连接,然后demux,codec,最后渲染。我们可以按照播放器的四大块,依次优化
数据请求耗时
解复用耗时
解码耗时
渲染出图耗时
数据请求
这里就是网络和协议相关。无论是http-flv,还是rtmp,都主要是基于tcp的,所以一定会有tcp三次握手,同时打开tcp.c分析。需要加日志在一些方法中,如下tcp_open方法。是已经改动过的
/* return non zero if error */
static int tcp_open(URLContext *h, const char *uri, int flags)
{
av_log(NULL, AV_LOG_INFO, "tcp_open begin");
...省略部分代码
if (!dns_entry) {
#ifdef HAVE_PTHREADS
av_log(h, AV_LOG_INFO, "ijk_tcp_getaddrinfo_nonblock begin.\n");
ret = ijk_tcp_getaddrinfo_nonblock(hostname, portstr, &hints, &ai, s->addrinfo_timeout, &h->interrupt_callback, s->addrinfo_one_by_one);
av_log(h, AV_LOG_INFO, "ijk_tcp_getaddrinfo_nonblock end.\n");
#else
if (s->addrinfo_timeout > 0)
av_log(h, AV_LOG_WARNING, "Ignore addrinfo_timeout without pthreads support.\n");
av_log(h, AV_LOG_INFO, "getaddrinfo begin.\n");
if (!hostname[0])
ret = getaddrinfo(NULL, portstr, &hints, &ai);
else
ret = getaddrinfo(hostname, portstr, &hints, &ai);
av_log(h, AV_LOG_INFO, "getaddrinfo end.\n");
#endif
if (ret) {
av_log(h, AV_LOG_ERROR,
"Failed to resolve hostname %s: %s\n",
hostname, gai_strerror(ret));
return AVERROR(EIO);
}
cur_ai = ai;
} else {
av_log(NULL, AV_LOG_INFO, "Hit DNS cache hostname = %s\n", hostname);
cur_ai = dns_entry->res;
}
restart:
#if HAVE_STRUCT_SOCKADDR_IN6
// workaround for IOS9 getaddrinfo in IPv6 only network use hardcode IPv4 address can not resolve port number.
if (cur_ai->ai_family == AF_INET6){
struct sockaddr_in6 * sockaddr_v6 = (struct sockaddr_in6 *)cur_ai->ai_addr;
if (!sockaddr_v6->sin6_port){
sockaddr_v6->sin6_port = htons(port);
}
}
#endif
fd = ff_socket(cur_ai->ai_family,
cur_ai->ai_socktype,
cur_ai->ai_protocol);
if (fd < 0) {