目录
[前言:为什么 FFmpeg 6 的 HLS 开发才值得写](#前言:为什么 FFmpeg 6 的 HLS 开发才值得写)
[一、先建立心智模型:HLS 在 FFmpeg API 里是什么?](#一、先建立心智模型:HLS 在 FFmpeg API 里是什么?)
[二、最小可跑的 FFmpeg 6 HLS 初始化](#二、最小可跑的 FFmpeg 6 HLS 初始化)
[1️. 找 hls muxer(不是 flv / mp4)](#1️. 找 hls muxer(不是 flv / mp4))
[2️. 核心 HLS 选项](#2️. 核心 HLS 选项)
[3️. 打开输出(playlist 名)](#3️. 打开输出(playlist 名))
[4️. 喂流(和 mp4 一模一样)](#4️. 喂流(和 mp4 一模一样))
[5️. 结束 / 重推](#5️. 结束 / 重推)
[三、FFmpeg 6 里 HLS 切片的"真正触发条件"](#三、FFmpeg 6 里 HLS 切片的“真正触发条件”)
[四、进阶:接管 segment IO](#四、进阶:接管 segment IO)
[用 io_openhook segment 写入](#用 io_openhook segment 写入)
[五、多码率 Master Playlist](#五、多码率 Master Playlist)
[Step 1:生成多个 variant](#Step 1:生成多个 variant)
[Step 2:自己写 master.m3u8](#Step 2:自己写 master.m3u8)
[六、fmp4 + LL-HLS](#六、fmp4 + LL-HLS)
[七、直播HLS的5个API 级坑](#七、直播HLS的5个API 级坑)
[坑 1:没 keyframe → ts 无限长](#坑 1:没 keyframe → ts 无限长)
[坑 2:avformat_write_header 后立刻断流](#坑 2:avformat_write_header 后立刻断流)
[坑 3:segment_filename 没 %d](#坑 3:segment_filename 没 %d)
[坑 4:pts/dts 不连续没 discontinuity](#坑 4:pts/dts 不连续没 discontinuity)
[坑 5:Windows 路径 UTF-8 问题](#坑 5:Windows 路径 UTF-8 问题)
[八、调试 HLS API 的黄金三板斧](#八、调试 HLS API 的黄金三板斧)
[九、什么时候该用 API,什么时候用命令?](#九、什么时候该用 API,什么时候用命令?)
觉得有用,就请您帮忙点赞转发收藏吧,您的鼓励是我创作的动力,多谢看官。
由于能力水平有限,文中的错误或不严谨的地方在所难免,还请批评指正。
FFmpeg 是一个非常强大的开源库,用于处理视频和音频文件,包括编码、转码、流媒体等。使用 FFmpeg 的 API 来开发 HLS(HTTP Live Streaming)流媒体服务,你可以将视频实时编码并封装成 HLS 格式,使其能够在支持 HLS 的播放器中播放。以下是如何使用 FFmpeg 的 API 来实现 HLS 开发的基本步骤和示例。
前言:为什么 FFmpeg 6 的 HLS 开发才值得写
很多人对 FFmpeg + HLS 的认知还停在:
ffmpeg -i x.mp4 -f hls out.m3u8
但真实工程里(播放器 / 推流服务器 / 边缘录制 / 本地 OSD 回看):
-
你要 自己 mux → segment → playlist
-
要控制 切片时机 / discontinuity / init segment
-
要对接 CDN 回调 / 磁盘回收 / 加密
FFmpeg 6.x 的 libavformat 对 HLS / Segment muxer 做了不少收敛:
-
hlsenc内部逻辑更稳 -
AVFormatContext选项体系完整 -
io_open / io_close钩子成熟,适合"自己接管 ts 写文件 / 发内存"
这篇不讲命令行,只讲 API 层:用 FFmpeg 6 写一套"能上生产的 HLS 切片器"。
一、先建立心智模型:HLS 在 FFmpeg API 里是什么?
API 视角下,HLS 不是"格式",而是:
AVFormatContext
│
├── oformat = av_guess_format("hls", NULL, NULL)
│
├── priv_options ──► HLS muxer internal state
│
├── avio_open ──► playlist (.m3u8)
│
└── segment_write ──► ts / fmp4 + 滚动删除
一句话:
HLS muxer = 一个"会定时切片的 Muxer",你只负责往里喂 AVPacket
切片、命名、m3u8 维护,全是 libavformat/hlsenc.c帮你做的。
二、最小可跑的 FFmpeg 6 HLS 初始化
1️. 找 hls muxer(不是 flv / mp4)
AVFormatContext *oc = avformat_alloc_context();
oc->oformat = av_guess_format("hls", NULL, NULL);
if (!oc->oformat) {
// FFmpeg 没编进 hls muxer
}
2️. 核心 HLS 选项
FFmpeg 6 推荐用 AVDictionary设置 priv options:
AVDictionary *opts = nullptr;
av_dict_set(&opts, "hls_time", "4", 0); // segment 建议时长
av_dict_set(&opts, "hls_list_size", "5", 0); // 直播窗口
av_dict_set(&opts, "hls_flags", "delete_segments+omit_endlist", 0);
av_dict_set(&opts, "hls_segment_filename", "seg_%05d.ts", 0);
av_dict_set(&opts, "hls_segment_type", "mpegts", 0); // 或 fmp4
注意:
-
不是 AVCodecContext 参数
-
是
hlsenc的 priv_class options
3️. 打开输出(playlist 名)
if (avformat_write_header(oc, &opts) < 0)
// 失败多半是 hls_segment_filename 路径非法
FFmpeg 6 里:
-
oc->url = "index.m3u8"(playlist) -
ts 文件名由
hls_segment_filename控制
4️. 喂流(和 mp4 一模一样)
AVPacket *pkt = av_packet_alloc();
// fill pkt (stream_index / pts / dts / flags)
pkt->pts = av_rescale_q(pts, tb_in, oc->streams[0]->time_base);
pkt->dts = av_rescale_q(dts, tb_in, oc->streams[0]->time_base);
av_interleaved_write_frame(oc, pkt);
av_packet_unref(pkt);
关键认知
-
GOP / keyframe 决定真实切片点
-
你不用管"切不切",hls muxer 自己 watch pkt->flags
5️. 结束 / 重推
av_write_trailer(oc);
avformat_free_context(oc);
直播场景一般 不写 trailer,进程退出前 flush 即可。
三、FFmpeg 6 里 HLS 切片的"真正触发条件"
很多人 API 层踩坑,以为 hls_time=4就是 4 秒必切。
实际触发顺序
av_write_frame()
→ hls_write_packet()
→ cur_segment_duration >= hls_time ?
AND pkt->flags & AV_PKT_FLAG_KEY ?
→ hls_start_new_segment()
所以你必须保证:
-
视频流 定期 keyframe
-
或强制:
oc->oformat->video_codec = AV_CODEC_ID_H264;
// 编码器 side-data 给 keyint
FFmpeg 6 官方建议:
HLS 切片时长 = hls_time + GOP 对齐
四、进阶:接管 segment IO
这是工程级差异点。
用 io_openhook segment 写入
static int my_io_open(AVFormatContext *s, AVIOContext **pb,
const char *url, int flags, AVDictionary **opts)
{
// url = "seg_00001.ts"
FILE *f = fopen(url, "wb");
avio_alloc_context(...);
return 0;
}
oc->io_open = my_io_open;
oc->io_close = my_io_close;
能干什么:
-
ts 直接写内存
-
写 S3 / 本地缓存
-
segment 写完后回调 CDN flush
-
统计每个 ts 大小 / 码率
五、多码率 Master Playlist
FFmpeg 不会自动生成 master.m3u8,API 层标准做法:
Step 1:生成多个 variant
hls_720p/index.m3u8
hls_480p/index.m3u8
Step 2:自己写 master.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
hls_720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=854x480,CODECS="avc1.64001e,mp4a.40.2"
hls_480p/index.m3u8
FFmpeg 6 官方示例也是这么干的
别试图用 avformat_write_header自动生成(不支持)
六、fmp4 + LL-HLS
参数差异
av_dict_set(&opts, "hls_segment_type", "fmp4", 0);
av_dict_set(&opts, "hls_fmp4_init_filename", "init.mp4", 0);
av_dict_set(&opts, "hls_time", "2", 0);
av_dict_set(&opts, "hls_flags", "independent_segments+omit_endlist", 0);
生成结构:
init.mp4
seg_00000.m4s
seg_00001.m4s
index.m3u8
优势:
-
头小
-
Apple LL-HLS 友好
-
适合 H.265 / AV1
七、直播HLS的5个API 级坑
坑 1:没 keyframe → ts 无限长
解决:
x264_param.rc.i_keyint_max = fps * 2;
x264_param.b_repeat_headers = 1;
坑 2:avformat_write_header 后立刻断流
→ 没喂一帧就 write_trailer
→ m3u8 只有 header + ENDLIST
坑 3:segment_filename 没 %d
→ 所有 ts 同名 → CDN 脏缓存
坑 4:pts/dts 不连续没 discontinuity
API 层解决:
av_dict_set(&opts, "hls_flags", "discont_start", 0);
或 source 重连后手动:
av_write_frame(NULL); // hlsenc 内部识别 discontinuity
坑 5:Windows 路径 UTF-8 问题
建议:
av_dict_set(&opts, "hls_segment_filename", "./seg_%05d.ts", 0);
或 hook io_open 自己 fopen_s + utf8 转 wchar
八、调试 HLS API 的黄金三板斧
av_log_set_level(AV_LOG_DEBUG);
av_dump_format(oc, 0, oc->url, 1);
-
直接看 m3u8:
cat index.m3u8 | grep -A5 "#EXTINF"
九、什么时候该用 API,什么时候用命令?
| 场景 | 推荐 |
|---|---|
| 播放器本地录制 | API |
| 边缘节点切片 | API |
| 服务器转码 | 命令 |
| 快速验证 | 命令 |
金句:
命令行是 demo,API 才是产品。
十、总结
cpp
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/opt.h>
#include <libavutil/time.h>
}
#include <cstdio>
#include <cstdlib>
static AVFrame* alloc_yuv_frame(int w, int h, AVPixelFormat fmt)
{
AVFrame *f = av_frame_alloc();
f->format = fmt;
f->width = w;
f->height = h;
av_image_alloc(f->data, f->linesize, w, h, fmt, 32);
return f;
}
int main()
{
av_log_set_level(AV_LOG_INFO);
const int W = 1280, H = 720, FPS = 25;
const char *out_m3u8 = "out/index.m3u8";
avformat_alloc_output_context2(&oc, nullptr, "hls", out_m3u8);
if (!oc) return -1;
/* ===== HLS 核心选项 ===== */
AVDictionary *opts = nullptr;
av_dict_set(&opts, "hls_time", "4", 0); // 建议切片时长
av_dict_set(&opts, "hls_list_size", "5", 0); // 直播窗口
av_dict_set(&opts, "hls_flags", "delete_segments+omit_endlist", 0);
av_dict_set(&opts, "hls_segment_filename", "out/seg_%05d.ts", 0);
av_dict_set(&opts, "hls_segment_type", "mpegts", 0);
/* ===== 视频编码器 ===== */
const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
AVStream *st = avformat_new_stream(oc, codec);
AVCodecContext *cc = avcodec_alloc_context3(codec);
cc->codec_type = AVMEDIA_TYPE_VIDEO;
cc->width = W;
cc->height = H;
cc->framerate = {FPS, 1};
cc->time_base = {1, FPS};
cc->gop_size = FPS * 2; // ★ 关键:GOP=2s
cc->keyint_min = cc->gop_size;
cc->max_b_frames = 0;
cc->pix_fmt = AV_PIX_FMT_YUV420P;
av_opt_set(cc, "preset", "ultrafast", 0);
av_opt_set(cc, "tune", "zerolatency", 0);
avcodec_open2(cc, codec, nullptr);
avcodec_parameters_from_context(st->codecpar, cc);
st->time_base = cc->time_base;
/* ===== 打开 HLS 输出 ===== */
avformat_write_header(oc, &opts);
/* ===== 生成测试画面 ===== */
AVFrame *frame = alloc_yuv_frame(W, H, AV_PIX_FMT_YUV420P);
AVPacket *pkt = av_packet_alloc();
for (int i = 0; i < FPS * 20; i++) { // 录 20 秒
frame->pts = i;
// 简单灰阶动画
uint8_t v = (i * 10) % 255;
memset(frame->data[0], v, W * H);
memset(frame->data[1], 128, W/2 * H/2);
memset(frame->data[2], 128, W/2 * H/2);
avcodec_send_frame(cc, frame);
while (avcodec_receive_packet(cc, pkt) == 0) {
av_packet_rescale_ts(pkt, cc->time_base, st->time_base);
pkt->stream_index = st->index;
av_interleaved_write_frame(oc, pkt);
av_packet_unref(pkt);
}
av_usleep(1000000 / FPS);
}
/* ===== 收尾 ===== */
av_write_trailer(oc);
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&cc);
avformat_free_context(oc);
printf("HLS 生成完成:%s\n", out_m3u8);
return 0;
}
FFmpeg 6 的 HLS相关API并不复杂,复杂的是你以为"切片是时间问题",其实它是 GOP / keyframe / playlist 窗口 / CDN 语义 的组合拳。
用 API 写一次 HLS,你会突然明白:
为什么以前用命令行的直播,总是慢那么几秒。