ffmpeg 之 hls

前言

  与其他博客不同,本文不是讲解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 到底是怎么一回事了。

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值