& 前言
HLS(Http Live Streaming)是由Apple公司定义的用于实时流传输的协议,HLS基于HTTP协议实现,包括M3U8描述文件和TS媒体文件,本文基于FFMPEG 4.2.2版本,重点介绍了加密HLS和非加密HLS的数据读取差异。
几个流程:
HLS M3U8列表打开流程:
avformat_open_input->init_input->io_open_default->ffio_open_whitelist->ffurl_open_whitelist->ffurl_connect->http_open->http_open_cnx->http_open_cnx_internal->http_connect
HLS打开网络连接流程:
socket函数创建描述符fd对应两个缓冲区,一个输入缓冲区,一个输出缓冲区,而recv和send函数就是对这两个函数进行操作。
int recv( SOCKET s, char *buf, int len, int flags);
函数功能:客户端、服务端用recv从TCP对端接收数据。
参数s指定接收端套接字描述符;参数buf指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;参数len指明buf的长度;参数flag一般置为0。
int send( SOCKET s,char *buf,int len,int flags );
客户端、服务端用send函数来向TCP连接的另一端发送数据。客户程序用send函数向服务器发送请求,服务器用send函数来向客户程序发送应答。
参数s指定发送端套接字描述符,参数buf存放应用程序要发送数据的缓冲区,参数len表示实际要发送的数据的字节数,参数flag一般置为0。
HLS数据读取流程:
http_connect之后,http_read_header调用ffurl_read会返回如下列表,包括HTTP Header和M3U8列表,如下图示:
M3U8列表会被保存在URLContext的priv_data中,此时priv_data是HTTPContext,也就是下载到的M3U8列表会被保存在HTTPContext的buf_ptr指向的那段buffer。URLContext在ffurl_open_whitelist时调用ffurl_alloc分配。在ffio_open_whitelist中ffio_fdopen时,URLContext被挂在AVIOInternal的URLContext变量h中。AVIOInternal作为AVIOContext的变量,被保存在AVIOContext的void *opaque变量中。而AVIOContext就是AVFormatContext的pb参数。由于每一个HLS的playlist都需要有不同的AVIOContext,因此,此处的AVIOContext和playlist对应的AVIOContext不是同一个。
http_read_stream的时候,先调用http_buf_read查看HTTPContext的buf_ptr指向的那段buffer中有无数据,若无数据才会调用ffurl_read从网络读取。因此无需要担心此M3U8列表会被丢掉,此时,avformat_open_input->init_input->av_probe_input_buffer2->av_probe_input_format2->av_probe_input_format3->hls_probe这个流程就识别到了HLS DEMUX。
HLS Parse M3U8列表
avformat_open_input->hls_read_header->parse_playlist
playlist挂在HLSContext的playlists变量中的。HLS可以有多个playlist,数量统计在n_playlists中,一个HLS包含所有的variant和所有的playlist:
playlist从来都不是单独产生,要么是在new variant时产生,要么是在new rendition时产生,不管哪种方式产生,都会添加到HLS环境变量的playlists中。就算是没有variant列表的场景,也是要产生variant。
这儿有必要讲述一样variant相关含义,variant stream,就是可变的码流,表示同一个影片有多个不同版本的码流,比如007这部影片,variant stream 就可以包含480p,720p,1080p等等多个不同分辨率的码流,这个variant 的变化也可以是分辨率以外的其他部分,比如Audio,编码方式,字幕等等。用户第一个M3U8列表称为主列表(Master Playlist),这个主列表可能就是媒体播放列表(Media Playist),里面是媒体文件的URL,Gstreamer用main_stream来表示不包含下级播放列表的M3U8文件信息。主列表可能就是包含下一级播放列表的列表,用#EXT-X-STREAM-INF描述下一级M3U8列表的URL,这就是variant。这儿有一个包含variant主列表的例子:即主列表有多个variant stream。关系就是主M3U8->variant->playlist。
EXT-X-MEDIA: 描述了一个可供选择的媒体内容组,这些组内的码流具有类似的编码类型,相同的带宽等诸多相似特征。如果#EXT-X-MEDIA后面有URI,则表示相应将源在URI描述的列表中,如果后面没有URL,则表示它对应的版本在EXT-X-STREAM-INF中体现,此时EXT-X-STREAM-INF中的AUDIO或者VIDEO的GROUP-ID必须和EXT-X-MEDIA中的GROUP-ID匹配。renditions也是在找到#EXT-X-MEDIA后产生的,一个rendition的例子:
EXT-X-MEDIA的GROUP可以有多个,GROUP间的码流可以具有不同的CODEC和码率,
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="englo/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="frelo/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="es",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="splo/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="eng/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="fre/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="es",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="sp/prog_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=195023,CODECS="mp4a.40.5", AUDIO="audio-lo"
lo/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=260000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-lo"
hi/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=591680,CODECS="mp4a.40.2, avc1.64001e", AUDIO="audio-hi"
lo/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=650000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-hi"
hi/prog_index.m3u8
这样的需求来源一方面是可变码率视频自适应网络带宽以便流畅播放视频内容,同时也可以进行音轨的切换;另外一方面,有些视频可以进行多角度的播放,这些不同角度的视频存在于不同的M3U8子列表中,而声音只需要同一个就行,所以用这种方式将声音和视频分开存放,最后合在一起播放,如:
#EXTM3U
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO, URI="Angle2/500kbs/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO, URI="Angle3/500kbs/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="en",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="eng/prog_index.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=754857,CODECS="mp4a.40.2,avc1.4d401e", VIDEO="500kbs",AUDIO="aac"
Angle1/500kbs/prog_index.m3u8
上述列表有3个不同角度的视频,对应一个音频流。也可以用GROUP-ID将相同码率的音频和视频对应起来,不同码率相应也相应地区分开来。
#EXTM3U
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO, URI="Angle2/200kbs/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO, URI="Angle3/200kbs/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO, URI="Angle2/500kbs/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO, URI="Angle3/500kbs/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="en",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="eng/prog_index.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000,CODECS="mp4a.40.2,avc1.4d401e", VIDEO="200kbs",AUDIO="aac"
Angle1/200kbs/prog_index.m3u
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=754857,CODECS="mp4a.40.2,avc1.4d401e", VIDEO="500kbs",AUDIO="aac"
Angle1/500kbs/prog_index.m3u8
所以rendition就可以理解成是分离出音视频的码流,而variant未必,#EXT-X-MEDIA存在播放列表当中,就表示当前多媒体流的A/V存放可能是分开的,当然也可以不分开。
HLSContext则挂在AVFormatContext的priv_data中;playlist中也有直接指向AVFormatContext的指针ctx。
M3U8列表的一个时间分片叫做一个segment,,一个playlist对应n_segments个码流切片。上述的HLS码流经过了加密,所以有Key和IV信息,segment包含了url和URL对应码流的key的信息,以及16字节的定长IV数据。一个M3U8切片可以有一个IV数据和Key URL,也可以多个M3U8切片共用一个IV数据和Key URL,取决于EXT-X-KEY字段的下发方式。Key保存在服务器上,通过Key的URL获取,IV数据保存在HLS码流的M3U8列表的EXT-X-KEY字段中。Key用来初始化解密模块的参数,再配合IV数据解密。也就是共用同一个Key的那些切片,实际上只用初始化一次解密模块。若同一个URL中的Key可变,那就得每个切片老老实实地去取Key并且初始化解密模块了。存在于URL中的Key在KEY_AES_128中也是16个字节,打开一次切片Seg时,就向服务器请求一次Key。从这个层面来讲,即便Key所属的服务器URL是同一个,IV数据也是同一样,Seg单独请求URL的时候,存在于服务器的Key的实际值也是可以不同的。
非加密HLS & 加密HLS
hls_read_header的时候,向服务器请求Key。
请求Key的流程走的是普通的http open流程:avformat_open_input->hls_read_header->av_probe_input_buffer->av_probe_input_buffer2->avio_read->fill_buffer->read_packet_wrapper->read_data->open_input->open_url->io_open_default->ffio_open_whitelist->ffurl_open_whitelist->ffurl_connect->http_open。
打开加KEY数据URL时走的流程是:avformat_open_input->hls_read_header->av_probe_input_buffer->av_probe_input_buffer2->avio_read->fill_buffer->read_packet_wrapper->read_data->open_input->open_url->io_open_default->ffio_open_whitelist->ffurl_open_whitelist->ffurl_connect->crypto_open2->ffurl_open_whitelist->ffurl_connect->http_open->http_open_cnx->http_open_cnx_internal->http_connect,如果此时打开失败,HLS也是不能播放的。
即没有加密Key的场景,read_data时,走key_type == KEY_NONE分支;有加密Key的场景,open_url时,会先打开加密Key URL,再打开seg->url; 非加密场景用的是seg->url,加密场景在url前添加了"crypto+"或者"crypto:"字符。
这样,crypto_open2打开的加密URL就和当前Read的Segment的数据对应起来了!
带Key的URL读取数据流程:
read_thread->avformat_open_input->hls_read_header->av_probe_input_buffer->av_probe_input_buffer2->avio_read->fill_buffer->read_packet_wrapper->read_data->read_from_url->avio_read->fill_buffer->read_packet_wrapper->io_read_packet->ffurl_read->retry_transfer_wrapper->crypto_read->ffurl_read->retry_transfer_wrapper->http_read->http_read_stream
HLS 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;
restart:
/* 放弃读不需要的播放列表 */
if (!v->needed)
return AVERROR_EOF;
/* input 就是playlist的AVIOContext, 在aviobuf.c的ffio_fdopen函数中用avio_alloc_context分配,挂接I/O操作函数
* 加密场景每个Seg读取完后都会释放input,非加密HLS不会释放,只打开两次,在input和input_next之间切换。
* http_persistent就是HTTP persistent connection,也称作HTTP keep-alive或HTTP connection reuse,
* 是使用同一个TCP连接来发送和接收多个HTTP请求/应答,而不是为每一个新的请求/应答打开新的连接的方法
* 默认值是1.
* input_read_done 表示非加密HLS时,http持续连接,当前Seg切片数据读取结束。
*/
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. */
v->needed = playlist_needed(v);
……
/* 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:
/* finished 为0表示播放列表未遇到#EXT-X-ENDLIST标记,否则为1
* 播放列表未结束并且超过重新获取播放列表的时间,重新获取播放列表。
*/
if (!v->finished &&
av_gettime_relative() - v->last_load_time >= reload_interval) {
if ((ret = parse_playlist(c, v->url, v, NULL)) < 0) {
……
}
/* 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. */
reload_interval = v->target_duration / 2;
}
/* 播放列表过期,使用新的 */
if (v->cur_seq_no < v->start_seq_no) {
v->cur_seq_no = v->start_seq_no;
}
/* 播放列表全部无效,超时重新更新 */
if (v->cur_seq_no >= v->start_seq_no + v->n_segments) {
if (v->finished)
return AVERROR_EOF;
while (av_gettime_relative() - v->last_load_time < reload_interval) {
……
}
/* Enough time has elapsed since the last reload */
goto reload;
}
v->input_read_done = 0;
/* 获取需要的segment信息,即pls->segments[pls->cur_seq_no - pls->start_seq_no] */
seg = current_segment(v);
/* load/update Media Initialization Section, if any,通常init_section和cur_init_section都有空,什么都不做 */
ret = update_init_section(v, seg);
……
/* 非加密HLS,支持多个HTTP连接,并且下一个seg的I/O成功打开。http_multiple初始值为-1,所以加密还是非加密,第一次进来都会走下面的open_input流程 */
if (c->http_multiple == 1 && v->input_next_requested) {
/* 将v->input_next和 v->input交换,所以在free 播放列表的时候,要关闭input_next,交换后,
* input_next_requested置0, 表示期望准备好下一个URL的环境,也就是使用当前I/O的同时,准备好下一次I/O的环境。*/
FFSWAP(AVIOContext *, v->input, v->input_next);
v->input_next_requested = 0;
ret = 0;
} else {
ret = open_input(c, v, seg, &v->input);
}
/* open_input失败取下一个seg 切片 */
if (ret < 0) {
v->cur_seq_no += 1;
goto reload;
}
just_opened = 1;
}
if (c->http_multiple == -1) {
/* HTTP 1.1, 2.0支持长连接(PersistentConnection),在一个TCP连接上可以传送多个HTTP请求和响应,1.0不支持 */
c->http_multiple = (!strncmp((const char *)http_version_opt, "1.1", 3) || !strncmp((const char *)http_version_opt, "2.0", 3));
}
/* pls->cur_seq_no - pls->start_seq_no + 1, 未打开过的下一个URL,注意这儿是v->input_next
* 非加密时,不会真正去做open_input操作,而是在open_url时,由于is_http && c->http_persistent && AVIOContext,
* 由open_url_keepalive函数,准备好下一次I/O操作所需要的环境*/
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) {
……
} 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);
/* 执行到此处,就表明当前的seg打开OK,可以读取数据了 */
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;
}
/* 已经不能从当前切片中读取数据了,关闭v->input,移动到下一个切片,非加密场合,播放列表的I/O不用重新open_input */
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);
}
v->cur_seq_no++;
c->cur_seq_no = v->cur_seq_no;
goto restart;
}
HLS hls_read_packet
hls_read_header
static int hls_read_header(AVFormatContext *s)
{
HLSContext *c = s->priv_data;
int ret = 0, i;
int highest_cur_seq_no = 0;
int select_playlist = 0;
c->ctx = s;
/* avformat_open_input前用户设置的callback参数 */
c->interrupt_callback = &s->interrupt_callback;
c->first_packet = 1;
c->first_timestamp = AV_NOPTS_VALUE;
c->cur_timestamp = AV_NOPTS_VALUE;
if ((ret = save_avio_options(s)) < 0)
goto fail;
/* Some HLS servers don't like being sent the range header */
av_dict_set(&c->avio_opts, "seekable", "0", 0);
/* 解析M3U8列表 */
if ((ret = parse_playlist(c, s->url, NULL, s->pb)) < 0)
goto fail;
/* 无M3U8列表存在 */
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.存在多个子列表 */
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的第一个播放列表无seg */
if (c->variants[0]->playlists[0]->n_segments == 0) {
av_log(s, AV_LOG_WARNING, "Empty segment\n");
ret = AVERROR_EOF;
goto fail;
}
/* If this isn't a live stream, calculate the total duration of the
* stream. 非直播流,计算时长*/
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];
/* audio的GROUP-ID存在,找到var->audio_group对应的rendition的playlist,
* 如果这个playlist存在,表明rendition是一个外部播放列表,
* 就把rendition的playlist加到var代表的播放列表中,这儿就体现了出了variant具有多个playlists的原因
* 如果这个playlist不存在,即EXT-X-MEDIA后没有带URI,代表rendition其实就在Master播放列表里面,
* 由#EXT-X-STREAM-INF来描述,此时,只需要将这个rendition加到var的playlists的renditions中,此时AV不一定会分开存放。
* 后面两个解释类似。
*/
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;
/* 每个variant占用一个节目 */
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);
}
/* Open the demuxer for each playlist ,可以看到,
* 每个播放列表都要分配一套音视频播放参数,存储列表较多时,这个开销还是挺夸张的
*/
for (i = 0; i < c->n_playlists; i++) {
struct playlist *pls = c->playlists[i];
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->pb.seekable = 0;
/* 识别当前播放列表所属码流的封装类型。*/
ret = av_probe_input_buffer(&pls->pb, &in_fmt, pls->segments[0]->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", pls->segments[0]->url);
avformat_free_context(pls->ctx);
pls->ctx = NULL;
goto fail;
}
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;
/* 用具体的demux来初步分析这个流,比如用ts demux来解析这个流。*/
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);
pls->id3_deferred_extra = NULL;
}
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;
fail:
hls_close(s);
return ret;
}
ISOBMFF
除包含TS格式的码流外,HLS列表中的码流还可以是MP4,称为ISO Base Media File Format ISO/IEC 14496-12 [ISOBMFF],AV可以分开存放,Master列表如下:
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="ac-3",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="audio/prog_index.m3u8"
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=4333805,BANDWIDTH=10968495,CODECS="avc1.64002a",RESOLUTION=1920x1080,FRAME-RATE=60.000,CLOSED-CAPTIONS=NONE,AUDIO="ac-3"
video/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=641207,BANDWIDTH=1743419,CODECS="avc1.64002a",RESOLUTION=1920x1080,URI="video/iframe_index.m3u8"
Audio的m3u8子列表:
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:7
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;charset=UTF-16;base64,uAIAAAEAAQCuAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADMALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AZQB4AHAAZQByAGkAbQBlAG4AdABhAGwAMQAuAGEAegB1AHIAZQB3AGUAYgBzAGkAdABlAHMALgBuAGUAdAAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4AD8AYwBmAGcAPQAoAGMAawA6AFcAMwAxAGIAZgBWAHQAOQBXADMAMQBiAGYAVgB0ADkAVwAzADEAYgBmAFEAPQA9ACwAYwBrAHQAOgBBAEUAUwAxADIAOABCAGkAdABDAEIAQwApADwALwBMAEEAXwBVAFIATAA+ADwAUABSAE8AVABFAEMAVABJAE4ARgBPAD4APABLAEkARABTAD4APABLAEkARAAgAEEATABHAEkARAA9ACIAQQBFAFMAQwBCAEMAIgAgAFYAQQBMAFUARQA9ACIAQQBBAEEAQQBFAEEAQQBRAEEAQgBBAFEAQQBCAEEAQQBBAEEAQQBBAEEAUQA9AD0AIgA+ADwALwBLAEkARAA+ADwALwBLAEkARABTAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A",KEYFORMAT="com.microsoft.playready",KEYFORMATVERSIONS="1"
#EXT-X-MAP:URI="bbb_sunflower_1080p_60fps_normal_AUDIO0.mp4"
#EXTINF:9.98400,
bbb_sunflower_1080p_60fps_normal_AUDIO1.mp4
#EXTINF:9.98400,
bbb_sunflower_1080p_60fps_normal_AUDIO2.mp4
#EXTINF:9.98400,
bbb_sunflower_1080p_60fps_normal_AUDIO3.mp4
……
#EXT-X-ENDLIST
Video子列表:
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:7
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;charset=UTF-16;base64,uAIAAAEAAQCuAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADMALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AZQB4AHAAZQByAGkAbQBlAG4AdABhAGwAMQAuAGEAegB1AHIAZQB3AGUAYgBzAGkAdABlAHMALgBuAGUAdAAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4AD8AYwBmAGcAPQAoAGMAawA6AFcAMwAxAGIAZgBWAHQAOQBXADMAMQBiAGYAVgB0ADkAVwAzADEAYgBmAFEAPQA9ACwAYwBrAHQAOgBBAEUAUwAxADIAOABCAGkAdABDAEIAQwApADwALwBMAEEAXwBVAFIATAA+ADwAUABSAE8AVABFAEMAVABJAE4ARgBPAD4APABLAEkARABTAD4APABLAEkARAAgAEEATABHAEkARAA9ACIAQQBFAFMAQwBCAEMAIgAgAFYAQQBMAFUARQA9ACIAQQBBAEEAQQBFAEEAQQBRAEEAQgBBAFEAQQBCAEEAQQBBAEEAQQBBAEEAUQA9AD0AIgA+ADwALwBLAEkARAA+ADwALwBLAEkARABTAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A",KEYFORMAT="com.microsoft.playready",KEYFORMATVERSIONS="1"
#EXT-X-MAP:URI="bbb_sunflower_1080p_60fps_normal_VIDEO0.mp4"
#EXTINF:8.31667,
bbb_sunflower_1080p_60fps_normal_VIDEO1.mp4
#EXTINF:7.91667,
bbb_sunflower_1080p_60fps_normal_VIDEO2.mp4
#EXTINF:8.03333,
bbb_sunflower_1080p_60fps_normal_VIDEO3.mp4
……
#EXT-X-ENDLIST
有如下约束:
一、媒体的类型必须可以识别,就是说能够判断出当前是声音还是视频。
二、必须具有用于初始化数据
ISO BMFF用于初始化的数据定义成“ftyp”文件类型box——File Type Box和紧接着的Movie Box (moov),注意:
1. 用户必须检查File Type Box 中的版本和兼容性标记,确保支持。
2. Movie Box中的参数不能违反第1条中的版本要求。
3. Movie Box包含音视频数据或者其信息,如stts, stsc or stco boxes设置成非0。
4. mvex,即A Movie Extends box必须包含在初始化数据moov box中,这个BOX用于告诉用户,接下来可能会有Movie Fragment Boxes(moof).
此外,
1. 用户需要能够处理影片合成时间到显示时间的偏移
2. 能够处理带内带外codec参数配置(比如存放在文件中的SPS,PPS信息以及存放在码流中的这些信息)。
3. pdin, free, and sidx可以出现在moov box之前,但不作为初始化数据的一部分。
4. 能够处理轨道信息中的id,kind,label和language信息。
三、要有流媒体数据
这个阶段有一个可选的BOX,叫Segment Type Box (styp),styp不存在时意味着当前数据段应该使用文件类型box ftyp中的初始化参数。styp后依次是Movie Fragment Box(moof)和Media Data Boxes (mdat),一个segment中的moof可以有多个。除ftyp, moov, styp, moof, 和mdat之外,数据段之前可能有其他类型的box,这些box可以被识别,但不作为段本身的一部分。同样具有以下约束:
1. moof必须与ftyp, styp中参数兼容
2. styp和ftyp必须兼容
3. moof至少包含一个轨道分段box, Track Fragment Box (traf)。
4. moof使用相对地址
5. 不能使用外部数据
6. traf必须包含Track Fragment Decode Time Box (tfdt)
7. Media Data Boxes mdat必须包含所有的码流数据,这些码流数据由Track Fragment Run Boxes(trun)来索引。
8. 带内参数必须出现在相应的码流中。
详情可以参阅如下链接:
1. https://www.w3.org/TR/mse-byte-stream-format-isobmff/#bib-ISOBMFF
2. https://www.w3.org/TR/media-source/#abstract
3. https://www.rfc-editor.org/rfc/rfc6381 关于BOX类型的媒体'Codecs'和'Profiles'参数介绍
4. https://developer.apple.com/documentation/http-live-streaming 苹果网站,有关于HLS的原理性介绍和工具