本篇就针对HLS
协议案例本身做一个整体分析,从客户端的角度分析下整体流程
客户端需要做什么
客户端负责选择合适的请求资源,下载器资源,然后解码显示(整成播放器的功能)。 客户端从获取索引文件开始,通常使用给定的URL来识别该流的信息。这个索引文件一般给出了可用媒体文件、解密密钥和其他可选流的位置。客户端选定流之后,就开始顺序下载每个可用的媒体文件。每个文件中包含特定流的连续分片。只要客户端下载到足够的数据,就可以开始解码数据并显示了。 如果需要,客户端负责读取所有解密密钥、认证或为用户提供用于认证或解密的接口。 客户端可以一直持续这个过程,直到它遇到索引文件中的*#EXT-X-ENDLIST*
标签;若不存在该标签,则表示该索引文件是一个直播源,客户端需要定期更新索引文件,重复上述过程。
较为常用的HLS
系统中,使用硬编码器将输入的音频编码为AAC
、将输入的视频编码为h264
,并将二者复用到MPEG-TS
中,之后使用分片工具将其切分为一系列小的ts文件;这些文件将可以放到web服务器上。分片工具同时会创建并维护一个索引文件(HLS
中称为*.M3U8*
),其中包含可用媒体文件的列表。索引文件的URL会在web服务器上发布。客户端可以读取该索引文件,然后顺序请求列出的媒体文件,这些分片可以无缝播放。
hls.c
前面两章讲到了avformat_open_input
,会根据文件路径推测出对应的AVInputFormat
,然后去读header
,去read_packet
,回顾一下,大致流程如下。
avformat_open_input(http.xxx.m3u8)
init_input(s, filename, &tmp))
//提供的文件名信息不能探测格式
av_probe_input_format2(&pd, 0, &score)))
io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
io_open_default
ffio_open_whitelist
ffurl_alloc
//探测是HTTP协议 URLProtocol ff_http_protocol
url_find_protocol(filename);
ffurl_connect //发送HTTP报文头,下载http.xxx.m3u8文件
//读m3u8文件探测解复用是AVInputFormat *iformat ="hls,applehttp"
*fmt = av_probe_input_buffer2(s->pb, &s->iformat, filename, s, 0, s->format_probesize);
s->iformat->read_header(s); //iformat iformat hls.c-->hls_read_header
复制代码
hls.c/read_header
static int hls_read_header(AVFormatContext *s)
{
....................
//解析playlist,也就是m3u8文件
if ((ret = parse_playlist(c, s->url, NULL, s->pb)) < 0)
goto fail;
if (c->n_variants == 0) {
av_log(s, AV_LOG_WARNING, "Empty playlist\n");
ret = AVERROR_EOF;
goto fail;
}
/* If the playlist only contained playlists (Master Playlist),
* parse each individual playlist. */
//如果解析出来是 Master 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];
pls->m3u8_hold_counters = 0;
if ((ret = parse_playlist(c, pls->url, pls, NULL)) < 0) {
av_log(s, AV_LOG_WARNING, "parse_playlist error %s [%s]\n", av_err2str(ret), pls->url);
pls->broken = 1;
if (c->n_playlists > 1)
continue;
goto fail;
}
}
}
for (i = 0; i < c->n_variants; i++) {
if (c->variants[i]->playlists[0]->n_segments == 0) {
av_log(s, AV_LOG_WARNING, "Empty segment [%s]\n", c->variants[i]->playlists[0]->url);
c->variants[i]->playlists[0]->broken = 1;
}
}
/* If this isn't a live stream, calculate the total duration of the
* stream. */
//如果有finished这个标志,则说明这个不是一个直播流,那么计算下整个时长duration
if (c->variants[0]->playlists[0]->finished) {
int64_t duration = 0;
for (i = 0; i < c->variants[0]->playlists[0]->n_segments; i++)
duration += c->variants[0]->playlists[0]->segments[i]->duration;
s->duration = duration;
}
/* Associate renditions with variants */
for (i = 0; i < c->n_variants; i++) {
struct variant *var = c->variants[i];
if (var->audio_group[0])
add_renditions_to_variant(c, var, AVMEDIA_TYPE_AUDIO, var->audio_group);
if (var->video_group[0])
add_renditions_to_variant(c, var, AVMEDIA_TYPE_VIDEO, var->video_group);
if (var->subtitles_group[0])
add_renditions_to_variant(c, var, AVMEDIA_TYPE_SUBTITLE, var->subtitles_group);
}
/* Create a program for each variant */
for (i = 0; i < c->n_variants; i++) {
struct variant *v = c->variants[i];
AVProgram *program;
program = av_new_program(s, i);
if (!program)
goto fail;
av_dict_set_int(&program->metadata, "variant_bitrate", v->bandwidth, 0);
}
/* Select the starting segments */
for (i = 0; i < c->n_playlists; i++) {
struct playlist *pls = c->playlists[i];
if (pls->n_segments == 0)
continue;
pls->cur_seq_no = select_cur_seq_no(c, pls);
highest_cur_seq_no = FFMAX(highest_cur_seq_no, pls->cur_seq_no);
}
}
复制代码
先看一部分代码,主要是通过parse_playlist
这个方法,解析出相关的节目信息,并赋值给HLSContext
parse_playlist
static int parse_playlist(HLSContext *c, const char *url,
struct playlist *pls, AVIOContext *in)
{
//解析m3u8文件的playerlist文件
int ret = 0, is_segment = 0, is_variant = 0;
int64_t duration = 0;
enum KeyType key_type = KEY_NONE;
uint8_t iv[16] = "";
int has_iv = 0;
char key[MAX_URL_SIZE] = "";
char line[MAX_URL_SIZE];
const char *ptr;
int close_in = 0;
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;
int is_http = av_strstart(url, "http", NULL);
struct segment **prev_segments = NULL;
int prev_n_segments = 0;
int64_t prev_start_seq_no = -1;
if (is_http && !in && c->http_persistent && c->playlist_pb) {
in = c->playlist_pb;
ret = open_url_keepalive(c->ctx, &c->playlist_pb, url, NULL);
if (ret == AVERROR_EXIT) {
return ret;
} else if (ret < 0) {
if (ret != AVERROR_EOF)
av_log(c->ctx, AV_LOG_WARNING,
"keepalive request failed for '%s' with error: '%s' when parsing playlist\n",
url, av_err2str(ret));
in = NULL;
}
}
if (!in) {
AVDictionary *opts = NULL;
av_dict_copy(&opts, c->avio_opts, 0);
if (c->http_persistent)
av_dict_set(&opts, "multiple_requests", "1", 0);
ret = c->ctx->io_open(c->ctx, &in, url, AVIO_FLAG_READ, &opts);
av_dict_free(&opts);
if (ret < 0)
return ret;
if (is_http && c->http_persistent)
c->playlist_pb = in;
else
close_in = 1;
}
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")) {
ret = AVERROR_INVALIDDATA;
goto fail;
}
if (pls) {
prev_start_seq_no = pls->start_seq_no;
prev_segments = pls->segments;
prev_n_segments = pls->n_segments;
pls->segments = NULL;
pls->n_segments = 0;
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)) {
//表示master playlist,描述了variant的诸多信息,比如下载带宽、音视频编码信息、视频分辨率等等
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-KEY:", &ptr)) {
//这个字段表示是加密playlist
struct key_info info = {{0}};
ff_parse_key_value(ptr, (ff_parse_key_val_cb) handle_key_args,
&info);
key_type = KEY_NONE;
has_iv = 0;
if (!strcmp(info.method, "AES-128"))
key_type = KEY_AES_128;
if (!strcmp(info.method, "SAMPLE-AES"))
key_type = KEY_SAMPLE_AES;
if (!strncmp(info.iv, "0x", 2) || !strncmp(info.iv, "0X", 2)) {
ff_hex_to_data(iv, info.iv + 2);
has_iv = 1;
}
av_strlcpy(key, info.uri, sizeof(key));
} else if (av_strstart(line, "#EXT-X-MEDIA:", &ptr)) {
//alternate media为HLS提供了一种外挂音频、视频、字幕的格式,可以在不改动已生成的HLS分片信息的情况下,
// 为客户端提供新的可选媒体信息。为此,在STREAM-INF添加了AUIOD、VIDEO属性,以实现关联
//#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 1",AUTOSELECT=YES,DEFAULT=YES
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)) {
//EXT-X-TARGETDURATION 切片时长
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)) {
//序号
uint64_t seq_no;
ret = ensure_playlist(c, &pls, url);
if (ret < 0)
goto fail;
seq_no = strtoull(ptr, NULL, 10);
if (seq_no > INT64_MAX) {
av_log(c->ctx, AV_LOG_DEBUG, "MEDIA-SEQUENCE higher than "
"INT64_MAX, mask out the highest bit\n");
seq_no &= INT64_MAX;
}
pls->start_seq_no = seq_no;
} else if (av_strstart(line, "#EXT-X-PLAYLIST-TYPE:", &ptr)) {
//playerlist类型 vod:点播 EVENT:Event playlist
ret = ensure_playlist(c, &pls, url);
if (ret < 0)
goto fail;
if (!strcmp(ptr, "EVENT"))
pls->type = PLS_TYPE_EVENT;
else if (!strcmp(ptr, "VOD"))
pls->type = PLS_TYPE_VOD;
} else if (av_strstart(line, "#EXT-X-MAP:", &ptr)) {
//为'#EXT-X-MAP' 将一个完整视频的头单独存放, 将其他部分存在另一个视频文件中, 这样就是你们抓到了其中一个视频, 也播放不了,
// 这是防止盗链的一种手段, "将鸡蛋放在不同的篮子里"
struct init_section_info info = {{0}};
ret = ensure_playlist(c, &pls, url);
if (ret < 0)
goto fail;
ff_parse_key_value(ptr, (ff_parse_key_val_cb) handle_init_section_args,
&info);
cur_init_section = new_init_section(pls, &info, url);
cur_init_section->key_type = key_type;
if (has_iv) {
memcpy(cur_init_section->iv, iv, sizeof(iv));
} else {
int64_t seq = pls->start_seq_no + pls->n_segments;
memset(cur_init_section->iv, 0, sizeof(cur_init_section->iv));
AV_WB64(cur_init_section->iv + 8, seq);
}
if (key_type != KEY_NONE) {
ff_make_absolute_url(tmp_str, sizeof(tmp_str), url, key);
if (!tmp_str[0]) {
av_free(cur_init_section);
ret = AVERROR_INVALIDDATA;
goto fail;
}
cur_init_section->key = av_strdup(tmp_str);
if (!cur_init_section->key) {
av_free(cur_init_section);
ret = AVERROR(ENOMEM);
goto fail;
}
} else {
cur_init_section->key = NULL;
}
} else if (av_strstart(line, "#EXT-X-ENDLIST", &ptr)) {
//有结束标志,则不是直播类型playerlist
if (pls)
pls->finished = 1;
} else if (av_strstart(line, "#EXTINF:", &ptr)) {
//EXTINF ts片时长
is_segment = 1;
duration = atof(ptr) * AV_TIME_BASE;
} else if (av_strstart(line, "#EXT-X-BYTERANGE:", &ptr)) {
//使用该字段,可以不用在server上存储大量的小文件,而仅通过一个文件
// ,通过偏移量和长度来支持分片。与点播playlist中的m3u8等价的playlist内容如下:
//#EXT-X-BYTERANGE:75232@0
seg_size = strtoll(ptr, NULL, 10);
ptr = strchr(ptr, '@');
if (ptr)
seg_offset = strtoll(ptr+1, NULL, 10);
} else if (av_strstart(line, "#", NULL)) {
av_log(c->ctx, AV_LOG_INFO, "Skip ('%s')\n", line);
continue;
} else if (line[0]) {
//如果是包含master playerlist的,记录下,拼接url
if (is_variant) {
if (!new_variant(c, &variant_info, line, url)) {
ret = AVERROR(ENOMEM);
goto fail;
}
is_variant = 0;
}
//ts segment的url
if (is_segment) {
struct segment *seg;
ret = ensure_playlist(c, &pls, url);
if (ret < 0)
goto fail;
seg = av_malloc(sizeof(struct segment));
if (!seg) {
ret = AVERROR(ENOMEM);
goto fail;
}
if (has_iv) {
memcpy(seg->iv, iv, sizeof(iv));
} else {
int64_t seq = pls->start_seq_no + pls->n_segments;
memset(seg->iv, 0, sizeof(seg->iv));
AV_WB64(seg->iv + 8, seq);
}
if (key_type != KEY_NONE) {
ff_make_absolute_url(tmp_str, sizeof(tmp_str), url, key);
if (!tmp_str[0]) {
ret = AVERROR_INVALIDDATA;
av_free(seg);
goto fail;
}
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);
if (!tmp_str[0]) {
ret = AVERROR_INVALIDDATA;
if (seg->key)
av_free(seg->key);
av_free(seg);
goto fail;
}
seg->url = av_strdup(tmp_str);
if (!seg->url) {
av_free(seg->key);
av_free(seg);
ret = AVERROR(ENOMEM);
goto fail;
}
if (duration < 0.001 * AV_TIME_BASE) {
av_log(c->ctx, AV_LOG_WARNING, "Cannot get correct #EXTINF value of segment %s,"
" set to default value to 1ms.\n", seg->url);
duration = 0.001 * AV_TIME_BASE;
}
seg->duration = duration;
seg->key_type = key_type;
dynarray_add(&pls->segments, &pls->n_segments, seg);
is_segment = 0;
seg->size = seg_size;
if (seg_size >= 0) {
seg->url_offset = seg_offset;
seg_offset += seg_size;
seg_size = -1;
} else {
seg->url_offset = 0;
seg_offset = 0;
}
seg->init_section = cur_init_section;
}
}
}
......................................
if (pls)
pls->last_load_time = av_gettime_relative();
fail:
av_free(new_url);
if (close_in)
ff_format_io_close(c->ctx, &in);
c->ctx->ctx_flags = c->ctx->ctx_flags & ~(unsigned)AVFMTCTX_UNSEEKABLE;
if (!c->n_variants || !c->variants[0]->n_playlists ||
!(c->variants[0]->playlists[0]->finished ||
c->variants[0]->playlists[0]->type == PLS_TYPE_EVENT))
c->ctx->ctx_flags |= AVFMTCTX_UNSEEKABLE;
return ret;
}
复制代码
如果对于playlist
不太了解的话,可能看的有点懵,这里简单介绍下几种常用的playlist
,了解之后再去对照代码中的字段解析,就比较清晰明了了。
- 点播(
VOD,Video On Demand)playlist
- 直播
playlist
- 秘钥
playlist
master playlist
点播 playlist
HLS
中点播playlist
是静态的文件,生成之后一般不允许修改,server端可以预先生成切片文件。其格式如下:
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:3
#EXTINF:9.009,
http://media.example.com/first.ts
#EXTINF:9.009,
http://media.example.com/second.ts
#EXTINF:3.003,
http://media.example.com/third.ts
#EXT-X-ENDLIST
复制代码
这类playlist
比较典型的特征是
playlist
中包含#EXT-X-ENDLIST
字段,表示结束标志EXT-X-PLAYLIST-TYPE
字段为VOD
(可选)
直播playlist
对于直播playlist
,最简单的特征就是没有EXT-X-ENDLIST
标签,并且不存在EXT-X-PLAYLIST-TYPE
标签,直播playlist
是一个典型的滑动窗口,server端会对源格式做实时转码(存在一点延时),并定期清理已发布的分片信息。典型的滑动窗口大小为3-5个分片。其m3u8
格式如下
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:10,
fileSequence1.ts
#EXTINF:10,
fileSequence2.ts
#EXTINF:10,
fileSequence3.ts
复制代码
秘钥playlist
这里的密钥是由EXT-X-KEY
字段指定的,其中给出了加密方法,和密钥的存储路径。
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:7794
#EXT-X-TARGETDURATION:15
#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"
#EXTINF:2.833,
http://media.example.com/fileSequence52-A.ts
#EXTINF:15.0,
http://media.example.com/fileSequence52-B.ts
#EXTINF:13.333,
http://media.example.com/fileSequence52-C.ts
#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=53"
#EXTINF:15.0,
http://media.example.com/fileSequence53-A.ts
复制代码
master playlist
master playlist
其中并不包含分片有关信息,而是描述了同一个源的不同编码格式,在HLS
中称之为variant
。master playlist
中描述了variant
的诸多信息,比如下载带宽、音视频编码信息、视频分辨率等等。典型的示例如下:
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=150000,RESOLUTION=416x234, \
CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/low/index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=416x234, \
CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/lo_mid/index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=640000,RESOLUTION=640x360, \
CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/high/index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=64000,CODECS="mp4a.40.5"
http://example.com/audio/index.m3u8
复制代码
当然,还有一些类型playlist
就不在这里一一赘述了,有兴趣可以自己额外学习下。
ffio_init_context
/* Open the demuxer for each playlist */
for (i = 0; i < c->n_playlists; i++) {
struct playlist *pls = c->playlists[i];
char *url;
ff_const59 AVInputFormat *in_fmt = NULL;
if (!(pls->ctx = avformat_alloc_context())) {
ret = AVERROR(ENOMEM);
goto fail;
}
if (pls->n_segments == 0)
continue;
pls->index = i;
pls->needed = 1;
pls->parent = s;
/*
* If this is a live stream and this playlist looks like it is one segment
* behind, try to sync it up so that every substream starts at the same
* time position (so e.g. avformat_find_stream_info() will see packets from
* all active streams within the first few seconds). This is not very generic,
* though, as the sequence numbers are technically independent.
*/
if (!pls->finished && pls->cur_seq_no == highest_cur_seq_no - 1 &&
highest_cur_seq_no < pls->start_seq_no + pls->n_segments) {
pls->cur_seq_no = highest_cur_seq_no;
}
pls->read_buffer = av_malloc(INITIAL_BUFFER_SIZE);
if (!pls->read_buffer){
ret = AVERROR(ENOMEM);
avformat_free_context(pls->ctx);
pls->ctx = NULL;
goto fail;
}
ffio_init_context(&pls->pb, pls->read_buffer, INITIAL_BUFFER_SIZE, 0, pls,
read_data, NULL, NULL);
pls->ctx->probesize = s->probesize > 0 ? s->probesize : 1024 * 4;
pls->ctx->max_analyze_duration = s->max_analyze_duration > 0 ? s->max_analyze_duration : 4 * AV_TIME_BASE;
pls->ctx->interrupt_callback = s->interrupt_callback;
url = av_strdup(pls->segments[0]->url);
ret = av_probe_input_buffer(&pls->pb, &in_fmt, url, NULL, 0, 0);
if (ret < 0) {
/* Free the ctx - it isn't initialized properly at this point,
* so avformat_close_input shouldn't be called. If
* avformat_open_input fails below, it frees and zeros the
* context, so it doesn't need any special treatment like this. */
av_log(s, AV_LOG_ERROR, "Error when loading first segment '%s'\n", url);
avformat_free_context(pls->ctx);
pls->ctx = NULL;
av_free(url);
goto fail;
}
av_free(url);
pls->ctx->pb = &pls->pb;
pls->ctx->io_open = nested_io_open;
pls->ctx->flags |= s->flags & ~AVFMT_FLAG_CUSTOM_IO;
if ((ret = ff_copy_whiteblacklists(pls->ctx, s)) < 0)
goto fail;
//打开第0个ts片
ret = avformat_open_input(&pls->ctx, pls->segments[0]->url, in_fmt, NULL);
if (ret < 0)
goto fail;
if (pls->id3_deferred_extra && pls->ctx->nb_streams == 1) {
ff_id3v2_parse_apic(pls->ctx, pls->id3_deferred_extra);
avformat_queue_attached_pictures(pls->ctx);
ff_id3v2_parse_priv(pls->ctx, pls->id3_deferred_extra);
ff_id3v2_free_extra_meta(&pls->id3_deferred_extra);
}
if (pls->is_id3_timestamped == -1)
av_log(s, AV_LOG_WARNING, "No expected HTTP requests have been made\n");
/*
* For ID3 timestamped raw audio streams we need to detect the packet
* durations to calculate timestamps in fill_timing_for_id3_timestamped_stream(),
* but for other streams we can rely on our user calling avformat_find_stream_info()
* on us if they want to.
*/
if (pls->is_id3_timestamped || (pls->n_renditions > 0 && pls->renditions[0]->type == AVMEDIA_TYPE_AUDIO)) {
ret = avformat_find_stream_info(pls->ctx, NULL);
if (ret < 0)
goto fail;
}
pls->has_noheader_flag = !!(pls->ctx->ctx_flags & AVFMTCTX_NOHEADER);
/* Create new AVStreams for each stream in this playlist */
ret = update_streams_from_subdemuxer(s, pls);
if (ret < 0)
goto fail;
/*
* Copy any metadata from playlist to main streams, but do not set
* event flags.
*/
if (pls->n_main_streams)
av_dict_copy(&pls->main_streams[0]->metadata, pls->ctx->metadata, 0);
add_metadata_from_renditions(s, pls, AVMEDIA_TYPE_AUDIO);
add_metadata_from_renditions(s, pls, AVMEDIA_TYPE_VIDEO);
add_metadata_from_renditions(s, pls, AVMEDIA_TYPE_SUBTITLE);
}
update_noheader_flag(s);
return 0;
复制代码
拿到相关节目信息之后,针对每个playlist
进行解协议,ffio_init_context
,初始化IOContext
信息,注意这里面传入的**read_data
**,这个函数参数针对读取网络数据,真正IO
层做的事情
接下来的方法应该很熟悉了av_probe_input_buffer
,来根据文件名的去推断AVInputformat
(不熟悉的可以看看前面几章),这里是ts文件,所以最终推断出来的是ff_mpegts_demuxer
,在mpegts.c
之中
AVInputFormat ff_mpegts_demuxer = {
.name = "mpegts",
.long_name = NULL_IF_CONFIG_SMALL("MPEG-TS (MPEG-2 Transport Stream)"),
.priv_data_size = sizeof(MpegTSContext),
.read_probe = mpegts_probe,
.read_header = mpegts_read_header,
.read_packet = mpegts_read_packet,
.read_close = mpegts_read_close,
.read_timestamp = mpegts_get_dts,
.flags = AVFMT_SHOW_IDS | AVFMT_TS_DISCONT,
.priv_class = &mpegts_class,
};
复制代码
read_packet
static int hls_read_packet(AVFormatContext *s, AVPacket *pkt)
{
HLSContext *c = s->priv_data;
int ret, i, minplaylist = -1;
recheck_discard_flags(s, c->first_packet);
c->first_packet = 0;
for (i = 0; i < c->n_playlists; i++) {
struct playlist *pls = c->playlists[i];
/* Make sure we've got one buffered packet from each open playlist
* stream */
/* 从每一个已打开的playlist上读取一个AVPacket */
if (pls->needed && !pls->pkt->data) {
while (1) {
int64_t ts_diff;
AVRational tb;
ret = av_read_frame(pls->ctx, pls->pkt);
if (ret < 0) {
if (!avio_feof(&pls->pb) && ret != AVERROR_EOF)
return ret;
break;
} else {
/* stream_index check prevents matching picture attachments etc. */
if (pls->is_id3_timestamped && pls->pkt->stream_index == 0) {
/* audio elementary streams are id3 timestamped */
fill_timing_for_id3_timestamped_stream(pls);
}
if (c->first_timestamp == AV_NOPTS_VALUE &&
pls->pkt->dts != AV_NOPTS_VALUE)
c->first_timestamp = av_rescale_q(pls->pkt->dts,
get_timebase(pls), AV_TIME_BASE_Q);
}
if (pls->seek_timestamp == AV_NOPTS_VALUE)
break;
//下面都是关于有seek的操作的处理
if (pls->seek_stream_index < 0 ||
pls->seek_stream_index == pls->pkt->stream_index) {
if (pls->pkt->dts == AV_NOPTS_VALUE) {
pls->seek_timestamp = AV_NOPTS_VALUE;
break;
}
tb = get_timebase(pls);
ts_diff = av_rescale_rnd(pls->pkt->dts, AV_TIME_BASE,
tb.den, AV_ROUND_DOWN) -
pls->seek_timestamp;
if (ts_diff >= 0 && (pls->seek_flags & AVSEEK_FLAG_ANY ||
pls->pkt->flags & AV_PKT_FLAG_KEY)) {
pls->seek_timestamp = AV_NOPTS_VALUE;
break;
}
}
/* 到此,说面当前AVPacket是需要丢弃的,重新读取 */
av_packet_unref(pls->pkt);
}
}
/* Check if this stream has the packet with the lowest dts */
//找到最小的dts的packet,并记录其索引值
if (pls->pkt->data) {
struct playlist *minpls = minplaylist < 0 ?
NULL : c->playlists[minplaylist];
if (minplaylist < 0) {
minplaylist = i;
} else {
int64_t dts = pls->pkt->dts;
int64_t mindts = minpls->pkt->dts;
if (dts == AV_NOPTS_VALUE ||
(mindts != AV_NOPTS_VALUE && compare_ts_with_wrapdetect(dts, pls, mindts, minpls) < 0))
minplaylist = i;
}
}
}
/* If we got a packet, return it */
/* 成功读取AVPacket,需要返回给上层调用者 */
if (minplaylist >= 0) {
struct playlist *pls = c->playlists[minplaylist];
AVStream *ist;
AVStream *st;
ret = update_streams_from_subdemuxer(s, pls);
if (ret < 0) {
av_packet_unref(pls->pkt);
return ret;
}
// If sub-demuxer reports updated metadata, copy it to the first stream
// and set its AVSTREAM_EVENT_FLAG_METADATA_UPDATED flag.
if (pls->ctx->event_flags & AVFMT_EVENT_FLAG_METADATA_UPDATED) {
if (pls->n_main_streams) {
st = pls->main_streams[0];
av_dict_copy(&st->metadata, pls->ctx->metadata, 0);
st->event_flags |= AVSTREAM_EVENT_FLAG_METADATA_UPDATED;
}
pls->ctx->event_flags &= ~AVFMT_EVENT_FLAG_METADATA_UPDATED;
}
/* check if noheader flag has been cleared by the subdemuxer */
if (pls->has_noheader_flag && !(pls->ctx->ctx_flags & AVFMTCTX_NOHEADER)) {
pls->has_noheader_flag = 0;
update_noheader_flag(s);
}
if (pls->pkt->stream_index >= pls->n_main_streams) {
av_log(s, AV_LOG_ERROR, "stream index inconsistency: index %d, %d main streams, %d subdemuxer streams\n",
pls->pkt->stream_index, pls->n_main_streams, pls->ctx->nb_streams);
av_packet_unref(pls->pkt);
return AVERROR_BUG;
}
ist = pls->ctx->streams[pls->pkt->stream_index];
st = pls->main_streams[pls->pkt->stream_index];
av_packet_move_ref(pkt, pls->pkt);
pkt->stream_index = st->index;
if (pkt->dts != AV_NOPTS_VALUE)
c->cur_timestamp = av_rescale_q(pkt->dts,
ist->time_base,
AV_TIME_BASE_Q);
/* There may be more situations where this would be useful, but this at least
* handles newly probed codecs properly (i.e. request_probe by mpegts). */
if (ist->codecpar->codec_id != st->codecpar->codec_id) {
ret = set_stream_info_from_input_stream(st, pls, ist);
if (ret < 0) {
return ret;
}
}
return 0;
}
return AVERROR_EOF;
}
复制代码
这个代码调用了av_read_frame
方法,但是实际上可以看到,整个函数只是做一些解析判断的工作,并没有是想象中的IO层的相关流程,真正的处理就在av_read_frame
,最终会调用到read_data
方法,也就是前面初始化IOContext
的时候,传递进去的函数回调。
read_data
static int read_data(void *opaque, uint8_t *buf, int buf_size)
{
struct playlist *v = opaque;
HLSContext *c = v->parent->priv_data;
int ret;
int just_opened = 0;
int reload_count = 0;
struct segment *seg;
//使用了go语句
restart:
if (!v->needed)
return AVERROR_EOF;
if (!v->input || (c->http_persistent && v->input_read_done)) {
int64_t reload_interval;
/* Check that the playlist is still needed before opening a new
* segment. */
//校验playlist合法性
v->needed = playlist_needed(v);
if (!v->needed) {
av_log(v->parent, AV_LOG_INFO, "No longer receiving playlist %d ('%s')\n",
v->index, v->url);
return AVERROR_EOF;
}
/* If this is a live stream and the reload interval has elapsed since
* the last playlist reload, reload the playlists now. */
//直播流的情况下,重新刷新的时长。
reload_interval = default_reload_interval(v);
reload:
reload_count++;
if (reload_count > c->max_reload)
return AVERROR_EOF;
//如果是直播流的情况下,并且已经到达了reload的时机,重新去刷新playlist获取ts列表
if (!v->finished &&
av_gettime_relative() - v->last_load_time >= reload_interval) {
if ((ret = parse_playlist(c, v->url, v, NULL)) < 0) {
if (ret != AVERROR_EXIT)
av_log(v->parent, AV_LOG_WARNING, "Failed to reload playlist %d\n",
v->index);
return ret;
}
/* If we need to reload the playlist again below (if
* there's still no more segments), switch to a reload
* interval of half the target duration. */
// 按照HLS协议规定,如果还需要请求playlist,请求间隔可以设置为segment时长的一半
reload_interval = v->target_duration / 2;
}
//如果当前的seqnumer,比起始的seq都要小,那么直接跳过中间segment并刷新到最新数据。
if (v->cur_seq_no < v->start_seq_no) {
av_log(v->parent, AV_LOG_WARNING,
"skipping %"PRId64" segments ahead, expired from playlists\n",
v->start_seq_no - v->cur_seq_no);
v->cur_seq_no = v->start_seq_no;
}
if (v->cur_seq_no > v->last_seq_no) {
v->last_seq_no = v->cur_seq_no;
v->m3u8_hold_counters = 0;
} else if (v->last_seq_no == v->cur_seq_no) {
v->m3u8_hold_counters++;
if (v->m3u8_hold_counters >= c->m3u8_hold_counters) {
return AVERROR_EOF;
}
} else {
av_log(v->parent, AV_LOG_WARNING, "maybe the m3u8 list sequence have been wraped.\n");
}
//如果当前的的序号>起始序号+总数ts片数量。
if (v->cur_seq_no >= v->start_seq_no + v->n_segments) {
//如果是点播,说明整个视频就到达了尾端,就结束了
if (v->finished)
return AVERROR_EOF;
//如果是直播,如果还没有到达reload时间,那么就sleep一直等到reload时间
while (av_gettime_relative() - v->last_load_time < reload_interval) {
if (ff_check_interrupt(c->interrupt_callback))
return AVERROR_EXIT;
av_usleep(100*1000);
}
/* Enough time has elapsed since the last reload */
//到达reload时间,重新刷新playlist
goto reload;
}
/* 所有的前提都满足,可以开始读segment了 */
v->input_read_done = 0;
seg = current_segment(v);
/* load/update Media Initialization Section, if any */
ret = update_init_section(v, seg);
if (ret)
return ret;
if (c->http_multiple == 1 && v->input_next_requested) {
FFSWAP(AVIOContext *, v->input, v->input_next);
v->cur_seg_offset = 0;
v->input_next_requested = 0;
ret = 0;
} else {
/* 这里发起了的HTTP请求 */
ret = open_input(c, v, seg, &v->input);
}
if (ret < 0) {
if (ff_check_interrupt(c->interrupt_callback))
return AVERROR_EXIT;
//segment打开失败了,直接跳过当前segment,加载下一个 seq+1
av_log(v->parent, AV_LOG_WARNING, "Failed to open segment %"PRId64" of playlist %d\n",
v->cur_seq_no,
v->index);
v->cur_seq_no += 1;
goto reload;
}
just_opened = 1;
}
if (c->http_multiple == -1) {
uint8_t *http_version_opt = NULL;
int r = av_opt_get(v->input, "http_version", AV_OPT_SEARCH_CHILDREN, &http_version_opt);
if (r >= 0) {
c->http_multiple = (!strncmp((const char *)http_version_opt, "1.1", 3) || !strncmp((const char *)http_version_opt, "2.0", 3));
av_freep(&http_version_opt);
}
}
seg = next_segment(v);
if (c->http_multiple == 1 && !v->input_next_requested &&
seg && seg->key_type == KEY_NONE && av_strstart(seg->url, "http", NULL)) {
ret = open_input(c, v, seg, &v->input_next);
if (ret < 0) {
if (ff_check_interrupt(c->interrupt_callback))
return AVERROR_EXIT;
av_log(v->parent, AV_LOG_WARNING, "Failed to open segment %"PRId64" of playlist %d\n",
v->cur_seq_no + 1,
v->index);
} else {
v->input_next_requested = 1;
}
}
if (v->init_sec_buf_read_offset < v->init_sec_data_len) {
/* Push init section out first before first actual segment */
int copy_size = FFMIN(v->init_sec_data_len - v->init_sec_buf_read_offset, buf_size);
memcpy(buf, v->init_sec_buf, copy_size);
v->init_sec_buf_read_offset += copy_size;
return copy_size;
}
seg = current_segment(v);
/* 这是实际通过HTTP读取buf_size长度的数据,从server上获取的 */
ret = read_from_url(v, seg, buf, buf_size);
if (ret > 0) {
if (just_opened && v->is_id3_timestamped != 0) {
/* Intercept ID3 tags here, elementary audio streams are required
* to convey timestamps using them in the beginning of each segment. */
intercept_id3(v, buf, buf_size, &ret);
}
return ret;
}
/* 读取完成了,可以关闭segment所使用的HTTP资源了 */
if (c->http_persistent &&
seg->key_type == KEY_NONE && av_strstart(seg->url, "http", NULL)) {
v->input_read_done = 1;
} else {
ff_format_io_close(v->parent, &v->input);
}
//segment序号++,重新restart
v->cur_seq_no++;
c->cur_seq_no = v->cur_seq_no;
goto restart;
}
复制代码
这里可以发现read_data
才是真正io
层做的事情,从服务器读取数据,放到缓冲区去,真正的数据解析是由demuxer
去做的。
read_seek
最后看一下,seek
操作,基本思路就是按照给定的时间点(timestamp
)找到对应的流的读取位置,然后继续读取数据。所以在执行seek之前,需要清理下之前缓存的数据
static int hls_read_seek(AVFormatContext *s, int stream_index,
int64_t timestamp, int flags)
{
HLSContext *c = s->priv_data;
struct playlist *seek_pls = NULL;
int i, j;
int stream_subdemuxer_index;
int64_t first_timestamp, seek_timestamp, duration;
int64_t seq_no;
if ((flags & AVSEEK_FLAG_BYTE) || (c->ctx->ctx_flags & AVFMTCTX_UNSEEKABLE))
return AVERROR(ENOSYS);
first_timestamp = c->first_timestamp == AV_NOPTS_VALUE ?
0 : c->first_timestamp;
//根据传入的timestamp,和index,转换最终的seek时间戳
seek_timestamp = av_rescale_rnd(timestamp, AV_TIME_BASE,
s->streams[stream_index]->time_base.den,
flags & AVSEEK_FLAG_BACKWARD ?
AV_ROUND_DOWN : AV_ROUND_UP);
duration = s->duration == AV_NOPTS_VALUE ?
0 : s->duration;
/* 检查seek位置的有效性 */
if (0 < duration && duration < seek_timestamp - first_timestamp)
return AVERROR(EIO);
/* find the playlist with the specified stream */
/* 找到stream_index对应的playlist */
for (i = 0; i < c->n_playlists; i++) {
struct playlist *pls = c->playlists[i];
for (j = 0; j < pls->n_main_streams; j++) {
if (pls->main_streams[j] == s->streams[stream_index]) {
seek_pls = pls;
stream_subdemuxer_index = j;
break;
}
}
}
/* check if the timestamp is valid for the playlist with the
* specified stream index */
/* 检查给定的seek timestamp对指定的流是否有效 */
if (!seek_pls || !find_timestamp_in_playlist(c, seek_pls, seek_timestamp, &seq_no))
return AVERROR(EIO);
/* set segment now so we do not need to search again below */
seek_pls->cur_seq_no = seq_no;
seek_pls->seek_stream_index = stream_subdemuxer_index;
/* 下面是对正在读取的流的处理 */
for (i = 0; i < c->n_playlists; i++) {
/* Reset reading */
struct playlist *pls = c->playlists[i];
ff_format_io_close(pls->parent, &pls->input);
pls->input_read_done = 0;
ff_format_io_close(pls->parent, &pls->input_next);
pls->input_next_requested = 0;
av_packet_unref(pls->pkt);
pls->pb.eof_reached = 0;
/* Clear any buffered data */
//清除缓冲区数据
pls->pb.buf_end = pls->pb.buf_ptr = pls->pb.buffer;
/* Reset the pos, to let the mpegts demuxer know we've seeked. */
pls->pb.pos = 0;
/* Flush the packet queue of the subdemuxer. */
ff_read_frame_flush(pls->ctx);
pls->seek_timestamp = seek_timestamp;
pls->seek_flags = flags;
/* 对于不是seek的playlist进行处理 */
if (pls != seek_pls) {
/* set closest segment seq_no for playlists not handled above */
find_timestamp_in_playlist(c, pls, seek_timestamp, &pls->cur_seq_no);
/* seek the playlist to the given position without taking
* keyframes into account since this playlist does not have the
* specified stream where we should look for the keyframes */
pls->seek_stream_index = -1;
pls->seek_flags |= AVSEEK_FLAG_ANY;
}
}
c->cur_timestamp = seek_timestamp;
return 0;
}
复制代码
需要注意的是其中的第四个参数flag,表示seek到具体帧的方式。
#deflagfine AVSEEK_FLAG_BACKWARD 1 // 是seek到请求的timestamp之前最近的关键帧
#define AVSEEK_FLAG_BYTE 2 // 是基于字节位置的查找
#define AVSEEK_FLAG_ANY 4 //是可以seek到任意帧,注意不一定是关键帧,因此使用时可能会导致花屏
#define AVSEEK_FLAG_FRAME 8 //是基于帧数量快进
复制代码
小结
本文主要参考FFmpeg/libavformat/hls.c
,对其代码逻辑做了简单收集及整理。整体来说,本文总结了ffmpeg
中hls_demxuer
的实现逻辑,希望对读者有所帮助,感谢收看~~