从文件换到网络,代码一个字都不用改,代价全在定位上。 本地 seek 是改指针,网络 seek 是再跑一趟。
6.2 讲本地,这一节把同一个文件挂到本机 HTTP 上再开一次。 下一篇 6.4 换成自己造 AVIO,连「从哪读」都由你定。
一、换成网址,代码不动
这是最能体现分层价值的一次对比。 环节 本地文件 网络流
打开方式 给路径 给网址 AVIO 外壳 一样 一样 协议实现 file http 上层代码 不变 不变
变的只有最底下那层。 解封装、解码、滤镜,一行都不用动。 这也是本课反复强调的那句话的具体版本:换协议不换链。 实践意义很直接:先在本地把逻辑跑通,再换网址,出问题就只可能是网络那一层。
二、网络上一次定位就是一次新请求
HTTP 没有「指针」这个概念,只有「要哪一段」。 动作 本地 HTTP
读下一块 改指针 在同一连接里继续读 跳到第 N 字节 改指针 发一条 Range 请求 量文件大小 跳末尾 看 Content-Range 的总长
demo 里那个 46 KB 的文件被发了 92226 字节,正好两倍。原因就是定位之后重新要了一次。 文件越大、跳得越多,这个浪费越明显。 这就是为什么直播流干脆不支持随意跳——不是做不到, 是不划算。 点播能拖进度条,是因为服务器愿意配合:每次拖动都是一条新 Range,浏览器与 CDN 都扛得住。
三、直播流与点播流的区别
同样是网址,能不能 seek 差很远。 点播(mp4) 直播(HLS/RTP)
总时长 有 没有 能不能拖 能 只能拖回一小段窗口 数据形态 一个文件 一串不断生成的分片 本机可验 能 没有源,验不了
直播没有「总时长」这个概念,因为它还没结束。所以 AVIO 没法量大小,上层也拿不到 duration。 这就是为什么直播流的容器要特别设计:FLV 与 TS 都是「边生成边发」,不靠文件头描述全局。 HLS 是折中方案:切成一个个小文件,每个都能 seek,拼起来像一个流。 代价是延迟,几秒到几十秒。
四、本机能验证到哪一层
这一节用本机 HTTP 服务当对端,所以是真跑,不是模拟。 测到了 没测到
HTTP 拉流、解出全部帧 真正的直播源 Range 请求怎么发 断线重连 端口不通报什么 码率自适应
这个边界要说清楚:拉一个 mp4 与拉一路直播,在 AVIO 这一层是同一套代码,但对端完全不同。 没有直播源就验不了直播,这是硬限制,不是偷懒。 把能验的验到位,把验不了的标出来——第十三章也照这个规矩。
五、端口不通报什么,值得单独记
拿一个没人监听的端口试,报的是 HTTPServerError。 注意不是 ProtocolNotFoundError:协议在、连接也建了,只是对面没服务。 这跟 Demo 38 那套判法完全一致。 生产环境里这个区分很实用:报协议不在,是构建问题,重装 FFmpeg; 报连不上,是网络或服务问题,去查对端。报错类型直接把排查方向指出来了。
六、动手 Demo:把本地文件挂成本机 HTTP,真跑一次网络流
自己起一个支持 Range 的 HTTP 服务,再拿 av.open 去拉它。
import av, http.server, socketserver, threading
DATA = open("tiny.mp4", "rb").read()
log = []
class R(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, *a):
pass
def do_GET(self):
rng = self.headers.get("Range") or "bytes=0-"
a, _, b = rng[6:].partition("-")
start = int(a) if a else 0
end = int(b) if b else len(DATA) - 1
chunk = DATA[start:end + 1]
self.send_response(206)
self.send_header("Content-Range",
"bytes %d-%d/%d" % (start, end, len(DATA)))
self.send_header("Content-Length", str(len(chunk)))
self.end_headers()
self.wfile.write(chunk)
log.append((rng, len(chunk)))
srv = socketserver.ThreadingTCPServer(("127.0.0.1", 0), R)
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = "http://127.0.0.1:%d/tiny.mp4" % srv.server_address[1]
c = av.open(url, timeout=5)
print(c.format.name, len(c.streams))
c.close()
srv.shutdown()
print(log)
实跑输出:
--------------------------------------------------------------------
把本地文件挂成本机 HTTP,真跑一次网络流
--------------------------------------------------------------------
**① 起一个支持 Range 的本机 HTTP 服务,地址随机取端口**
这次起在 **51058** 端口,地址是 `http://127.0.0.1:51058/tiny.mp4`,
背后就是刚才那个 46137 字节的 mp4。**换一次就跑一个别的端口,所以这个数不用记。**
**② 拿 av.open 当播放器,真读一次**
认出 **mov,mp4,m4a,3gp,3g2,mj2**,时长 **2.02 秒**,解出 **50 帧**。
这份代码与 Demo 47 里读本地文件那段**一个字都没差**,只是把路径换成了网址。
**③ 服务器那边收到了什么**
次序 Range 头 发出去的字节
---------------------------------------
1 bytes=0- 46137
2 bytes=48- 46089
一共发了 **92226 字节**,文件只有 **46137 字节**——**2.0 倍**。多出来的那部分全是重发。
**第一行那个 `bytes=0-` 是「从 0 一直到结尾」**,服务器老老实实整个发了。
第二行 `bytes=48-` 是 AVIO 定位到第 48 字节之后重新要一次——**在网络上一个 seek 就是一次新请求**。
**这就是本地文件与网络流最本质的差别**:本地 seek 是改个指针,网络 seek 是再跑一趟。
文件越大、跳得越多,这一趟的代价越明显。
**④ 端口不通时报什么**
拿一个没人监听的端口试,报 **HTTPServerError**。
**注意它报的不是 ProtocolNotFoundError**,说明 http 协议在、连接也想建,只是对面没有服务。
这跟 Demo 38 那套判法是一致的。
**⑤ 老实说清楚这一节测到了哪**
测到的 没测到的
----------------------------------------------------
HTTP 拉流能解出帧 真正的直播流(HLS/RTMP)
seek 会变成新请求 断线重连、卡顿处理
端口不通的报错长相 码率自适应切换
**这里拉的是一个能 seek 的 mp4**,不是直播流。
直播流根本不能 seek,所以它才要切片(HLS)或者走 RTP。**这两种本机都没有源**,能验的就到这一层。
两条 Range 请求,总共发了 92226 字节,是文件的两倍。 多出来的全是「定位之后重新要」的那一趟。
30 秒实验
- 把服务改成不支持 Range(永远回 200 加整个文件),看请求数变几条。
- 在服务端记一条时间戳,看两次请求间隔多久。
- 把 timeout 设成 0.001,看超时抛哪个异常。
常见误区
- 以为网络流不能 seek 是 bug。 是代价问题,不是能力问题。
- 把「拉了个 mp4」当成「测过直播」。 对端完全是两码事。
- 端口不通就以为协议没编进来。 报的是 HTTPServerError。
自测三问(点开对答案)
1. 为什么同一个文件在 HTTP 上传了两遍? —— AVIO 定位之后重新发了一条 Range 请求。网络上 seek 就是新请求。 2. 直播流为什么通常没有总时长? —— 它还没结束,大小与时长都不确定。 3. 报 HTTPServerError 与 ProtocolNotFoundError 排查方向差在哪? —— 前者查服务与网络,后者查 FFmpeg 构建有没有编进这个协议。- ☐ (本节完,下一节 6.4 内存IO模式之自定义AVIO案例实战)