🎬 第十二章 AVDevice设备读写理论及案例实战
📄 一句话概括

从 URL 打开与从文件打开,返回的东西一模一样。 差别只在传输:要握手、seek 要重发、断了要能重连。

📝 本节和前面几节的关系

12.2 从设备读,这一节从网络读。 两者都是「给 av.open 一个字符串」,所以又一次不用改代码。 这也是播放器的前半段:拉流、解码、出 YUV。

一、本机起一个 HTTP 服务

沙箱访问不了公网流,但网络这条路可以真验。 项 值


服务地址 127.0.0.1:53518 资源 tiny.mp4 完整 URL http://127.0.0.1:53518/tiny.mp4

这不是模拟:av.open 走的是真的 HTTP,真的发了请求、真的收了响应, 只是服务器就在本机,所以没有网络延迟。 端口写 0 是「让系统挑一个空着的」,然后用 server_address[1] 读回来。 写死端口在多人环境里十有八九被占。 服务放在后台线程里跑,主线程才能继续用 av.open 去连它; 用完 shutdown() 收掉,否则脚本退不干净。

二、打开 URL,认出了什么

先看它把这段 URL 认成了什么。 项 值


格式 mov,mp4,m4a,3gp,3g2,mj2 时长 2.02 秒 流 video/h264、audio/aac

一行 URL 就把格式、时长、流都认出来了,与本地文件没有区别。 为什么时长都读得出来:MP4 的索引在文件尾部, 库发了一个带 Range 的请求去读末尾那几百字节,服务器支持 Range,就拿到了。 如果服务器不支持 Range,时长就读不出来(返回 0), 而且 seek 也会失效——这也是为什么直播流通常没有时长:它本来就没有尾。

三、解码并落成裸 YUV

50 帧全部解出来,把每一帧的 YUV 追加到同一个文件里。 项 值


帧数 50 帧尺寸 160×120 Y 平面字节合计 1536000 Y 的 line_size 256 落盘字节 1440000 每帧平均 28800

两个数不一样,正好把 stride 讲清楚。 Y 平面合计 1536000 = 256 × 120 × 50,按对齐后的行宽算; 落盘 1440000 = 160 × 120 × 1.5 × 50,按紧凑布局算。 差了 96000 字节,就是每行那 96 字节填充乘上去。 如果你拿 buffer_size 去写文件,写出来的 YUV 是错的——播放器会看到斜着的画面。 正确做法是用 to_ndarray() 转成紧凑数组再写,它会把填充去掉, 这样写出来的才是标准 YUV420P。11.2 讲的这是同一件事,这里是它在真实素材上的样子。

四、网络流与本地文件的三处差别

前两步一样,差别全在传输这一层。 差别 说明


打开要等握手 DNS、TCP、HTTP 头,快则几十毫秒 seek 靠重发请求 不能往回拨,只能带 Range 再要 断流要能重连 真实直播必须写重连逻辑

第三条是直播与点播的分水岭:点播断了重开就行, 直播断了要「接着刚才的位置」,这就得自己记 pts 与关键帧。 重连的正确姿势不是「重开一个 av.open」就完事:要从最后一个关键帧开始接, 否则解出来的头几帧是花的。这就得在拉流时顺手记下关键帧的 pts,或者用带 seek 参数的地址。 超时也要自己设:默认可能等很久,直播场景一般要把 timeout 选项压到几秒, 否则界面像卡死。

五、这条链就是拉流

从 URL 读、解码、出 YUV——这三步就是播放器的前半段。 这一段 播放器里对应什么


打开 URL 拉流(含握手与重连) 解码 出画面帧 落成 YUV 交给渲染或交给滤镜

后半段是「渲染到屏幕」,沙箱里做不了,所以到此为止。 但前半段是完整的:这一步跑通,说明拉流这一侧没有问题了。 12.6 会把这条链的后半段补上: 拿到帧之后不是落 YUV,而是画水印、再编码——这就是从「播放」跨到「转推」的那一步。

六、动手 Demo:从网络地址读流,解成 YUV 落盘

在本机起一个静态文件服务,用 http 地址打开它,解 50 帧并把 YUV 追加落盘。

import functools
import http.server
import socketserver
import threading
import os
import av

# 1) 起一个不打印日志的静态文件服务
class Quiet(http.server.SimpleHTTPRequestHandler):
    def log_message(self, *a):
        pass

socketserver.TCPServer.allow_reuse_address = True
srv = socketserver.TCPServer((
    ("127.0.0.1", 0),
    functools.partial(Quiet, directory=os.path.abspath("_media32"))))
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = "http://127.0.0.1:%d/tiny.mp4" % srv.server_address[1]
print(url)

# 2) 打开它:与打开文件一模一样
c = av.open(url)
print(c.format.name, c.duration / 1000000.0)
print([(s.type, s.codec_context.name) for s in c.streams])
c.close()

# 3) 解码并把 YUV 追加落盘
c = av.open(url)
st = c.streams.video[0]
buf = []
for pk in c.demux(st):
    if pk.size == 0:
        break
    for fr in st.codec_context.decode(pk):
        buf.append(fr.to_ndarray().tobytes())   # 紧凑布局,去掉填充
c.close()
srv.shutdown()
open("net_160x120.yuv", "wb").write(b"".join(buf))
print(len(buf), os.path.getsize("net_160x120.yuv"))

实跑输出:

--------------------------------------------------------------------
从网络地址读流,解成 YUV 落盘
--------------------------------------------------------------------

**① 本机没有公网流,就在本地开一个 HTTP 服务**

  项           值
  ------------------------------------------------
  服务地址     127.0.0.1:53878
  资源         tiny.mp4
  完整 URL     http://127.0.0.1:53878/tiny.mp4

**这不是模拟**:`av.open` 走的是真的 HTTP,真的发出了请求。
**区别只是服务器就在本机,所以没有网络延迟。**

**② 打开它,看看认出了什么**

  项       值
  --------------------------------------------------
  格式     mov,mp4,m4a,3gp,3g2,mj2
  时长     2.02 秒
  流       [('video', 'h264'), ('audio', 'aac')]

**从 URL 打开与从文件打开,返回的东西一模一样**,
**连 duration 都能读出来**——因为服务器支持 Range,**库可以往前跳着读文件尾的索引。**

**③ 解码,并把 YUV 落成裸文件**

  项                 值
  ------------------------------
  帧数               50
  帧尺寸             160×120
  Y 平面字节合计     1536000
  Y 的 line_size     [256]
  落盘字节           1440000
  每帧平均           28800

**注意 `line_size` 是 256,而宽度只有 160**——**解码出来的帧按 256 对齐,比「宽 × 高」多出一截。**
**这就是 11.2 讲过的 stride,真实素材上一定会碰到。**

**④ 网络流与本地文件的三处差别**

  差别                说明
  -----------------------------------------------------------
  打开要等握手        DNS、TCP、HTTP 头,快则几十毫秒
  seek 靠重发请求     不能真往回拨,只能带 Range 再要一次
  断流要能重连        真实直播必须写重连逻辑

**第三条是直播与点播的分水岭**:点播断了重开就行,
**直播断了要「接着刚才的位置」,这就得自己记 pts 与关键帧。**

**⑤ 这条链就是「拉流」**

**从 URL 读、解码、落成 YUV——这三步就是播放器的前半段。**
**后半段是「渲染到屏幕」,沙箱里做不了,所以到此为止。**

格式、时长、流与本地文件一模一样,因为服务器支持 Range。 Y 的 line_size 是 256 而宽只有 160,落盘要用 to_ndarray() 去填充。

30 秒实验

  1. 数一数 demux() 与 demux(st) 各拿到多少个包,看音频包占了几成。
  2. 把 URL 里的 127.0.0.1 换成 localhost,看还能不能开(提示:能,但多一次解析)。
  3. 把 srv.shutdown() 提到解码之前,看报什么错(提示:服务停了,读一半就断)。

常见误区

  1. 拿 buffer_size 去写 YUV 文件。 里面含行填充,写出来是斜的。
  2. 以为网络流没有格式名。 一样有,URL 只是换了个入口。
  3. 断流就重开 av.open。 要从关键帧接,否则头几帧是花的。
自测三问(点开对答案) 1. 从 URL 打开与从文件打开,返回的东西差在哪? —— 格式、时长、流都一样;差的是传输层与重连。 2. 为什么 Y 平面合计与落盘字节不一样? —— 前者按 line_size 对齐算,后者按紧凑布局算,差的是行填充。 3. 直播断流为什么要记关键帧? —— 重连要从关键帧开始接,否则解出来的头几帧是花的。

相关笔记