🎬 第六章 AVProtocol协议层理论及案例实战
📄 一句话概括

从文件换到网络,代码一个字都不用改,代价全在定位上。 本地 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 秒实验

  1. 把服务改成不支持 Range(永远回 200 加整个文件),看请求数变几条。
  2. 在服务端记一条时间戳,看两次请求间隔多久。
  3. 把 timeout 设成 0.001,看超时抛哪个异常。

常见误区

  1. 以为网络流不能 seek 是 bug。 是代价问题,不是能力问题。
  2. 把「拉了个 mp4」当成「测过直播」。 对端完全是两码事。
  3. 端口不通就以为协议没编进来。 报的是 HTTPServerError。
自测三问(点开对答案) 1. 为什么同一个文件在 HTTP 上传了两遍? —— AVIO 定位之后重新发了一条 Range 请求。网络上 seek 就是新请求。 2. 直播流为什么通常没有总时长? —— 它还没结束,大小与时长都不确定。 3. 报 HTTPServerError 与 ProtocolNotFoundError 排查方向差在哪? —— 前者查服务与网络,后者查 FFmpeg 构建有没有编进这个协议。

相关笔记