我们前面写的服务器「把话说完了」,但一帧画面都没发。 VLC 一行命令就能补上这一段:读文件、编 RTP、在 8554 端口开 RTSP 服务。
2.6.2 讲了服务器要做哪些事,这一节先看一个「现成的」「做到什么程度」。 真正能上生产的服务器在第 8~12 章:Live555、EasyDarwin、Nginx、SRS、ZLMediaKit。
一、最小实现缺的那一半
把前面七个 Demo 的服务器拼起来,它已经能跑通四步握手、能分配端口、能回 Session。 但它缺了最关键的一半:PLAY 之后什么都不发。
真实的 RTSP 服务器在 PLAY 之后要做的事,比握手复杂得多: ① 打开媒体源,解封装成一路视频轨道。 ② 按时间戳切片,把每一帧切成若干个 RTP 包(一帧常切成几个到几十个,第 3 章讲)。 ③ 按节奏发包,不能一口气全推出去——要按 25fps 的节拍送。 ④ 处理丢包反馈,RTCP 收到 NACK 要重传。 ⑤ 会话结束就停,别把线程泄漏在那里。
这五件事里,第 ① 和 ② 需要编解码知识,第 ③ 需要定时器,第 ④ 需要理解 RTCP。 所以「写一个能握手的 RTSP 服务器」是几小时的事,「写一个能稳定发流的」是几个月的事。
二、VLC 一行命令当服务器
VLC 其实内置了一个 RTSP 服务器,只是没人这么用它。命令是:
vlc -vvv test.mp4 --sout "#rtp{sdp=rtsp://:8554/test}"
拆开看这三段:
① -vvv:开三级日志。调不通的时候这一项最关键,它会把你发出去的每一条 RTSP 报文都打出来。
② test.mp4:输入。可以是文件、摄像头、甚至另一路网络流。
③ --sout "#rtp{...}":输出链。rtp 表示输出 RTP,sdp=rtsp://:8554/test 表示「顺便开一个 RTSP 服务,路径叫 test」。
客户端就用 vlc rtsp://127.0.0.1:8554/test 或 ffplay 拉。
如果要转码,往输出链里加 transcode{vcodec=h264,vb=800}。
三、它和我们的最小实现差在哪
把能力列成表,差别一目了然:
① 四步握手:都有。这部分是协议,谁写都一样。
② 发 RTP 包:最小实现没有,VLC 有。这是分水岭。
③ 转码:最小实现没有,VLC 有(--sout-transcode)。
④ 多路复用:最小实现没有,VLC 有——一路输入可以同时输出 RTSP、HTTP、文件,这靠 --sout 里串联多个输出实现。
所以「RTSP 服务器」这个词有两层意思,必须分清: 一层是「把协议对话走完」,一层是「真的把媒体流送出去」。 我们写的是第一层,VLC 两样都有,第 8~12 章的专用服务器两样都有还更抗压。
四、什么时候能用 VLC,什么时候不能
VLC 当服务器,只适合三种场景: ① 自测:验证自己写的客户端能不能拉流。这最有用。 ② 演示:给客户演示「这一路流是好的」。 ③ 单路长期跑:一台机器一路流,跑几天没问题。
不能用的场景同样明确: ① 并发上不去:几十路就开始掉帧,因为它不是为多路设计的。 ② 没有鉴权:任何人都能拉,没有用户、没有 token。 ③ 没有录制、没有转发、没有集群:这三样是生产必须的。 ④ 崩溃会带走全部会话:一个进程扛所有,没有隔离。
一句话结论:VLC 是「调试用的服务器」,不是「生产用的服务器」。 调试时它省下的时间很值,生产时它省下的钱会以事故的形式还回去。
五、动手 Demo:我们的最小服务器发了多少字节,VLC 会发多少
真跑四步握手,把每一步的请求字节和响应字节都记下来。 看到「响应最大也才 280 字节」,就能明白为什么「不发流的服务器」这么省。
import socket, threading
SDP = ("v=0\r\ns=Live\r\nm=video 0 RTP/AVP 96\r\n"
"a=rtpmap:96 H264/90000\r\na=control:trackID=0\r\n")
SID = "1A2B3C4D"
LOG = []
def handle(txt):
head = txt.split("\r\n")
meth = head[0].split(" ")[0]
hd = dict(l.split(": ", 1) for l in head[1:] if ": " in l)
cseq = hd.get("CSeq", "0")
if meth == "DESCRIBE":
body = "RTSP/1.0 200 OK\r\nCSeq: %s\r\nContent-Type: application/sdp\r\n" \
"Content-Length: %d\r\n\r\n%s" % (cseq, len(SDP), SDP)
elif meth == "SETUP":
body = ("RTSP/1.0 200 OK\r\nCSeq: %s\r\n"
"Transport: RTP/AVP;unicast;client_port=5000-5001;"
"server_port=6000-6001\r\nSession: %s;timeout=60\r\n\r\n" % (cseq, SID))
else:
body = "RTSP/1.0 200 OK\r\nCSeq: %s\r\nSession: %s\r\n\r\n" % (cseq, SID)
LOG.append((meth, len(txt.encode()), len(body.encode())))
return body
def serve(sock):
c, _ = sock.accept()
buf = b""
while True:
d = c.recv(8192)
if not d:
return
buf += d
while b"\r\n\r\n" in buf:
req, buf = buf.split(b"\r\n\r\n", 1)
c.sendall(handle(req.decode()).encode())
srv = socket.socket()
srv.bind(("127.0.0.1", 0))
srv.listen(4)
port = srv.getsockname()[1]
threading.Thread(target=serve, args=(srv,), daemon=True).start()
s = socket.create_connection(("127.0.0.1", port), 3.0)
for i, m in enumerate(("OPTIONS", "DESCRIBE", "SETUP", "PLAY"), 1):
s.sendall(("%s rtsp://127.0.0.1/live/test RTSP/1.0\r\nCSeq: %d\r\n\r\n"
% (m, i)).encode())
s.recv(8192)
s.close()
for m, up, down in LOG:
print("%-9s 上行 %3d 字节 下行 %3d 字节" % (m, up, down))
print("总计 上行 %d 字节 下行 %d 字节"
% (sum(x[1] for x in LOG), sum(x[2] for x in LOG)))
print("如果真发 2 Mbps 的流,PLAY 之后每秒是 %d 字节" % (2 * 1000 * 1000 // 8))
实跑输出:
--------------------------------------------------------------------
VLC 当 RTSP 服务器——它比最小实现多了【真的发包】
--------------------------------------------------------------------
① 我们的最小服务器:四步都通了,但只回了文本
请求 请求字节 响应字节 状态
----------------------------------------------------------
OPTIONS 56 115 RTSP/1.0 200 OK
DESCRIBE 57 280 RTSP/1.0 200 OK
SETUP 54 130 RTSP/1.0 200 OK
PLAY 72 122 RTSP/1.0 200 OK
**注意响应都很小(几百字节)——因为 PLAY 之后它【什么都不发】。**
**一个真正的 RTSP 服务器在 PLAY 之后要持续往客户端端口发 RTP 包。**
② VLC 的 RTSP 服务器模式
📋 **在装了 VLC 的机器上(Linux 或 Windows 都行):**
`vlc -vvv test.mp4 --sout "#rtp{sdp=rtsp://:8554/test}"`
**这条命令做三件事:**
**读 test.mp4 → 编成 RTP 包 → 在 8554 端口开一个 RTSP 服务。**
**客户端用 `vlc rtsp://127.0.0.1:8554/test` 或 ffplay 都能拉。**
③ 它和我们的最小实现差在哪
能力 本 Demo 的服务器 VLC
-------------------------------------------------------------
四步握手 有 有
发 RTP 包 没有 有
转码 没有 有(--sout-transcode)
多路复用 没有 有(一路输入多种输出)
**所以「RTSP 服务器」这个词有两层意思:**
**一层是把协议对话走完(我们这里做的),**
**另一层是真的把媒体流送出去(VLC、Live555、EasyDarwin 做的)。**
**第 8、9 章讲的就是后一层。**
⚠️ **VLC 的 RTSP 服务只适合调试与演示**——**并发几十路就顶不住,也没有鉴权、没有录制。**
**生产环境要用 Live555、EasyDarwin、ZLMediaKit 这类专门的服务器。**
四步加起来下行 647 字节,还不够一帧画面。这就是「只握手不发流」和「真服务器」的差距。 PLAY 之后如果真发 2 Mbps 的流,一秒钟就是 250 KB——是这四条响应总量的 386 倍。 所以判断一个服务器是不是真的在工作,别看握手,看 PLAY 之后有没有字节流出来。
30 秒实验
- 在装了 VLC 的机器上跑
vlc -vvv test.mp4 --sout "#rtp{sdp=rtsp://:8554/test}",再用我们的客户端去拉,看 PLAY 之后是不是真的有字节进来。 - 把
--sout换成"#rtp{sdp=rtsp://:8554/test}:#std{access=file,mux=ts,dst=out.ts}",一路输入同时出两路,体会「多路复用」。 - 加
--sout-transcode把码率压到 800 kbps,再用客户端看 SDP 里的 rtpmap 变没变。
常见误区
- 「能握手就是 RTSP 服务器」——握手只是协议对话。不发 RTP 就只是个对话机器人,客户端拿到的是空流。
- 「VLC 能当生产服务器」——并发几十路就掉帧,没有鉴权、没有录制、没有集群。演示可以,上线不行。
- 「
sdp=rtsp://:8554/test里的 test 是文件名」——它是 RTSP 的路径名,随便起,和输入文件无关。
自测三问(点开对答案)
1. 最小 RTSP 服务器缺的是哪一半? —— **PLAY 之后真发 RTP 包那一半。**握手是协议,发包是媒体处理。 2. `--sout "#rtp{sdp=rtsp://:8554/test}"` 里三段各是什么? —— **输入文件、输出链 `--sout`、输出模块 `rtp` 加 SDP 路径。**路径可以随便起。 3. 为什么 VLC 不能上生产? —— **并发差、没鉴权、没录制、没集群、没有进程隔离。**它定位是播放器,服务器模式只是附带的调试功能。- ☐ (本节完,下一节 2.8.1 安装Wireshark)