1、引言
本文将关注于FFmpeg中的HLS相关实现,相关代码在libavformat/hls.c中(我所使用的Ffmpeg版本是4.0的),分析hls_demuxer的主要实现逻辑。
本文作为我之前的HLS综述的后续文章,也是ffmpeg框架分析的后续文章。前者介绍了HLS协议相关的理论部分,后者介绍了FFmpeg主要框架分析(本文主要关注demuxer);要是你对此感兴趣建议了解下。
2、ff_hls_demuxer的主要对外接口
#define OFFSET(x) offsetof(HLSContext, x)
static const AVOption hls_options[] = {
{"live_start_index", "segment index to start live streams at (negative values are from the end)",
OFFSET(live_start_index), AV_OPT_TYPE_INT, {.i64 = -3}, INT_MIN, INT_MAX, FLAGS},
// ... HLS demuxer支持的参数,有删减
{NULL}
};
static const AVClass hls_class = {
.class_name = "hls,applehttp",
.item_name = av_default_item_name,
.option = hls_options,
.version = LIBAVUTIL_VERSION_INT,
};
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, // 关闭HLS通信
.read_seek = hls_read_seek, // 实现HLS的seek操作
};
通常分析FFmpeg中的demuxer,我们主要关注其read_probe、read_header、read_packet、read_close、read_seek这五个函数指针所对应的实现代码。FFmpeg在实现demuxer时将其协议解析的部分完整封装了demuxer中。 hls_demuxer中主要的结构是HLSContext,定义如下:
typedef struct HLSContext {
AVClass *class;
AVFormatContext *ctx;
int n_variants;
struct variant **variants;/* master playlist中有多个variants */
int n_playlists;
struct playlist **playlists;/* playlists中包含一个segment的列表 */
int n_renditions;
struct rendition **renditions;
int cur_seq_no;
int live_start_index;
int first_packet;
int64_t first_timestamp;
int64_t cur_timestamp;
AVIOInterruptCB *interrupt_callback;
AVDictionary *avio_opts;
int strict_std_compliance;
char *allowed_extensions;
int max_reload;
int http_persistent;
int http_multiple;
AVIOContext *playlist_pb;
} HLSContext;
接下来我们将逐个函数查看。
3、hls_demuxer核心接口实现
3.1 read_probe -- hls_probe
这个函数主要是HLS格式探测,其实现代码如下:
static int hls_probe(AVProbeData *p)
{
/* HLS协议要求必须以#EXTM3U打头,并至少有下面三个字段之一存在 */
if (strncmp(p->buf, "#EXTM3U", 7))
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;
}
这段代码逻辑比较简单,都是关于字符串匹配的处理,也是按照HLS协议规定的要求进行媒体格式探测的。
3.2 read_header -- hls_read_header
hls_read_header将是比较复杂的处理逻辑,因为这里涉及到master playlist解析、节目信息获取,同时初始化hls解析相关的结构。这个函数实现有300行左右。我们将分为三部分:playlist解析、hls相关初始化、提取节目信息。
3.2.1 playlist解析
hls_read_header第一部分代码如下:
static int hls_read_header(AVFormatContext *s)
{
HLSContext *c = s->priv_data;
int ret = 0, i;
c->ctx = s;
c->interrupt_callback = &s->interrupt_callback;
c->strict_std_compliance = s->strict_std_compliance;
c->first_packet = 1;
c->first_timestamp = AV_NOPTS_VALUE;
c->cur_timestamp = AV_NOPTS_VALUE;
/* 解析m3u8 */
if ((ret = parse_playlist(c, s->url, NULL, s->pb)) < 0)
goto fail;
if (c->n_variants == 0) {
av_log(NULL, AV_LOG_WARNING, "Empty playlist\n");
ret = AVERROR_EOF;
goto fail;
}
/* 对于master playlist,逐个解析其中的playlist */
if (c->n_playlists > 1 || c->playlists[0]->n_segments == 0) {
for (i = 0; i < c->n_playlists; i++) {
struct playlist *pls = c->playlists[i];
if ((ret = parse_playlist(c, pls->url, pls, NULL)) < 0)
goto fail;
}
}
/* 必须有至少一个variant */
if (c->variants[0]->playlists[0]->n_segments == 0) {
av_log(NULL, AV_LOG_WARNING, "Empty playlist\n");
ret = AVERROR_EOF;
goto fail;
}
// ... 部分代码,有删减
从上面代码来看最主要的逻辑就是调用parse_playlist函数来解析m3u8文件。解析部分主要参考HLS协议就可以。其代码如下:
static int parse_playlist(HLSContext *c, const char *url,
struct playlist *pls, AVIOContext *in)
{
int ret = 0, is_segment = 0, is_variant = 0;
int64_t duration = 0;
char line[MAX_URL_SIZE];
const char *ptr;
int64_t seg_offset = 0;
int64_t seg_size = -1;
uint8_t *new_url = NULL;
struct variant_info variant_info;
char tmp_str[MAX_URL_SIZE];
struct segment *cur_init_section = NULL;
if (!in) { /* 创建用于HTTP请求的AVIO */
AVDictionary *opts = NULL;
av_dict_copy(&opts, c->avio_opts, 0);
ret = c->ctx->io_open(c->ctx, &in, url, AVIO_FLAG_READ, &opts);
av_dict_free(&opts);
if (ret < 0)
return ret;
}
/* HTTP-URL重定向 */
if (av_opt_get(in, "location", AV_OPT_SEARCH_CHILDREN, &new_url) >= 0)
url = new_url;
ff_get_chomp_line(in, line, sizeof(line));
if (strcmp(line, "#EXTM3U")) {/* HLS协议标志起始头 */
ret = AVERROR_INVALIDDATA;
goto fail;
}
/* 释放已经存在的pls及segment */
if (pls) {
free_segment_list(pls);
pls->finished = 0;
pls->type = PLS_TYPE_UNSPECIFIED;
}/* 以下是具体协议的解析 */
while (!avio_feof(in)) {
ff_get_chomp_line(in, line, sizeof(line));
if (av_strstart(line, "#EXT-X-STREAM-INF:", &ptr)) {
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-&