ZLMediaKit 源码分析(七):HTTP-FLV 低延迟直播流分析

本文深入分析 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 实现要点:

  1. FlvMuxer 统一封装:Frame → FLV Tag,与传输层解耦
  2. HTTP Chunked 传输:无需 Content-Length,数据即产即发
  3. TCP_NODELAY:禁用 Nagle,减少小包延迟
  4. GOP 缓存:新连接从最近关键帧开始,快速起播
  5. WebSocket-FLV:复用 FlvMuxer,换 WebSocket 传输层即可穿透防火墙

下一篇:WebRTC 推拉流与 SFU 转发分析

相关推荐
北京耐用通信2 小时前
耐达讯自动化PROFIBUS光纤模块:工业通信的“光电翻译官”
人工智能·科技·网络协议·自动化·信息与通信
Zzzzmo_2 小时前
【网络原理】TCP/IP协议01
网络·tcp/ip
dxxt_yy2 小时前
鼎讯信通 TY-30H 光纤熔接机:铁路通信施工设备科普
网络
c++之路3 小时前
迭代器模式(Iterator Pattern)
网络协议·rpc·迭代器模式
星恒讯工业路由器3 小时前
4G自组网与VPDN专网技术解析
网络·物联网·信息与通信·4g自组网·vpdn专网
淼淼爱喝水3 小时前
DVWA靶场命令注入漏洞检测实验
网络·安全·靶场·dvwa
KaMeidebaby4 小时前
卡梅德生物技术快报|免疫共沉淀 - Co-IP 实验在转录因子 ATF3/Smad4 蛋白互作研究中的应用实例解析
网络·人工智能·网络协议·tcp/ip·其他·算法·新浪微博
林熙蕾LXL4 小时前
IPC使用套接字进程通讯
网络
宋浮檀s4 小时前
应急响应——Web高危漏洞应急(SQL注入+XSS跨站+文件上传)
前端·网络·安全·web安全·xss