前言
与其他博客不同,本文不是讲解HLS 协议本身,而是讲解在FFMPEG 中是如何解析HLS的,当然FFMPEG 也是按照HLS 协议去封装/解析 HLS 流的,因此读完本文不但能了解HLS 协议本身,还能看到HLS 是如何落地的。
1 综述
HLS(Http Live Streaming )是有苹果公司基于HTTP 传输的协议,目的解决防火墙屏蔽问题,如RTMP 不是走HTTP,容易被防火墙阻难。我们首先来看一个简单的m3u8 文件:
#EXTM3U //固定格式,首行开头必须是#EXTM3U
#EXT-X-VERSION:3 //HLS V3
#EXT-X-TARGETDURATION:10 //切片最大duration ,下面5段ts 最大duration 是10, 因此这里为10
#EXT-X-MEDIA-SEQUENCE:1 //第一个切片序列号
#EXTINF:9.640000, //切片info 信息,下面紧跟切片
output1.ts //ts 切片
#EXTINF:5.600000,
output2.ts
#EXTINF:10.000000,
output3.ts
#EXTINF:10.000000,
output4.ts
#EXTINF:3.040000,
output5.ts
#EXT-X-ENDLIST //代表HLS 切片结束
工作原理其实也很简单,先下载M3U8 文件,然后根据M3U8文件中提供的ts url ,采用HTTP 去下载视频数据。因此HLS = m3u8 + http
2 使用FFMPEG 转换成HLS 切片
1)从常规文件转换HLS 切片:
./ffmpeg.exe -re -i input.mp4 -c copy -f hls -bsf:v h264_mp4toannexb output.m3u8 //会生成一个m3u8 文件 及N 个 ts 流
2)通过start_number参数,设置第一片序列数
./ffmpeg.exe -re -i input.mp4 -c copy -f hls -bsf:v h264_mp4toannexb -start_number 100 output.m3u8 //第一片切片序数将从100 开始
3)通过hls_time 参数设置切片的duration
./ffmpeg.exe -re -i input.mp4 -c copy -f hls -bsf:v h264_mp4toannexb -hls_time 10 output.m3u8 //生成的切片duration 维持在10s
本文重点是分析HLS 在FFMPEG 中是如何实现的, 关于HLS 字段 & 使用FFMPEG 转HLS 参数 见下图。
问题:播放 http://127.0.0.1/streaming/output.m3u8 与播放 http://127.0.0.1/streaming/output1.ts 有何区别?
答: 通过上述两个url,我们可以得知前者是hls 协议,后者是http 协议。 对于后者通过ffmpeg 直接下载http 网络数据,然后去demux。 前者需在后者的基础上 通过m3u8文本解析出ts 流的url。 因此我们可以概述:相比与http 协议,hls 多了一个解析m3u8 文件的过程。 本文重点是在 hls.c 是如何解析m3u8 文件,并如何处理。让我们带着问题继续往下读。
3 FFMPEG 解析 HLS 源码分析
具体源码位于libavformat/hls.c 中,查看源码,hls.c 大致做了2个工作,1 解析M3U8 文件,获取ts 流的url, 2 通过ffmpeg 解复用API 去解析ts 流。
下面我们还是根据hls_probe等如下几支函数来分析HLS 是如何被解析的。
AVInputFormat ff_hls_demuxer = {
.name = "hls,applehttp",
.long_name = NULL_IF_CONFIG_SMALL("Apple HTTP Live Streaming"),
.priv_class = &hls_class,
.priv_data_size = sizeof(HLSContext),
.flags = AVFMT_NOGENSEARCH,
.read_probe = hls_probe,
.read_header = hls_read_header,
.read_packet = hls_read_packet,
.read_close = hls_close,
.read_seek = hls_read_seek,
};
1) hls_probe()
到底FFMPEG 如如何识别当前为HLS 的呢,通过下在M3U8 文件来进行判断,如下为hls probe 函数。
static int hls_probe(AVProbeData *p)
{
/* Require #EXTM3U at the start, and either one of the ones below
* somewhere for a proper match. */
if (strncmp(p->buf, "#EXTM3U", 7)) //第一行必须是#EXTM3U
return 0;
if (strstr(p->buf, "#EXT-X-STREAM-INF:") ||
strstr(p->buf, "#EXT-X-TARGETDURATION:") ||
strstr(p->buf, "#EXT-X-MEDIA-SEQUENCE:")) //通过其他字段,进一步判断
return AVPROBE_SCORE_MAX;
return 0;
}
2 ) hls_read_header()
下图说述的playlist 就是m3u8 文件的数组形式,segment 存放的是ts 的url。 本文只解析单个m3u8 文件,m3u8 嵌套(一个m3u8 文件包含子m3u8)不在本文学习之内。在下图中,比较重要的两个函数是:parse_playlist() 和 avformat_open_input() 。前者负责解析m3u8 文件, 并将信息记录在playlist 中,后者avformat_open_input() 函数,看起来太熟悉了, 我们在播放视频是都要使用到这支API。因此hls_read_header() 所做的工作是,解析出m3u8文件,将得到的ts url 传入avformat_open_input() 中。
下面我们分析下 parse_playlist()。此函数负责解析m3u8 文件, 并将信息记录到playlist 中。
static int parse_playlist(HLSContext *c, const char *url,
struct playlist *pls, AVIOContext *in)
{
.....
ff_get_chomp_line(in, line, sizeof(line));
if (strcmp(line, "#EXTM3U")) { //第一行如果不是#EXTM3U 直接退出
ret = AVERROR_INVALIDDATA;
goto fail;
}
.....
while (!avio_feof(in)) {
ff_get_chomp_line(in, line, sizeof(line)); //获取一行文件
if (av_strstart(line, "#EXT-X-STREAM-INF:", &ptr)) { //标签是#EXT-X-STREAM-INF..
is_variant = 1;
memset(&variant_info, 0, sizeof(variant_info));
ff_parse_key_value(ptr, (ff_parse_key_val_cb) handle_variant_args,
&variant_info);
}
} else if (av_strstart(line, "#EXT-X-MEDIA:", &ptr)) {
struct rendition_info info = {{0}};
ff_parse_key_value(ptr, (ff_parse_key_val_cb) handle_rendition_args,
&info);
new_rendition(c, &info, url);
} else if (av_strstart(line, "#EXT-X-TARGETDURATION:", &ptr)) {
ret = ensure_playlist(c, &pls, url);
if (ret < 0)
goto fail;
pls->target_duration = strtoll(ptr, NULL, 10) * AV_TIME_BASE;
} else if (av_strstart(line, "#EXT-X-MEDIA-SEQUENCE:", &ptr)) {
ret = ensure_playlist(c, &pls, url);
if (ret < 0)
goto fail;
pls->start_seq_no = atoi(ptr);
}
.....
else if (av_strstart(line, "#EXT-X-ENDLIST", &ptr)) {
if (pls)
pls->finished = 1;
} else if (av_strstart(line, "#EXTINF:", &ptr)) { //标签是#EXTINF 代表下面紧跟segment
is_segment = 1;
duration = atof(ptr) * AV_TIME_BASE;
} else if (av_strstart(line, "#EXT-X-BYTERANGE:", &ptr)) {
seg_size = strtoll(ptr, NULL, 10);
ptr = strchr(ptr, '@');
if (ptr)
seg_offset = strtoll(ptr+1, NULL, 10);
} else if (av_strstart(line, "#", NULL)) {
continue;
} else if (line[0]) {
if (is_variant) {
if (!new_variant(c, &variant_info, line, url)) {
ret = AVERROR(ENOMEM);
goto fail;
}
is_variant = 0;
}
if (is_segment) {// 下面是 segment 数据
struct segment *seg;
if (!pls) {
if (!new_variant(c, 0, url, NULL)) {
ret = AVERROR(ENOMEM);
goto fail;
}
pls = c->playlists[c->n_playlists - 1];
}
seg = av_malloc(sizeof(struct segment));//申请segment 段空间
if (!seg) {
ret = AVERROR(ENOMEM);
goto fail;
}
seg->duration = duration;
seg->key_type = key_type;
if (has_iv) {
memcpy(seg->iv, iv, sizeof(iv));
} else {
int seq = pls->start_seq_no + pls->n_segments;
memset(seg->iv, 0, sizeof(seg->iv));
AV_WB32(seg->iv + 12, seq);
}
if (key_type != KEY_NONE) {
ff_make_absolute_url(tmp_str, sizeof(tmp_str), url, key);
seg->key = av_strdup(tmp_str);
if (!seg->key) {
av_free(seg);
ret = AVERROR(ENOMEM);
goto fail;
}
} else {
seg->key = NULL;
}
ff_make_absolute_url(tmp_str, sizeof(tmp_str), url, line);
seg->url = av_strdup(tmp_str);//将url 传到此segment 节点中
if (!seg->url) {
av_free(seg->key);
av_free(seg);
ret = AVERROR(ENOMEM);
goto fail;
}
dynarray_add(&pls->segments, &pls->n_segments, seg);//添加到segment array 中
is_segment = 0;
}
}
}
....
return ret;
}
3)hls_read_packet()
对于hls_read_packet() 函数,往下call av_read_frame() 获得audio/video 数据。
4) hls_read_seek()
通过下图可以, 首先根据seek 时间点,rescale 当前hls 应该seek 的时间点,再根据seek_timestamp 计算出位于playlist 的那个片段。
5) hls_close()
直接贴源码,没啥好讲的。
static int hls_close(AVFormatContext *s)
{
HLSContext *c = s->priv_data;
free_playlist_list(c);
free_variant_list(c);
free_rendition_list(c);
av_dict_free(&c->avio_opts);
ff_format_io_close(c->ctx, &c->playlist_pb);
return 0;
}
4 总结
HLS基于HTTP的流媒体网络传输协议,相比与http, HLS 需先解析m3u8 文件得到ts 的url ,剩下的跟播单个文件类似了,通过 avformat_open_input() / av_read_frame() 去demux 。
本文讲解的是 HLS 最简单的应用场景,复杂场景(比如一个m3u8 文件中包含三个高,中,低码率的子m3u8 文件) 没有去分析,但是万变不离其宗,至此你应该明白HLS 到底是怎么一回事了。