这个系列主要是写关于用FFmpeg来拉取直播流时,会涉及到的优化项,可作为优化的实践。虽然是针对的直播,但对于点播的播放比如直接播放mp4的流,还是有一样的原理。
什么是ip拉流?
ip拉流就是指将拉流url里面的域名,比如http://flv-meipai.8686c.com/live/59c3507b20a05d24f928d6cf.flv
里面的flv-meipai.8686c.com
预先用第三方dns库解析出来,然后直接替换掉,例如http://1.1.1.1/live/59c3507b20a05d24f928d6cf.flv
这样的url,传给ffmpeg来拉流播放。
为什么要用ip拉流?
如果没有替换ip,那么在ffmpeg中的tcp.c文件中,tcp_open方法会调用getaddrinfo
方法进行dns的请求和解析。具体代码如下:
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
snprintf(portstr, sizeof(portstr), "%d", port);
if (s->listen)
hints.ai_flags |= AI_PASSIVE;
int64_t start = av_gettime();
if (!hostname[0])
ret = getaddrinfo(NULL, portstr, &hints, &ai);
else
ret = getaddrinfo(hostname, portstr, &hints, &ai);
int64_t end = av_gettime();
struct sockaddr_in *ipv4 = (struct sockaddr_in *)ai->ai_addr;
char *ipAddress = (char *)malloc(INET_ADDRSTRLEN);
inet_ntop(AF_INET, &(ipv4->sin_addr), ipAddress, INET_ADDRSTRLEN);
ffmpeg_dns_success(uri,hostname,(end-start)/1000,ipAddress);
free(ipAddress);
在这段代码中hints.ai_family = AF_UNSPEC
的意思是会请求这个域名对应的ipv4和ipv6的ip地址,如下图:
图中192.168.2.16
是我的手机ip地址,192.168.2.1
是我的电脑发出的热点的ip地址。可以看出DNS请求的时候是发出了两个请求分别是A
和AAAA
,如果你用wireshark展开就会发现A
是ipv4,AAAA
是ipv6。
这样的配置会导致两个问题:
- 第一个问题,因为我们的域名只配置了ipv4的地址,没有配置ipv6的地址,所以,ipv4的地址经过
160ms
左右就返回了。但ipv6的地址经过300ms
左右才返回,并且返回的是错误的,没有具体的地址。这里还有一个点应该是,ipv4的地址在各级域名服务器,如果之前有请求过,就会直接返回了,但ipv6的地址,从来没有获取到过,所以每次都需要回源到根域名服务器去查询,然后返回错误。如果设置hints.ai_family = AF_NET
就表示只请求ipv4的地址,dns耗时就很短。当然我们这里可以直接修改ffmpeg代码,但是这就破坏了ffmpeg的强容错性,如果将来需要ipv6的地址,就容易出bug。所以我们采用在外部传入ip地址进来,这样灵活性就可配置,并且可优化的空间更大。对于直播这种首屏要求很高的应用场景,即使是100ms也是优化的空间。 - 第二个问题,一般的域名解析都会返回一个有效期,然后由系统来缓存,一般是1分钟左右,也有3分钟的,具体看注册域名的时候是如何配置的(没搞过,所以不清楚)。然后,系统缓存并不会自动去更新,所以1分钟后,缓存失效,也会导致拉流时从新解析耗时300多ms。
所以,直接传入ip大部分情况下会直接提升这300ms的时间。
具体如何操作?
-
用什么方法解析出ip地址呢?
目前有几种方法可以解析,可以用开源的HappyDNS,也可以用各厂商的httpdns。HappyDNS是走的系统解析方法并且只请求了ipv4的地址,可以很好的避免这个问题。当然httpdns就有更复杂的方法了,而且配合缓存机制,也能达到很快的速度,并且httpdns通常也能解决小运营商dns 域名劫持的问题,所以还是很有用的。 -
拿到ip后直接传入就可以了吗?
如果对应一些CDN厂商,比如网宿,在ffmpeg里直接用http://1.1.1.1/live/59c3507b20a05d24f928d6cf.flv
类似这种http请求,就能拉取到数据了,但在某些CDN厂商那里不行,比如阿里云。这是因为他们的服务器不仅仅支持某一个域名的服务,还支持其他域名的服务,所以需要在http的header里面设置Host
这个参数。这样CDN服务器就能处理了。
在这里,我们可以通过设置参数的形式通过设置Host:
这个参数给ffmpeg,这样ffmpeg就可以直接填充了。具体代码如下:(这里参照ijkplayer源码)
av_dict_set(&ffp->format_opts, "headers", "Host: flv-meipai.8686c.com", 0);
注意这里的Host:
后面一定要有一个空格。这样,ffmpeg发起的http请求就有host参数了。
- 具体代码如何实现的?
这里的关键是av_dict_set 方法如何将参数传入到ffmpeg内部。首先看下ffp->format_opts是什么数据结构
AVDictionary *format_opts;
struct AVDictionary {
int count;
AVDictionaryEntry *elems;
};
typedef struct AVDictionaryEntry {
char *key;
char *value;
} AVDictionaryEntry;
可以看出,AVDictionary就是一个封装了dict类型的数据结构。
然后是看这个ffp->format_opts
合适传进去的。
err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
avformat_open_input这个方法就是ffmpeg的打开流,并找到流媒体的头部信息的函数,具体可以参考另一篇文章Avformat_open_input函数的分析之--HTTP篇。传入以后,最终format_opts这个结构体会走到avio.c
文件的ffurl_open_whitelist
方法,并赋值给URLContext **puc
结构体,代码如下:
if (options &&
(ret = av_opt_set_dict(*puc, options)) < 0)
goto fail;
if (options && (*puc)->prot->priv_data_class &&
(ret = av_opt_set_dict((*puc)->priv_data, options)) < 0)
goto fail;
然后最终会在http.c
文件的发起http连接的函数http_connect
方法中
char headers[HTTP_HEADERS_SIZE] = "";
if (!has_header(s->headers, "\r\nHost: "))
len += av_strlcatf(headers + len, sizeof(headers) - len,
"Host: %s\r\n", hoststr);
if (s->headers)
av_strlcpy(headers + len, s->headers, sizeof(headers) - len);
这段代码的意思,如果dict里面含有Host:
这个字符串,然后就取后面的值来作为http的headers。否则取hoststr
的值,这个值是之前从url里面解析出来的。
所以,经过上面几步,就完成了外部设置Host参数的功能,除了Host以外,http协议的所有headers里面的参数都可以设置。都可以通过这种方式来设置。这就是ffmpeg的强大的地方。
网友评论