吐血干货,直播首屏耗时400ms以下的优化实践

导读:直播行业的竞争越来越激烈,进过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) {
 

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值