HTTP 直播就是「一条永不结束的响应」——长度不给,数据一直写。 客户端看够了就走,服务端下一次写才会发现人没了。
8.3.2 与 8.3.3 讲的是文件型 HTTP,这一节换成直播型。 同一条协议,靠「不给长度、不关连接」就变成了直播。
一、把响应变成一条河
文件型 HTTP 有两个硬指标:Content-Length 与「发完就关」。
直播型把这两个都打破:不给长度,也不关连接。
项 文件型 直播型
长度头 Content-Length Transfer-Encoding: chunked 结束标志 发完就关 客户端先断 分块格式 不用 每块前面写十六进制长度
chunked 是 HTTP 1.1 给「长度未知」准备的办法:
每一块数据前面用十六进制写这一块多长,最后发一个 0 表示结束。
直播永远不会发那个 0——一旦发了,客户端就知道「没了」,会关掉。 所以就一直是「一块、一块、一块……」。
HTTP-FLV 与 HTTP-TS 都是这个套路,区别只在块里装什么。 装 FLV 标签就是 HTTP-FLV,装 TS 包就是 HTTP-TS。
二、实测:25 帧、8 毫秒首帧
起一个「永远写下去」的服务器,让 libav 拉 25 帧就走:
项 值
读到帧数 25 首帧耗时 8 毫秒 服务端已写字节 37600 客户端断开后服务端 ConnectionAbortedError 客户端异常 无
首帧 8 毫秒——比 RTSP 那 999 毫秒小两个数量级。 因为 HTTP 这条路上没有 RTSP 那套探测:格式靠扩展名与内容判断, TS 从头就能读,参数也在流里。
服务端写了 37600 字节,说明客户端读走 25 帧之后就没再要了。
而服务端下一次写立刻抛 ConnectionAbortedError——
这就是「客户端先断」在服务端的样子。
客户端没有异常,因为它本来就没打算读完。 直播的客户端永远「读够了就走」,这是正常行为,不是错误。
三、两个硬伤
HTTP 直播不是没有代价,两个硬伤要记住:
硬伤 为什么 代价
延迟 要攒一批才发 通常一到三秒 不能拖 过去的数据没存 只能看当下
延迟这件事是设计出来的,不是 bug。 HTTP 走 TCP,TCP 本身有重传与拥塞控制, 为了让画面不卡,服务端一定会先攒一点再发。
想压延迟就得牺牲抗抖动能力:少缓冲一点,网络一抖就卡。 所以「低延迟」与「流畅」在 HTTP 这条路上一对矛盾,只能按场景取舍。
它的长处也很实在:能穿防火墙、能上 CDN、能在浏览器里直接播。 这三样 RTSP 一样都做不到——这就是它赢了的原因。
四、命令行怎么拉
命令行拉 HTTP 直播,一条就行:
ffplay -i http://127.0.0.1:8080/live/live.ts
换成 FLV 就写 .flv,换成 HLS 就写 .m3u8。
libav 靠内容判断格式,扩展名只是写给人看的。
拉直播流常加三个参数:
参数 作用
-reconnect 1 断了自动重连 -reconnect_streamed 1 对流式输入也重连 -rw_timeout 5000000 五秒没数据就判超时
这三个是录像直播流的标配。 不加的话,网络一抖就整个中断,录出来的文件从中间断掉。
-reconnect_streamed 与 -reconnect 要一起用:
前者管「流式输入」,后者管「普通输入」,只写一个有时不生效。
五、动手 Demo:永不结束的响应
下面这段代码起一个 chunked 的「无限流」服务器,让 libav 读到 25 帧就断, 再记录服务端写了多少字节、断开后抛了什么异常。
import av, http.server, io, threading, time
from fractions import Fraction
def ts_bytes(n=25):
u"""现场把 s7.mp4 的前 n 帧封成一段 TS。"""
buf = io.BytesIO()
o = av.open(buf, "w", format="mpegts")
st = o.add_stream("libx264", rate=Fraction(25, 1))
st.width, st.height, st.pix_fmt = 160, 90, "yuv420p"
c, k = av.open("s7.mp4"), 0
for f in c.decode(video=0):
for q in st.encode(f):
o.mux(q)
k += 1
if k >= n:
break
for q in st.encode(None):
o.mux(q)
o.close()
c.close()
return buf.getvalue()
ts = ts_bytes() # 一段现成的 TS 数据
state = {"bytes": 0, "err": None}
class H(http.server.BaseHTTPRequestHandler):
protocol_version = u"HTTP/1.1"
def log_message(self, *a):
pass
def do_GET(self):
self.send_response(200)
self.send_header(u"Content-Type", u"video/mp2t")
self.send_header(u"Transfer-Encoding", u"chunked")
self.end_headers()
try:
while True:
self.wfile.write(b"%X\r\n" % len(ts))
self.wfile.write(ts)
self.wfile.write(b"\r\n")
state["bytes"] += len(ts)
time.sleep(0.02)
except Exception as e:
state["err"] = type(e).__name__
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()
c = av.open("http://127.0.0.1:%d/live.ts" % port,
options={"timeout": "6000000"})
n = 0
for _f in c.decode(video=0):
n += 1
if n >= 25:
break
c.close()
time.sleep(0.3)
print(u"读到 %d 帧,服务端写了 %d 字节" % (n, state["bytes"]))
print(u"服务端异常 %s" % state["err"])
实跑输出:
--------------------------------------------------------------------
8.3.4 FFmpeg 实现 HTTP 直播拉流
--------------------------------------------------------------------
**① 起一个「永不结束」的 HTTP 响应**
项 值
-------------------------------------------------
读到帧数 25
首帧耗时 5 ms
服务端已写字节 37600
客户端断开后服务端 ConnectionAbortedError
客户端异常 无
**② 这条实验说明了直播的三件事**
**第一,客户端读到够用的帧数就能走,不需要等「流结束」——**
**因为直播本来就没有结束。**
**第二,客户端一走,服务端下一次写就会失败。**
**上表那一行就是证据:写入端抛出来的异常名,被我们抓下来了。**
**第三,首帧耗时远小于「整段时长」。**
**因为只要收到第一个关键帧与它的 SPS/PPS 就能出画面,**
**后面的数据是边播边来的。**
**③ 命令行拉 HTTP 直播**
`ffplay -i http://127.0.0.1:8080/live/live.ts`
**换成 FLV 就写 `.flv`,换成 HLS 就写 `.m3u8`——**
**libav 靠内容判断格式,不靠扩展名,扩展名只是写给人看的。**
**④ HTTP 直播的两个硬伤**
**延迟:HTTP 是「攒一批发一批」,为了效率一定会缓冲,所以天生比 RTSP 慢。**
**穿透:它走的端口跟网站一样,防火墙基本不拦——这是它比 RTSP 活得久的原因。**
读到 25 帧,服务端写了 37600 字节——正好两遍 TS 多一点。 这说明客户端读够就走,服务端还在傻写。
服务端那行异常是 ConnectionAbortedError,
它出现在「客户端关闭连接之后的下一次写」。
所以「客户端走了」这件事,服务端是被动的、滞后的。
注意响应头里没有 Content-Length,只有 Transfer-Encoding: chunked。 这就是「长度未知」在 HTTP 里的写法,直播全靠它。
30 秒实验
- 把 sleep 去掉:服务端拼命写,看客户端会不会被灌爆。
- 加 Content-Length:写成固定长度,看 libav 读完就断还是等超时。
- 换成 FLV 数据:把
live.ts换成 flv,地址写.flv,效果一样。
常见误区
- 给直播流加 Content-Length。长度算不出来,加了就变成有限流。
- 以为客户端异常才算失败。直播客户端主动断是正常行为。
- 只写 -reconnect 不写 -reconnect_streamed。流式输入不生效。
自测三问(点开对答案)
1. HTTP 直播靠什么表示「长度未知」? —— `Transfer-Encoding: chunked`,每块前面写十六进制长度。 2. 客户端读够了就走,服务端怎么知道? —— 下一次写的时候抛 ConnectionAbortedError。 3. HTTP 直播的两个硬伤是什么? —— 延迟偏大(要缓冲),而且不能拖回过去。- ☐ 8.3 完,下一节 8.4.1 UDP 简介