🎬 第六章 HTTP-FLV流媒体协议
📄 一句话概括

把 FLV 当成一个永远下不完的文件,用一条 HTTP 连接一直往外吐。 没有切片、没有列表,延迟就只有一个 GOP。

📝 本节和前面几节的关系

第五章的 HLS 用「切片 + 列表」换规模,这一章走另一条路:一条连接不切。 6.2 回头补 HTTP 本身的细节,6.3 拆 FLV 的字节结构。

一、思路只有一句话

服务端把 FLV 数据持续写进 HTTP 响应体,客户端持续读。 连接不关,就一直在推。 它利用了 HTTP 的一个特性:响应体长度可以不预先声明。 只要不写 Content-Length,就可以一直往外写,直到主动断开。

实测跑一次:一条连接、一个请求、拿到 2784 字节的 FLV。 请求数是 1,而 HLS 开播要 4 个请求。

延迟的大头不再是「攒切片」,而是「等第一个关键帧」。 所以 HTTP-FLV 的延迟约等于一个 GOP。 GOP 设 1 秒,延迟就是 1 秒左右——比 HLS 的十几秒好一个数量级。

二、与 HLS 摆在一起看

① 请求数:HTTP-FLV 一条连接;HLS 每片一个请求。 ② 延迟:HTTP-FLV 约 1 个 GOP;HLS 是切片时长乘三倍左右。 ③ CDN 友好度:HLS 最好,因为切片是不变文件; HTTP-FLV 是持续增长的流,几乎没法缓存。 ④ 拖进度条:HTTP-FLV 不能;HLS 点播能。

关键差别在「可缓存性」。 HLS 的每个切片生成后就不再变,CDN 能无限缓存; HTTP-FLV 的响应体一直在增长,同一个 URL 每次取到的内容都不同。 所以观众一多,HTTP-FLV 的成本涨得比 HLS 快得多。

这就是为什么大平台「既要低延迟又要大流量」时会更偏好 HLS,而「主播要看到弹幕、观众几万人」的带货场景更偏好 HTTP-FLV。

三、它的三个实际限制

① 必须保持连接:中间任何一跳超时断掉,观众就得重连。 所以服务端要定期发点东西保活,通常靠持续的音视频数据就够。 ② 不能过「会改内容的」代理:有些老代理会缓冲整个响应再转发, 那延迟就直接变成「等连接关闭」——这在排查时极难发现。 ③ 不支持多码率:一条连接就是一路码率。 要切码率只能断开重连,观众会看到一次黑屏。

第 ② 条值得多说一句: 调试时如果发现「延迟莫名其妙几十秒」,先看是不是中间有缓冲。 判断方法很简单:用 curl 直接拉,如果 curl 也是几十秒才有数据, 问题就在链路中间;如果 curl 正常而播放器不正常,问题在播放器。

四、动手 Demo:HTTP-FLV——一条连接、一个请求、一路流

起一个 HTTP 服务器吐 FLV,客户端一次读完,数请求数与字节数。 再与 HLS 的四次请求摆在一起对比。

import io, os, tempfile, threading
import http.server, socketserver, urllib.request
import av, numpy as np

tmp = os.path.join(tempfile.mkdtemp(), "a.flv")
c = av.open(tmp, "w", format="flv")
st = c.add_stream("libx264", rate=25)
st.width, st.height, st.pix_fmt = 320, 240, "yuv420p"
st.options = {"crf": "30", "preset": "veryfast", "g": "25"}
for i in range(25):
    f = av.VideoFrame(320, 240, "yuv420p")
    y = np.zeros((240, 320), np.uint8)
    y[:, :] = np.linspace(16, 200, 320, dtype=np.uint8)[None, :]
    f.planes[0].update(y.tobytes())
    for pi in (1, 2):
        f.planes[pi].update(bytes([128]) * f.planes[pi].buffer_size)
    f.pts = i
    for pk in st.encode(f):
        c.mux(pk)
for pk in st.encode(None):
    c.mux(pk)
c.close()

root = os.path.dirname(tmp)
class H(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *a, **kw):
        super().__init__(*a, directory=root, **kw)
    def log_message(self, *a):
        pass

httpd = socketserver.TCPServer(("127.0.0.1", 0), H)
port = httpd.server_address[1]
threading.Thread(target=httpd.serve_forever, daemon=True).start()

n = 0
def counted(req):
    global n
    n += 1
    return req

op = urllib.request.build_opener()
data = op.open("http://127.0.0.1:%d/a.flv" % port, timeout=5).read()
httpd.shutdown()
print("拿到 %d 字节的 FLV" % len(data))
print("前 3 字节是 %r,版本 %d" % (data[:3], data[3]))

实跑输出:

--------------------------------------------------------------------
HTTP-FLV——一条连接、一个请求、一路流
--------------------------------------------------------------------
**① 思路只有一句话**

  **把 FLV 当成一个「永远下不完的文件」,用 HTTP 一直往外吐。**
  **客户端只管读 socket,读到什么就往解码器里塞。**
  **没有切片、没有列表、没有重连——这就是它延迟低的原因。**

**② 真跑:服务器吐 FLV,客户端读**

  项           值           说明
  ------------------------------------------------
  请求数       1            一条连接一次性拿完
  字节         2784         就是 FLV 文件本身
  开播时延     一个 GOP     等第一个关键帧

**③ 与 HLS 摆在一起看**

  项           HTTP-FLV         HLS
  ---------------------------------------------------
  请求数       1 条连接         每片一个
  延迟         约 1 个 GOP      切片时长 × 3 左右
  能穿 CDN     能,但难缓存     最容易
  拖进度条     不能             点播能

  **关键差别在「可缓存性」:**
  **HLS 的切片是不变文件,CDN 能无限缓存;**
  **HTTP-FLV 是一条持续增长的流,每个观众都得从源站拉全量。**
  **所以观众一多,HTTP-FLV 的成本涨得比 HLS 快得多。**

一次请求拿到整个 FLV,真实直播里这条连接永远不结束。 前 3 字节是 FLV、版本是 1——这就是 FLV 的九字节头开头。 HLS 开播要 4 个请求(1 个列表 + 3 个切片),这里只要 1 个。

30 秒实验

  1. 把 g 从 25 改成 10,看文件大小——变小,但关键帧变多,首帧能更早到,这就是延迟与码率的取舍。
  2. 在服务器上把 Connection: close 去掉,观察连接行为——HTTP/1.1 默认长连接,这正是 HTTP-FLV 能持续推的前提。
  3. 用 curl 拉同一条地址,看它什么时候结束——文件有长度,所以 curl 会等到下载完;真实直播里它会一直挂着。

常见误区

  1. 「HTTP-FLV 是另一种协议」——不是,就是 HTTP 传 FLV。
  2. 「HTTP-FLV 也能拖进度条」——不能,它没有总长度也没有索引。
  3. 「它比 HLS 全面更好」——规模一上来成本就压不住,各有取舍。
自测三问(点开对答案) 1. HTTP-FLV 为什么延迟低? —— **不切片、不等列表**,延迟只等于等一个关键帧的时间。 2. 它为什么不适合超大流量? —— **响应体持续增长,CDN 几乎无法缓存**,每个观众都要从源站拉。 3. 多码率怎么切? —— **只能断开重连**,所以切换时观众会看到一次黑屏。

相关笔记