hslenc.c 代码提纲挈领分析
author: hjjdebug
date: 2025年 09月 28日 星期日 17:48:41 CST
descrip: hslenc.c 代码提纲挈领分析
文章目录
- 1. 前言
- 1.1 运行方式:
- 1.2: 执行结果
- 1.3. 目的. 想在test.m3u8 文件中加点自己的东西.
- 2: 我想搞懂以下问题.
- 2.1: 是先打开m3u8文件, 还是先打开切片文件例如test0.ts?
- 2.2 问: pb 中都保存了什么呢?
- 2.3: 切片文件例如test0.ts 数据是怎样被写入的?
- 2.4 AVOutputFormat对象指针为什么可以直接转换为FFOutputFormat 对象指针?
- 2.5 hls 中有一个重要概念VariantStream. 它是什么?
- 2.6 AVIOContext 中的 write_packet 指针是何时赋值的?
- 3 test.m3u8 文件是在哪里打开的?
- 4. hls_write_packet 把数据写到了哪里? 为什么写了很多次包才调用到打开test.m3u8.tmp 文件?
1. 前言
被研究的程序. doc/example/transcode 程序.
这里分析的是ffmpeg6.1.1的版本, 在ubuntu24 下调试的.
1.1 运行方式:
命令行参数:
./transcode mp2.ts ts/test.m3u8
我们把一个ts 流, 按m3u8 格式重新编码后来输出.
1.2: 执行结果
我们看到, 在ts 目录下生成了test.m3u8 文件及 一系列切片文件.
$ls ts
test0.ts test1.ts test2.ts test3.ts test4.ts test.m3u8
$ cat test.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:2.400000,
test0.ts
#EXTINF:1.920000,
test1.ts
#EXTINF:1.920000,
test2.ts
#EXTINF:1.920000,
test3.ts
#EXTINF:1.800000,
test4.ts
#EXT-X-ENDLIST
1.3. 目的. 想在test.m3u8 文件中加点自己的东西.
难点: 在hlsenc.c 中观摩了一天,不是半天. 竟然还是懵懵懂懂. 菜啊,看不懂.
文件的打开已经看不到fopen, 文件写入也看不到fwrite, 这些是底层的接口,
上层就是另一种接口了,它封装了协议,封装了URL. 原则上应该更简单. 但实际上却不是.
单这一个文件就3500行代码, 很难理解. 这里不能copy过来一行行标注,那还是不得要领.
下面就要送根拐杖了, 帮助我们理解代码.
2: 我想搞懂以下问题.
test.m3u8 文件是在哪里打开的? 又是在哪里写入的? 在哪里关闭的?
切片文件例如 test0.ts, test1.ts 又是在哪里打开,写入和关闭的?
下面按执行顺序来依次解释.
先忽略掉 hsl_init() 及 hls_write_header() 函数, 这两个函数是avformat_write_header()调用的.
与创建test.m3u8 没有直接关系. 而且代码很多,开始时不易看懂.
- hls_init(AVFormatContext *s) 是由mux.c 调用的, 通过FFOutputFormat 对象 ff_hls_muxer 来调用
其调用栈:
0 in hls_init of libavformat/hlsenc.c:3264
1 in init_muxer of libavformat/mux.c:408 // 通过 s->oformat->init(), s->oformat就是ff_hls_muxer
2 in avformat_init_output of libavformat/mux.c:504
3 in avformat_write_header of libavformat/mux.c:529
hls_write_header 的调用栈也是一样,通过ff_hls_muxer 对象来调用
2. hls_write_header(AVFormatContext *s) …```
0 in hls_write_header of libavformat/hlsenc.c:2617
1 in avformat_write_header of libavformat/mux.c:536 //通过s->oformat->write_header
虽然它们也很关键,但只是做了一些初始化工作,即完善了外层AVFormatContext 对象的一些参数及
内层HLSContext 对象的一些参数. 对m3u8文件的生成没有直接关系.
libavformat/hlsenc.c 文件中, 打开文件的接口是
int hlsenc_io_open(AVFormat s, AVIOContextpb, char *filename, AVDictionary **options);
2.1: 是先打开m3u8文件, 还是先打开切片文件例如test0.ts?
答, 先打开和写入切片文件.
此时调用栈:
(gdb) info args
s = 0x555555624340
pb = 0x555555633be0
filename = 0x55555566d080 “ts/test0.ts”
options = 0x7fffffffd6f0
0 in hlsenc_io_open of libavformat/hlsenc.c:295
1 in hls_write_packet of libavformat/hlsenc.c:2926
2 in write_packet of libavformat/mux.c:818
3 in interleaved_write_packet of libavformat/mux.c:1238
4 in write_packet_common of libavformat/mux.c:1264
5 in write_packets_common of libavformat/mux.c:1333
6 in av_interleaved_write_frame of libavformat/mux.c:1404
7 in encode_write_frame of transcode.c:498
8 in filter_encode_write_frame of transcode.c:539
9 in main of transcode.c:610
可见它的库调用入口是av_interleaved_write_frame,
其底层对象ff_hls_muxer的接口时 hls_write_packet
文件内函数是hlsenc_io_open
hlsenc_io_open()
通过调用s->io_open 来打开filename, 保存结果到pb.
s->io_open(s, pb, filename, AVIO_FLAG_WRITE, options);
上面的函数s->io_open 作为基础接口,就不向下分析了. 意思时AVFormatContext s,
打开 filename, 结果春入pb.
2.2 问: pb 中都保存了什么呢?
pb 是一个AVIOContext 指针, 它包含一个opaque 指针, 实际是URLContext *
当AVIO 需要底层操作时,会传递这个指针.
URLContext 中包含一个char *filename, 保存了文件名.
另外还有2个重要成员: prot 指针, 对于此例它指向ff_file_protocol
及 priv_data 指针. 对于此例它指向一个FileContext *, 当调用prot中的函数时,
传递这个私有的context, 这个context中, 被打开的文件描述符fd 就保存于此.
这就是分层管理的概念, 也是面向对象的概念.
对象就是一个结构体, 对象有嵌套的概念,对象可以包含对象(或对象指针).
指针使得对象可以呈现树状结构,使得对象可以很复杂.
搞懂了底层数据结构,才能看懂底层程序.
如果我们不关心底层程序, 层次化管理, 也使我们可以不关心底层实现,而只关心接口.扯的有点远了.
2.3: 切片文件例如test0.ts 数据是怎样被写入的?
方式很多了,例如:
av_write_frame(oc, NULL); /* Flush any buffered data */
avio_flush(oc->pb);
avio_write(vs->out, vs->init_buffer, range_length);
ret = flush_dynbuf(vs, &range_length);
具体怎么写的,还是有点难查的.
用AVIOContext 对象打开的文件, 怎样进行读写?
采用一点逆向的技巧, 如果设置函数断点fwrite, 不行,会中断在av_log中.
另一个函数断点. libavformat/file.c 中有file_write() 函数
我们抓到了它的函数调用栈,
判定出它的书写函数在本文件中使用的是flush_dynbuf(核心函数). 返回长度range_length,
调用栈如下, 对你关注的flush_dynbuf 进行浏览, 理解其工作原理.
0 in file_write of libavformat/file.c:153
1 in retry_transfer_wrapper of libavformat/avio.c:364
2 in ffurl_write2 of libavformat/avio.c:425
3 in writeout of libavformat/aviobuf.c:186
4 in flush_buffer of libavformat/aviobuf.c:214
5 in avio_write of libavformat/aviobuf.c:263
6 in flush_dynbuf of libavformat/hlsenc.c:607 // *************** 重点
7 in hls_write_packet of libavformat/hlsenc.c:2939
8 in write_packet of libavformat/mux.c:818
9 in interleaved_write_packet of libavformat/mux.c:1238
10 in write_packet_common of libavformat/mux.c:1264
11 in write_packets_common of libavformat/mux.c:1333
12 in av_interleaved_write_frame of libavformat/mux.c:1404
13 in encode_write_frame of transcode.c:498
14 in filter_encode_write_frame of transcode.c:539
15 in main of transcode.c:610
// 函数代码不多,但意义却很重要, 重点标注一下.
static int flush_dynbuf(VariantStream* vs, int* range_length)
{AVFormatContext* ctx = vs->avf;//健壮性判别if (!ctx->pb) return AVERROR(EINVAL);// flush, 把编码器所有的frame 刷出去av_write_frame(ctx, NULL);// write out to file, 关闭动态缓冲区,并返回固定缓冲区地址temp_buffer和大小range_length*range_length = avio_close_dyn_buf(ctx->pb, &vs->temp_buffer);ctx->pb = NULL;//把内容写到缓冲区,缓冲区满会写到文件.avio_write(vs->out, vs->temp_buffer, *range_length);//刷出AVIOContext, 把缓存中所余内容全部写入到文件avio_flush(vs->out);// re-open buffer, 重新打开动态缓冲区return avio_open_dyn_buf(&ctx->pb);
}
再上一层:
int hls_write_packet(AVFormatContext* s, AVPacket* pkt)
{
......if(能够在此切分,并且时间也到了){ret = hlsenc_io_open(s, &vs->out, filename, &options); //代开文件ret = flush_dynbuf(vs, &range_length); //写数据ret = hlsenc_io_close(s, &vs->out, filename);//关闭文件}
......
}
可见切片文件是在一定条件下(video I frame and slice time over), 打开文件,书写数据,关闭文件
输出文件对象是AVFormatContext s, 它有一个AVOutputFormat 对象->oformat,
2.4 AVOutputFormat对象指针为什么可以直接转换为FFOutputFormat 对象指针?
ffofmt(s->oformat)
ffofmat() 函数是如下定义的,实际上就是一个强制指针类型转换. 是父类转换为子类!!
static inline const FFOutputFormat *ffofmt(const AVOutputFormat *fmt)
{return (const FFOutputFormat*)fmt;
}
当我打开hlsenc.c 文件, 发现定义的对象本来就是FFOutputFormat 对象. 这才一下恍然大悟!
const FFOutputFormat ff_hls_muxer = {.p.name = "hls",.p.long_name = NULL_IF_CONFIG_SMALL("Apple HTTP Live Streaming"),.p.extensions = "m3u8",.p.audio_codec = AV_CODEC_ID_AAC,.p.video_codec = AV_CODEC_ID_H264,.p.subtitle_codec = AV_CODEC_ID_WEBVTT,
#if FF_API_ALLOW_FLUSH.p.flags = AVFMT_NOFILE | AVFMT_GLOBALHEADER | AVFMT_ALLOW_FLUSH | AVFMT_NODIMENSIONS,
#else.p.flags = AVFMT_NOFILE | AVFMT_GLOBALHEADER | AVFMT_NODIMENSIONS,
#endif.p.priv_class = &hls_class,//后面成员是属于子类FFOutputFormat 对象的.flags_internal = FF_FMT_ALLOW_FLUSH,.priv_data_size = sizeof(HLSContext),.init = hls_init,.write_header = hls_write_header,.write_packet = hls_write_packet,.write_trailer = hls_write_trailer,.deinit = hls_deinit,
};
可见所谓的
oformat = av_guess_format(format, NULL, NULL);
其返回值地址
const AVOutputFormat *av_guess_format(const char *short_name, const char *filename,
const char *mime_type)
函数声明的返回地址是AVOutputFormat, 但实际枚举的是FFOutputFormat 地址.
所以,你以后把AVOutputFormat 指针强制转换为FFOutputFormat 地址不会出错, 因为它本来就是继承类地址.
而子类退化为父类地址是自然的. 你用的信息少,这不会出问题, 这是c++中继承的概念.
父类指针被强制转换为子类,是因为它本来就是子类指针.
2.5 hls 中有一个重要概念VariantStream. 它是什么?
它是一个对象, 把结构抄写过来也没有什么意义. 这里就节省篇幅不copy了.
它的用途是为了针对不同的码率而设置的一个对象. 多个对象意味着有多种输出.
因而会有一个VariantStream 指针和一个个数. 不过通常只有一个输出流(我们测试的是一个输出流).
它会包含一个 AVOutputFormat *oformat; 用以保存输出文件封装格式参数(muxer)
并包含一个 AVIOContext *out; 用以书写数据
还包含一个 AVFormatContext *avf; 用以保存格式,流信息,i/o操作句柄等信息
2.6 AVIOContext 中的 write_packet 指针是何时赋值的?
如果直接定位hls 中的 VarantStream 中的out 变量, 即hls->var_streams[0].out
而hls 是AVOutputFormat 中的私有数据结构, 就知道这个数据隐藏的很深了.
(gdb) p ((HLSContext *)g_ofmt_ctx->priv_data)->var_streams
$25 = (VariantStream *) 0x555555633bc0
(gdb) p ((HLSContext *)g_ofmt_ctx->priv_data)->var_streams[0].out
$26 = (AVIOContext *) 0x5555556c1380
hls->var_streams 在hls_init()->update_variant_stream_info() 函数中赋值.
而其下var_streams[0].out 是一个AVIOContext 对象. 其赋值通过下列函数.
*s = avio_alloc_context(buffer, buffer_size, h->flags & AVIO_FLAG_WRITE, h,
ffurl_read2, ffurl_write2, ffurl_seek2);
其write_packet 指针的赋值,显然是传递的参数ffurl_write2.
是的,我是通过内存断点及函数观察多重方式获取的信息.
3 test.m3u8 文件是在哪里打开的?
先打开一个test.m3u8.tmp, 以后再重命名为test.m3u8
调用栈如下: 你可以找到它的代码.
(gdb) info args
s = 0x555555624340
pb = 0x555555633be0
filename = 0x7fffffffb670 “ts/test.m3u8.tmp”
options = 0x7fffffffb628
0 in hlsenc_io_open of libavformat/hlsenc.c:295
1 in hls_window of libavformat/hlsenc.c:1777 //****在该函数中形成m3u8文件
2 in hls_write_packet of libavformat/hlsenc.c:2987
3 in write_packet of libavformat/mux.c:818
4 in interleaved_write_packet of libavformat/mux.c:1238
5 in write_packet_common of libavformat/mux.c:1264
6 in write_packets_common of libavformat/mux.c:1333
7 in av_interleaved_write_frame of libavformat/mux.c:1404
8 in encode_write_frame of transcode.c:498
9 in filter_encode_write_frame of transcode.c:539
10 in main of transcode.c:610
观察hls_window(), 赫然发现, 它调用了一堆ff_hls_write_开头的函数, 原来这些函数在其它的文件中,
是外部引用函数. 例如: ff_hls_write_playlist_head(…) 函数, 就在hlsplaylist.c文件中定义.
这个文件不大, 原来字符串向m3u8文件的输出都在这里. 不足200行代码我们还是能看懂!
怎样向文件输出的, 用avio_printf(AVIOContest *s, char *fmt, …) 函数向文件输出的.
当创建了切片文件,然后会更新m3u8文件, 通过hls_window() 调用 hlsenc_io_open, 参考上面的调用栈
打开文件, 在hls_window() 函数中调用ff_hls_开始的函数,用avio_printf 书写信息到文件中.
4. hls_write_packet 把数据写到了哪里? 为什么写了很多次包才调用到打开test.m3u8.tmp 文件?
答: 它写到了dyn_buffer中, 动态分配的内存中. 写的时间到,一次性写到文件中. 这里就不详细分析了