基于 FFmpeg 实现 HTTP-FLV 内置推流服务器
用 C++ 自己实现一个 HTTP-FLV 推流服务器(无需 SRS/Nginx)

前言
项目中需要将本地视频实时推送到局域网内的浏览器播放,调研了常见方案后决定放弃外部依赖(SRS、Nginx-RTMP),直接在 C++ 程序内嵌一个 HTTP-FLV 服务器。
本文记录完整实现思路、核心代码及踩坑记录,环境:C++17、Qt 5.14.2、FFmpeg 4.x、Windows 11。
一、方案选型
| 协议 | 延迟 | 浏览器原生支持 | 实现复杂度 |
|---|---|---|---|
| HLS | 5–30s | 原生(iOS/macOS) | 低 |
| RTMP | 1–3s | 需 Flash(已死) | 高(需外部服务) |
| WebRTC | <1s | 原生 | 极高 |
| HTTP-FLV | 1–3s | flv.js(JS 库) | 中 |
HTTP-FLV 本质是一条永不关闭的 HTTP 响应,body 持续追加 FLV tag。浏览器端配合 flv.js 即可播放,无需插件,适合局域网场景。
二、整体架构
DemuxThread
├─ videoPacketQueue ──┐
└─ audioPacketQueue ──┤
▼
HttpFlvServer(QThread)
├─ processPackets()
│ └─ FFmpeg FLV muxer(avio_alloc_context)
│ └─ writeCallback() → broadcastData()
└─ QTcpServer(HTTP 监听)
├─ GET / → 重定向 /player.html
├─ GET /player.html → 内嵌播放器页面
├─ GET /flv.js → flv.min.js 文件
└─ GET /live.flv → FLV 流(chunked)
关键设计:FFmpeg 的 avio_alloc_context 允许自定义 IO,将 FLV muxer 的输出字节流通过回调函数直接转发给所有已连接的 HTTP 客户端。
三、核心实现
3.1 自定义 IO 上下文
FFmpeg 默认将 muxer 输出写入文件,通过 avio_alloc_context 可以重定向到任意目标:
bool HttpFlvServer::initMuxer() {
// 分配输出格式上下文
avformat_alloc_output_context2(&fmtCtx_, nullptr, "flv", nullptr);
// 4KB 写缓冲,writeCallback 是静态成员函数
const int BUF_SIZE = 4096;
uint8_t* buf = (uint8_t*)av_malloc(BUF_SIZE);
fmtCtx_->pb = avio_alloc_context(
buf, BUF_SIZE,
1, // write_flag
this, // opaque,传回 this 指针
nullptr, // read_packet(不需要)
&HttpFlvServer::writeCallback,
nullptr // seek(FLV 流式输出不支持 seek)
);
fmtCtx_->flags |= AVFMT_FLAG_CUSTOM_IO;
// 添加视频流、音频流,复制编解码参数
vStream_ = avformat_new_stream(fmtCtx_, nullptr);
avcodec_parameters_copy(vStream_->codecpar, vpar_);
vStream_->time_base = videoTb_;
aStream_ = avformat_new_stream(fmtCtx_, nullptr);
avcodec_parameters_copy(aStream_->codecpar, apar_);
aStream_->time_base = audioTb_;
// 写 FLV 文件头(触发 writeCallback)
AVDictionary* opts = nullptr;
av_dict_set(&opts, "flvflags", "no_duration_filesize", 0);
return avformat_write_header(fmtCtx_, &opts) >= 0;
}
3.2 写回调广播
int HttpFlvServer::writeCallback(void* opaque, const uint8_t* buf, int size) {
auto* self = static_cast<HttpFlvServer*>(opaque);
QByteArray data(reinterpret_cast<const char*>(buf), size);
// 冻结前:追加到 FLV 头缓冲(包含 sequence header)
if (!self->headerFrozen_)
self->flvHeader_.append(data);
// 关键帧后重置 GOP 缓冲
if (self->newGopStarting_) {
self->currentGopBytes_.clear();
self->newGopStarting_ = false;
}
self->currentGopBytes_.append(data);
self->broadcastData(data);
return size;
}
void HttpFlvServer::broadcastData(const QByteArray& data) {
for (QTcpSocket* sock : streamClients_) {
if (sock->isWritable())
sock->write(data);
}
}
3.3 数据包处理循环
processPackets() 在 run() 的事件循环中周期性调用(QTimer 10ms):
void HttpFlvServer::processPackets() {
// 处理视频包
AVPacket* vpkt = nullptr;
while (videoQueue_->tryPop(vpkt)) {
bool isKey = (vpkt->flags & AV_PKT_FLAG_KEY);
if (needsKeyframe_) {
if (!isKey) { av_packet_free(&vpkt); continue; }
// 首个关键帧:记录源 PTS,冻结 FLV 头
firstKeyframeSrcSec_ = vpkt->pts * av_q2d(videoTb_);
needsKeyframe_ = false;
headerFrozen_ = true;
// 通知 writeCallback 开始 GOP 缓冲
newGopStarting_ = true;
} else if (isKey) {
newGopStarting_ = true; // 新 GOP 开始,重置缓冲
}
// PTS 续接(处理 seek 后时间戳跳变)
int64_t pts = computeContinuousPts(vpkt, /*isVideo=*/true);
AVPacket out = *vpkt;
out.stream_index = vStream_->index;
out.pts = out.dts = pts;
av_interleaved_write_frame(fmtCtx_, &out);
av_packet_free(&vpkt);
}
// 处理音频包(等待首帧视频关键帧后才开始输出)
AVPacket* apkt = nullptr;
while (!needsKeyframe_ && audioQueue_->tryPop(apkt)) {
int64_t pts = computeContinuousPts(apkt, /*isVideo=*/false);
AVPacket out = *apkt;
out.stream_index = aStream_->index;
out.pts = out.dts = pts;
av_interleaved_write_frame(fmtCtx_, &out);
av_packet_free(&apkt);
}
}
3.4 新客户端接入(延迟接入优化)
新客户端连接时直接发原始 FLV 流会有两个问题:
- 浏览器收到 FLV 需要先有 sequence header(AVC/AAC 解码配置)
- 如果只发 sequence header 没有关键帧,播放器需等到下一个关键帧才能起播,可能有数秒黑屏
解决方案:维护两段缓冲
// 新客户端接入时发送:
// 1. codecConfigHeader_:FLV头 + metadata + AVC序列头 + AAC序列头(无帧数据)
// 2. currentGopBytes_:最近一个关键帧起至今的帧数据
void HttpFlvServer::startStreaming(QTcpSocket* socket) {
// HTTP 响应头
socket->write(
"HTTP/1.1 200 OK\r\n"
"Content-Type: video/x-flv\r\n"
"Transfer-Encoding: chunked\r\n"
"Connection: keep-alive\r\n"
"Cache-Control: no-cache\r\n\r\n"
);
// 发送编解码配置 + 当前 GOP
if (!codecConfigHeader_.isEmpty())
socket->write(codecConfigHeader_);
if (!currentGopBytes_.isEmpty())
socket->write(currentGopBytes_);
streamClients_.append(socket);
}
codecConfigHeader_ 和 currentGopBytes_ 的提取方式:解析 flvHeader_ 中的 FLV tag,按 tag type 分类(0x08=音频,0x09=视频),序列头 tag 的 AVCPacketType 字节为 0。
四、HTTP 服务层
run() 使用 Qt 事件循环:
void HttpFlvServer::run() {
QTcpServer server;
server.listen(QHostAddress::Any, port_);
connect(&server, &QTcpServer::newConnection, [&]() {
QTcpSocket* sock = server.nextPendingConnection();
connect(sock, &QTcpSocket::readyRead, [=]() {
QByteArray req = sock->readAll();
handleRequest(sock, req);
});
});
QTimer timer;
connect(&timer, &QTimer::timeout, this, &HttpFlvServer::processPackets);
timer.start(10);
// 尝试添加 Windows 防火墙入站规则
addFirewallRule();
exec(); // Qt 事件循环
// 退出后写 FLV trailer
if (fmtCtx_) av_write_trailer(fmtCtx_);
cleanupMuxer();
}
五、前端播放器
服务器内嵌一段 HTML,由 buildPlayerHtml() 在运行时生成:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="/flv.js"></script>
</head>
<body style="background:#000;margin:0">
<video id="v" muted style="width:100vw;height:100vh" controls></video>
<button onclick="document.getElementById('v').muted=false"
style="position:absolute;top:10px;right:10px;z-index:9">
开启声音
</button>
<script>
if (flvjs.isSupported()) {
var player = flvjs.createPlayer({ type:'flv', url:'/live.flv' });
player.attachMediaElement(document.getElementById('v'));
player.load();
player.play();
}
</script>
</body>
</html>
▎ 注意:移动端浏览器自动播放策略限制,video 必须带 muted 才能自动播放,页面上额外提供"开启声音"按钮。
六、踩坑记录
坑 1:FLV 容器只支持 H.264 + AAC
在 init() 时需要提前检查编解码器,否则 avformat_write_header 会失败但错误信息极不明显:
bool HttpFlvServer::init(quint16 port,
AVCodecParameters* vpar, AVRational vtb,
AVCodecParameters* apar, AVRational atb) {
if (vpar && vpar->codec_id != AV_CODEC_ID_H264) {
qWarning() << "HttpFlvServer: codec"
<< avcodec_get_name(vpar->codec_id)
<< "not supported by FLV (H.264 only)";
return false;
}
// ...
}
上层 StreamController 据此给出明确提示:
QString reason = (vpar && vpar->codec_id != AV_CODEC_ID_H264)
? QString("视频编码 %1 不支持 FLV,请使用 H.264 编码的视频")
.arg(avcodec_get_name(vpar->codec_id))
: QString("端口 %1 初始化失败").arg(dest.port);
emit errorOccurred("HTTP-FLV 推流失败:" + reason);
坑 2:initMuxer 失败后队列需要 abort
若 initMuxer() 失败但 run() 直接返回,生产者线程(DemuxThread)的 tryPush 会一直阻塞:
if (!initMuxer()) {
if (videoQueue_) videoQueue_->abort();
if (audioQueue_) audioQueue_->abort();
emit errorOccurred("FLV muxer 初始化失败");
return;
}
坑 3:seek 后音视频时间戳需要续接
seek 会使 PTS 产生跳变,需要在 processPackets 中做时间戳重映射。以视频为例:
int64_t HttpFlvServer::computeContinuousPts(AVPacket* pkt, bool isVideo) {
int64_t& segBase = isVideo ? videoSegBase_ : audioSegBase_;
int64_t& accumPts = isVideo ? videoAccumPts_ : audioAccumPts_;
int64_t& lastOut = isVideo ? videoLastOut_ : audioLastOut_;
AVRational tb = isVideo ? videoTb_ : audioTb_;
if (segBase == AV_NOPTS_VALUE || pkt->pts < segBase) {
// 新段:将上次输出 PTS 作为续接基准
segBase = pkt->pts;
accumPts = lastOut + 1;
}
int64_t out = accumPts + (pkt->pts - segBase);
lastOut = out;
return out;
}
坑 4:首帧音频对齐
seek 后视频关键帧可能比音频目标早几帧,若不对齐会导致声音超前或滞后。做法:记录首个关键帧的源 PTS,首帧音频到来时将音频的 accumPts 对齐至该时间点:
// 首帧音频:对齐至首个视频关键帧的时间位置
if (firstKeyframeSrcSec_ >= 0.0) {
int64_t alignPts = (int64_t)(firstKeyframeSrcSec_ / av_q2d(audioTb_));
audioAccumPts_ = alignPts;
audioSegBase_ = apkt->pts;
firstKeyframeSrcSec_ = -1.0; // 只对齐一次
}
坑 5:Windows 防火墙自动开放端口
局域网其他设备无法访问时,需要添加入站规则。程序启动时自动尝试(需管理员权限),失败时发出信号提示用户:
void HttpFlvServer::addFirewallRule() {
QString cmd = QString(
"netsh advfirewall firewall add rule name=\"RambosPlayer HTTP-FLV\" "
"protocol=TCP dir=in localport=%1 action=allow"
).arg(port_);
int ret = QProcess::execute("netsh", cmd.split(' '));
if (ret != 0)
emit firewallHint(cmd); // 通知用户以管理员身份手动执行
}
七、效果
- 同局域网浏览器打开 http://局域网IP:8080/player.html 即可观看
- 延迟约 1–2 秒(取决于 GOP 大小,默认 2s 一个关键帧)
- 支持多客户端同时连接(广播模式)
- 客户端任意时刻接入均能快速起播(GOP 缓冲)
八、总结
| 核心技术点 | 实现方式 |
|---|---|
| FLV 流式输出 | avio_alloc_context + 自定义 writeCallback |
| HTTP 广播 | QTcpServer + QTcpSocket::write |
| 延迟接入优化 | codecConfigHeader_ + currentGopBytes_ |
| 音视频同步 | needsKeyframe_ 门控 + firstKeyframeSrcSec_ 对齐 |
| seek 时间戳续接 | 分段基准 + 累计 PTS 重映射 |
| 移动端播放 | muted 属性 + 用户点击解除静音 |
完整代码参见:src/httpflvserver.h / src/httpflvserver.cpp
github:https://github.com/johnjiamzhong-project/RambosPlayer
环境:C++17 / Qt 5.14.2 / FFmpeg 4.x / Windows 11

浙公网安备 33010602011771号