本文深入分析 HTTP-FLV 的请求处理、FLV Tag 封装、HTTP 分块传输和低延迟优化策略,追踪从 RingBuffer 读取到 Socket 发送的完整链路。
1. HTTP-FLV 在 ZLMediaKit 中的位置
HTTP-FLV 是将 FLV 容器格式的音视频数据通过 HTTP 协议实时推送给客户端的方案,浏览器端可由 flv.js 等播放器直接播放。与 RTMP 相比,无需 Flash 插件;与 HLS 相比,延迟更低。
推流端 → RTMP/RTSP → ZLMediaKit → HTTP-FLV → 浏览器(flv.js)
│
└── FLV Muxer 将 Frame 封装为 FLV Tag
通过 HTTP chunked 传输编码推送
2. FlvMuxer --- FLV 封装器
2.1 FLV 文件格式
FLV Header (9 bytes)
┌─────────────────────────────────┐
│ 'F' 'L' 'V' │ version │ flags │
│ (3B) │ (1B) │ (1B) │
└─────────────────────────────────┘
│ Data Offset (4B) │
FLV Body: 重复的 Tag
┌─────────────────────────────────┐
│ Tag Header (11 bytes) │
│ TagType(1B) + DataSize(3B) │
│ Timestamp(3B) + TimestampExt │
│ StreamID(3B) │
├─────────────────────────────────┤
│ Tag Data (DataSize bytes) │
├─────────────────────────────────┤
│ Previous Tag Size (4 bytes) │
└─────────────────────────────────┘
2.2 FlvMuxer 类
文件:src/Http/FlvMuxer.h
cpp
class FlvMuxer {
public:
using Ptr = std::shared_ptr<FlvMuxer>;
FlvMuxer(const TitleSdp::Ptr &title = nullptr);
// 输入 Frame → 封装为 FLV Tag → 回调输出
void inputFrame(const Frame::Ptr &frame);
// 输入 Track → 生成 FLV Header + MetaData
void addTrack(const Track::Ptr &track);
// 设置输出回调
void setOnFlvData(const function<void(const Buffer::Ptr &buf,
bool is_key)> &cb);
private:
function<void(const Buffer::Ptr &, bool)> _on_flv_data;
bool _has_video = false;
bool _has_audio = false;
bool _header_sent = false;
};
2.3 FLV Header 生成
cpp
void FlvMuxer::sendFlvHeader() {
// FLV Header: 'FLV' + version(1) + flags(5=AV) + header_size(9)
char header[9];
header[0] = 'F';
header[1] = 'L';
header[2] = 'V';
header[3] = 1; // version
header[4] = (_has_video ? 1 : 0) | (_has_audio ? 4 : 0);
// bit 0: has video bit 2: has audio
header[5] = 0; header[6] = 0;
header[7] = 0; header[8] = 9; // header size = 9
auto buf = std::make_shared<BufferRaw>(header, 9);
_on_flv_data(buf, false);
// Previous Tag Size 0
char prev_tag_size[4] = {0, 0, 0, 0};
auto prev_buf = std::make_shared<BufferRaw>(prev_tag_size, 4);
_on_flv_data(prev_buf, false);
_header_sent = true;
}
2.4 FLV Tag 封装
cpp
void FlvMuxer::inputFrame(const Frame::Ptr &frame) {
if (!_header_sent) {
sendFlvHeader();
}
auto codec_id = frame->getCodecId();
if (codec_id == CodecH264 || codec_id == CodecH265) {
writeVideoTag(frame);
} else if (codec_id == CodecAAC || codec_id == CodecG711A ||
codec_id == CodecG711U || codec_id == CodecOpus) {
writeAudioTag(frame);
}
}
2.5 Video Tag 写入
cpp
void FlvMuxer::writeVideoTag(const Frame::Ptr &frame) {
bool is_key = frame->keyFrame();
bool is_config = frame->configFrame(); // SPS/PPS
// FLV Video Tag Data:
// [1B: frameType(4) + codecId(4)] + [1B: AVCPacketType] +
// [3B: CompositionTime] + [payload]
BufferLikeString buffer;
// Byte 1: Frame Type + Codec ID
// Frame Type: 1=key, 2=inter
// Codec ID: 7=AVC(H264), 12=HEVC(H265)
uint8_t flags = (is_key ? 0x10 : 0x20);
if (frame->getCodecId() == CodecH264) {
flags |= 7; // AVC
} else {
flags |= 12; // HEVC
}
buffer.push_back(flags);
// Byte 2: AVC Packet Type
// 0 = AVC Sequence Header (SPS+PPS)
// 1 = AVC NALU
// 2 = AVC End of Sequence
buffer.push_back(is_config ? 0 : 1);
// Byte 3-5: Composition Time (CTS = PTS - DTS)
int32_t cts = frame->_pts - frame->_dts;
buffer.push_back((cts >> 16) & 0xFF);
buffer.push_back((cts >> 8) & 0xFF);
buffer.push_back(cts & 0xFF);
// Payload
if (frame->getCodecId() == CodecH264 && is_config) {
// AVC Sequence Header: AVCDecoderConfigurationRecord
buffer.append(frame->data() + frame->prefixSize(),
frame->size() - frame->prefixSize());
} else {
// NALU data(去掉 start code,加上 4 字节长度前缀)
auto nalu = frame->data() + frame->prefixSize();
auto nalu_len = frame->size() - frame->prefixSize();
uint32_t be_len = htonl(nalu_len);
buffer.append((char *)&be_len, 4);
buffer.append(nalu, nalu_len);
}
// 封装为完整 FLV Tag
writeTag(8, buffer, frame->_dts, is_key); // 8 = video tag type
}
2.6 writeTag --- Tag 封装
cpp
void FlvMuxer::writeTag(uint8_t type, const BufferLikeString &data,
uint64_t dts, bool is_key) {
// Tag Header (11 bytes)
BufferLikeString tag;
tag.reserve(11 + data.size() + 4);
// Tag Type
tag.push_back(type);
// Data Size (3 bytes big-endian)
uint32_t data_size = data.size();
tag.push_back((data_size >> 16) & 0xFF);
tag.push_back((data_size >> 8) & 0xFF);
tag.push_back(data_size & 0xFF);
// Timestamp (3 bytes) + Timestamp Extended (1 byte)
uint32_t ts = dts;
tag.push_back((ts >> 16) & 0xFF);
tag.push_back((ts >> 8) & 0xFF);
tag.push_back(ts & 0xFF);
tag.push_back((ts >> 24) & 0xFF); // extended
// Stream ID (3 bytes, always 0)
tag.push_back(0); tag.push_back(0); tag.push_back(0);
// Tag Data
tag.append(data);
// Previous Tag Size (4 bytes)
uint32_t prev_size = 11 + data.size();
uint32_t be_prev = htonl(prev_size);
tag.append((char *)&be_prev, 4);
// 输出
auto buf = std::make_shared<BufferLikeString>(std::move(tag));
_on_flv_data(buf, is_key);
}
3. HttpSession --- HTTP 请求处理
3.1 请求分发
cpp
void HttpSession::onRecv(const Buffer::Ptr &buf) {
// 解析 HTTP 请求
HttpRequestSplitter::onRecv(buf);
}
void HttpSession::onRecvHeader(const char *data, size_t len) {
_parser.parse(data, len);
auto url = _parser.url();
if (url.find(".flv") != string::npos ||
url.find("/live/") != string::npos) {
// HTTP-FLV 请求
handleFlvRequest();
} else if (url.find(".ts") != string::npos) {
// HLS TS 切片请求
handleTsRequest();
} else {
// 普通 HTTP 请求
handleHttpRequest();
}
}
3.2 handleFlvRequest --- FLV 流处理
cpp
void HttpSession::handleFlvRequest() {
// 1. 从 URL 中提取 vhost/app/stream_id
// 例: /live/test.flv → app=live, stream_id=test
auto media_info = parseFlvUrl(_parser.url());
// 2. 查找 MediaSource
auto src = MediaSource::find(Rtmp::kType,
media_info._vhost, media_info._app, media_info._stream_id);
if (!src) {
sendNotFound();
return;
}
// 3. 发送 HTTP 响应头
// HTTP/1.1 200 OK
// Content-Type: video/x-flv
// Transfer-Encoding: chunked ← 分块传输
// Cache-Control: no-cache
sendResponse(200, {
{"Content-Type", "video/x-flv"},
{"Transfer-Encoding", "chunked"},
{"Cache-Control", "no-cache"},
{"Connection", "keep-alive"}
});
// 4. 创建 FlvMuxer
_flv_muxer = std::make_shared<FlvMuxer>();
// 5. 设置 FLV 数据输出回调
_flv_muxer->setOnFlvData([this](const Buffer::Ptr &buf,
bool is_key) {
sendFlvChunk(buf);
});
// 6. 添加 Track(触发 MetaData + Sequence Header 输出)
for (auto &track : src->getTracks()) {
_flv_muxer->addTrack(track);
}
// 7. 注册为 MediaSource Reader
_ring_reader = src->createReader();
_ring_reader->setReadCB([this](const Frame::Ptr &frame) {
_flv_muxer->inputFrame(frame);
});
}
3.3 sendFlvChunk --- HTTP 分块发送
cpp
void HttpSession::sendFlvChunk(const Buffer::Ptr &buf) {
// HTTP Chunked Transfer Encoding 格式:
// [chunk-size in hex]\r\n
// [chunk-data]\r\n
auto size_str = toHex(buf->size()) + "\r\n";
auto end_str = "\r\n";
// 合并为一次 send
BufferLikeString combined;
combined.append(size_str);
combined.append(buf->data(), buf->size());
combined.append(end_str);
_sock->send(std::make_shared<BufferLikeString>(std::move(combined)));
}
调用链:
RingBuffer::write() [推流端写入]
→ RingReaderDispatcher::onRead()
→ Reader 回调 → FlvMuxer::inputFrame()
→ writeVideoTag/writeAudioTag()
→ writeTag() --- 封装 FLV Tag
→ _on_flv_data(buf, is_key)
→ HttpSession::sendFlvChunk(buf)
→ _sock->send(combined)
→ ::send(fd, data, len, MSG_NOSIGNAL) 系统调用
4. 低延迟优化
4.1 HTTP Chunked vs 固定 Content-Length
HTTP-FLV 使用 Chunked Transfer Encoding,优势:
- 无需预先知道 Content-Length(直播流无限长)
- 数据产生即发送,无需缓冲整块数据
- 每个关键帧后可 flush,减少延迟
4.2 TCP_NODELAY
cpp
//TcpServer 创建 Socket 时设置
int opt = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
禁用 Nagle 算法,小数据包立即发送,关键在流媒体场景下减少 40ms 延迟。
4.3 SO_SNDBUF 调整
cpp
// 增大发送缓冲区
int sndbuf = 512 * 1024; // 512KB
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
4.4 关键帧对齐
新客户端连接时,从最近的关键帧开始发送:
cpp
// RingBuffer 创建 Reader 时,可选是否等待关键帧
_ring_reader = src->createReader(true); // true = 等待关键帧
RingBuffer 内部缓存了最近的 GOP(Group of Pictures),新 Reader 连接时从最近的 I 帧开始,确保客户端快速解码。
4.5 GOP 缓存
cpp
class RingBuffer {
// 默认缓存最近的 GOP 数据
static const int kDefaultRingSize = 1024;
RingStorage<Frame::Ptr> _storage; // list<Frame>
// 写入时,关键帧标识 GOP 起点
// 新 Reader 从最近的 GOP 起点读取
};
5. WebSocket-FLV
ZLMediaKit 还支持 WebSocket-FLV,与 HTTP-FLV 共用 FlvMuxer,只是传输层不同:
cpp
void HttpSession::handleWsFlvRequest() {
// WebSocket 握手
if (!handleWebSocketHandshake()) return;
// 创建 FlvMuxer(与 HTTP-FLV 相同)
_flv_muxer = std::make_shared<FlvMuxer>();
_flv_muxer->setOnFlvData([this](const Buffer::Ptr &buf,
bool is_key) {
// 通过 WebSocket 帧发送
sendWebSocketFrame(buf, WS_BINARY);
});
// ... 同 HTTP-FLV 注册 Reader
}
WebSocket 帧发送:
cpp
void HttpSession::sendWebSocketFrame(const Buffer::Ptr &buf,
uint8_t opcode) {
// WebSocket 帧格式:
// [FIN(1) + RSV(3) + opcode(4)]
// [MASK(1) + payload_len(7/16/64)]
// [payload data]
BufferLikeString frame;
frame.push_back(0x80 | opcode); // FIN=1
// ... payload length encoding
frame.append(buf->data(), buf->size());
_sock->send(std::make_shared<BufferLikeString>(std::move(frame)));
}
6. 完整数据流对比
HTTP-FLV 延迟分析
推流端编码 (0ms)
→ 网络传输 (50-100ms)
→ ZLMediaKit 接收 → Frame → RingBuffer::write() (0ms)
→ FlvMuxer::inputFrame() → FLV Tag 封装 (0.1ms)
→ HTTP Chunked 发送 → Socket::send() (0.1ms)
→ 网络传输 (50-100ms)
→ 浏览器 flv.js 解码渲染 (50ms)
总延迟: ~150-250ms (理想网络)
对比 RTMP:+100-200ms(TCP 聚合 + Nagle)
对比 HLS:+5-30s(TS 切片 + m3u8 轮询)
7. 小结
ZLMediaKit 的 HTTP-FLV 实现要点:
- FlvMuxer 统一封装:Frame → FLV Tag,与传输层解耦
- HTTP Chunked 传输:无需 Content-Length,数据即产即发
- TCP_NODELAY:禁用 Nagle,减少小包延迟
- GOP 缓存:新连接从最近关键帧开始,快速起播
- WebSocket-FLV:复用 FlvMuxer,换 WebSocket 传输层即可穿透防火墙