从 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 秒实验
- 数一数
demux()与demux(st)各拿到多少个包,看音频包占了几成。 - 把 URL 里的
127.0.0.1换成localhost,看还能不能开(提示:能,但多一次解析)。 - 把
srv.shutdown()提到解码之前,看报什么错(提示:服务停了,读一半就断)。
常见误区
- 拿
buffer_size去写 YUV 文件。 里面含行填充,写出来是斜的。 - 以为网络流没有格式名。 一样有,URL 只是换了个入口。
- 断流就重开
av.open。 要从关键帧接,否则头几帧是花的。
自测三问(点开对答案)
1. 从 URL 打开与从文件打开,返回的东西差在哪? —— 格式、时长、流都一样;差的是传输层与重连。 2. 为什么 Y 平面合计与落盘字节不一样? —— 前者按 line_size 对齐算,后者按紧凑布局算,差的是行填充。 3. 直播断流为什么要记关键帧? —— 重连要从关键帧开始接,否则解出来的头几帧是花的。- ☐ (本节完,下一节 12.6 给摄像头实时添加水印后编码为 H.264)