libav 一上来就带 Range: bytes=0-,它默认就想要能随机访问的连接。 服务器回 200 就一次给完,回 206 它就分段要——两次请求的区别在这。
8.3.1 讲的是请求格式,这一节讲「同一个文件,两种服务器,行为差在哪」。 结论比想象中微妙:两种都能播,差别在请求次数与能不能拖。
一、两种服务器,两种结果
把 48741 字节的 mp4 放两种服务器上,各拉一遍:
服务器 帧数 首帧 请求数
忽略 Range 50 8 毫秒 1 支持 Range 50 9 毫秒 2
帧数一样(都是 50),首帧耗时也只差一毫秒。 差别只有一个:请求数,一个 1 次、一个 2 次。
这个「1 与 2」不是随机波动,是协议行为决定的。 把请求明细摊开看就清楚了。
二、请求明细摊开看
两条链路收到的请求是这样的:
服务器 第几个 Range 头 回了什么
忽略 Range 1 bytes=0- 200 支持 Range 1 bytes=0- 206 支持 Range 2 bytes=48- 206
第一个发现:libav 一上来就发 Range: bytes=0-,不是裸 GET。
这一句的意思是「我从第 0 字节开始,能分段给就分段给」。
第二个发现:服务器回 200 之后,libav 就再也不问了。 因为 200 的含义是「整份都给你」,它把整个文件读进缓冲,一次就够。
第三个发现:服务器回 206 之后,它又发了第二个请求,从第 48 字节起。 这说明它认定「这个服务器能跳读」,于是换起点重新要剩下的部分。
第 48 字节这个数字是有讲究的:mp4 开头是 ftyp 盒子,长度正好 32 字节, 加上前面 8 字节的盒子头,第一个真正有内容的盒子从第 40 字节往后开始。 它跳过已经拿到的部分,从还没读的地方接着要。
三、为什么两种都能播
因为「能不能播」跟「怎么读」是两件事。 只要最后字节都到齐了,解码器都能解。
读法 优点 缺点
一次读完(200) 请求少、逻辑简单 不能拖,白读了后面的 分段读(206) 能拖、能省流量 请求多、服务器要配合
小文件看不出差别:48741 字节,一次读完也就几十毫秒。 文件大到几个 GB 时,差别就出来了——一次读完要等很久,分段读才能秒开。
所以「边下边播」的前提是服务器支持 Range。 很多网盘、CDN 都支持,但一些简单的手写服务器不支持, 表现就是「点开要转很久才开始播」。
四、直播型 HTTP 差在哪
文件型与直播型,四条本质区别:
项 文件型 直播型
长度 事先知道 不知道,边发边生成 连接 发完就关 一直开着 定位 能拖进度条 不能,只能往后 容器 mp4/mkv TS/FLV
为什么直播不用 mp4:它的索引(moov)要么在文件头写死,要么在文件尾收尾。 直播既没法写死(还没生成完),也没法等收尾。
TS 与 FLV 都是「一个个包自己带说明」,发到哪算到哪, 所以可以永远发下去。这就是 8.4 与 8.6 要用的容器。
五、动手 Demo:两种服务器对比
下面这段代码起两个服务器(一个忽略 Range、一个支持 Range), 各让 libav 拉一遍,记下帧数、首帧耗时与每一条请求的 Range 头。
import av, http.server, os, threading, time
MODE = [u"plain"]
LOG = []
class H(http.server.BaseHTTPRequestHandler):
protocol_version = u"HTTP/1.1"
def log_message(self, *a):
pass
def do_GET(self):
rng = self.headers.get(u"Range")
LOG.append((self.path, rng))
data = open(u"s7.mp4", u"rb").read()
if rng and MODE[0] == u"range":
a, b = rng.split(u"=")[1].split(u"-")
a = int(a or 0)
b = int(b) if b else len(data) - 1
body = data[a:b + 1]
self.send_response(206)
self.send_header(u"Content-Range",
u"bytes %d-%d/%d" % (a, b, len(data)))
else:
body = data
self.send_response(200)
self.send_header(u"Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def run(mode):
MODE[0] = mode
LOG.clear()
httpd = http.server.ThreadingHTTPServer(("127.0.0.1", 0), H)
port = httpd.server_address[1]
th = threading.Thread(target=httpd.serve_forever)
th.daemon = True
th.start()
t0 = time.time()
n = 0
c = av.open("http://127.0.0.1:%d/s7.mp4" % port,
options={"timeout": "6000000"})
for _f in c.decode(video=0):
n += 1
c.close()
ms = (time.time() - t0) * 1000
httpd.shutdown()
print(u"%s %d 帧 %.0f ms %d 个请求" % (mode, n, ms, len(LOG)))
for p, r in LOG:
print(u" %s Range=%s" % (p, r))
run(u"plain")
run(u"range")
实跑输出:
--------------------------------------------------------------------
8.3.2 HTTP 流媒体
--------------------------------------------------------------------
**① 同一个文件,两种服务器,分别拉一遍**
服务器 帧数 首帧 请求数
--------------------------------------------
忽略 Range 50 7 ms 1
支持 Range 50 23 ms 2
**两种都解出全部 50 帧,但请求数不一样:一个 1 次,一个 2 次。**
**把两次的请求明细摊开看,原因就清楚了。**
**② 客户端每次都要了 Range,区别在服务器怎么回**
服务器 第几个 Range 头 回了什么
----------------------------------------------------
忽略 Range 1 bytes=0- 200
支持 Range 1 bytes=0- 206
支持 Range 2 bytes=48- 206
**原来 libav 一上来就发 `Range: bytes=0-`,不是裸的 GET。**
**这是它默认的「我要能随机访问」的意思。**
**忽略 Range 的服务器直接回 200 加整份文件,libav 拿到 200 就明白了:**
**「你不支持分段」,于是整份读进缓冲,1 个请求就够。**
**支持 Range 的服务器回 206,libav 于是认定「这个服务器能跳着读」,**
**就会换个起点再要一次——上表第二个请求要的是 `bytes=48-`,从第 48 字节往后。**
**这就是「能不能拖进度条」的底层能力:一次请求只拿一段,不够再要。**
**③ 直播型 HTTP 与文件型 HTTP 的差别**
项 文件型 直播型
------------------------------------------------
长度 事先知道 不知道,边发边生成
连接 发完就关 一直开着
定位 能拖进度条 不能,只能从当下往后
容器 mp4/mkv TS/FLV(能随便切)
**为什么直播不用 mp4:mp4 的索引(moov)要么在最前面写死,**
**要么在最后面收尾——直播既没法写死也没法等收尾。**
**TS 与 FLV 都是「一个个包自己带说明」,发到哪算到哪。**
两次都是 50 帧,耗时也差不多(8 与 9 毫秒)。 所以「支持 Range 会更快」这个说法在小文件上不成立——它影响的是别的事。
请求明细才是重点:plain 只有 1 条,range 有 2 条。
而且第一条都带 bytes=0-——libav 默认就想分段读。
第二条的 Range 是 bytes=48-,从第 48 字节接着要。
这就是「分段读」的实现方式:拿到一段之后,从断点继续要下一段。
30 秒实验
- 把文件换成几 MB 的:看首帧耗时的差距会不会拉开。
- 让服务器回 206 但 Content-Range 写错:看 libav 会不会卡住。
- 故意让第二个请求回 404:看它能播多少帧。
常见误区
- 以为 libav 发的是裸 GET。它默认带
Range: bytes=0-。 - 以为支持 Range 一定更快。小文件上看不出差别。
- 以为直播也能用 mp4。索引放不下,只能换 TS/FLV。
自测三问(点开对答案)
1. libav 的第一条 HTTP 请求带什么头? —— `Range: bytes=0-`,意思是希望分段读。 2. 服务器回 200 与回 206 有什么后果? —— 回 200 它一次读完(1 个请求);回 206 它分段要(2 个请求)。 3. 为什么直播不用 mp4? —— 索引要么写死在前、要么收尾在后,直播两头都做不到。- ☐ (本节完,下一节 8.3.3 VLC 作为 HTTP 流媒体服务器)