把 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 秒实验
- 把
g从 25 改成 10,看文件大小——变小,但关键帧变多,首帧能更早到,这就是延迟与码率的取舍。 - 在服务器上把
Connection: close去掉,观察连接行为——HTTP/1.1 默认长连接,这正是 HTTP-FLV 能持续推的前提。 - 用 curl 拉同一条地址,看它什么时候结束——文件有长度,所以 curl 会等到下载完;真实直播里它会一直挂着。
常见误区
- 「HTTP-FLV 是另一种协议」——不是,就是 HTTP 传 FLV。
- 「HTTP-FLV 也能拖进度条」——不能,它没有总长度也没有索引。
- 「它比 HLS 全面更好」——规模一上来成本就压不住,各有取舍。
自测三问(点开对答案)
1. HTTP-FLV 为什么延迟低? —— **不切片、不等列表**,延迟只等于等一个关键帧的时间。 2. 它为什么不适合超大流量? —— **响应体持续增长,CDN 几乎无法缓存**,每个观众都要从源站拉。 3. 多码率怎么切? —— **只能断开重连**,所以切换时观众会看到一次黑屏。- ☐ (本节完,下一节 6.2.1 HTTPS 简介)