
搞过多路摄像头接入或者自己写过RTSP客户端拉流的朋友应该都有过这种经历DESCRIBE发过去200 OK回来了SETUP发过去也200 OK了然后……画面就是不出来。不是网络不通也不是端口不对就是没有数据流。那十有八九是漏了PLAY这一步。RTSP的设计和HTTP最大的不同就是它不是一个“一问一答”的协议。它不是客户端发个GET服务器甩一堆数据回来就完事了。RTSP是有状态的服务器要记住你现在在哪个环节而状态从READY切到PLAYING靠的就是PLAY方法。很多初学RTSP协议的人会把SETUP当成“开始播放”其实SETUP只是告诉服务器我要在这个端口上接收你的数据端口分配好了通道准备好了。真正让服务器按下“播放键”的是PLAY。如果你正在写自己的RTSP播放器或者在做推流、拉流服务端又或者在看别人写的协议栈源码这一篇把PLAY相关的坑和细节一次讲清楚。1. 状态机里的一步之遥为什么SETUP之后非要再来一次PLAY要理解PLAY得先把RTSP的会话状态机搞明白。RTSP有几种定义好的状态INIT、READY、PLAYING。初始建立连接后不管是TCP还是UDP客户端和服务器之间的RTSP交互就开始了这时候状态是INIT。OPTIONS、DESCRIBE都还在INIT状态里待着它们属于“会话建立前”的探询与描述。真正常用的状态转换是这样走下来的客户端发SETUP服务器返回200 OK之后会话进入READY状态。在READY状态客户端发PLAY服务器返回200 OK会话进入PLAYING状态。在PLAYING状态客户端可以发PAUSE让会话回到READY状态。在任何状态都能发TEARDOWN直接把会话干掉回到INIT。也就是说PLAY是READY到PLAYING的唯一入口。服务器只有在PLAYING状态下才会真正开始往你SETUP时指定的传输地址上推RTP报文。没有PLAYSETUP之后的RTP端口是静默的。我自己第一次调试的时候就吃过这个亏SETUP返回了200也拿到了服务器分配的Session ID和传输参数以为万事大吉了结果在抓包里看了半天发现服务器完全不往客户端端口发包。后来才意识到状态还没切到PLAYING服务器当然不会搬运数据。从协议设计者的角度看这个机制是合理的SETUP解决的问题是“数据往哪送”PLAY解决的是“现在开始送”。把“目的地”和“出发动作”分开客户端才能真正控制流的启停比如先SETUP好多个流然后精准控制在哪个时间点开始播、用什么速率播。这里有个额外的好处如果客户端有多个SETUP好的流比如视频和音频分别SETUP播放时可以只对某一个流发PLAY也可以对会话的整体URL发PLAY让会话内所有流一起启动。这种机制在点播文件时特别有用——可以先SETUP音视频两个流各自Stay in READY等用户点了播放按钮再统一PLAY实现音视频同时起播避免画面先出声音后出、或者反过来。其实这个状态转换的语义用一台老式录音机来理解特别方便SETUP相当于把磁带装进卡槽、按下暂停等播放键亮起PLAY就是按下播放键磁带开始走带。你没有把磁带规规矩矩放到正确轨道上直接按播放是无效的同样你不发PLAY磁带装得再端正转子也是不动的。2. PLAY请求逐字段拆解Range、Scale、Speed背后是同一个动作PLAY请求的行格式和RTSP其他请求一样第一行就是方法名和URI接着是一堆头字段。日常打流时最常用的一版看起来是下面这样PLAY rtsp://192.0.2.100/stream1 RTSP/1.0 CSeq: 7 User-Agent: RTSPClient/1.0 Session: 9f8e7d6c5b4a Range: npt0.000-这里有几个字段每个都有讲究。URI怎么填如果之前SETUP用的是媒体控制URL比如rtsp://192.0.2.100/stream1/trackID1PLAY的URI通常用会话控制URL比如rtsp://192.0.2.100/stream1让服务器对该会话里的所有流统一生效。有些服务器两处都用trackID那样的全URL也能接受但按标准实现推荐用聚合控制URL。如果你只有一个流直接顺手把DESCRIBE响应里的Control: rtsp://...原样填进去就行。CSeq是个容易被忽略但不能错的东西。RTSP里的每个请求都要带流水号服务器返回的响应头里必须带相同的CSeq。CSeq乱跳的基本都是自己代码写错了。我见过有人的客户端在重发请求时忘了递增CSeq导致服务器把重发当成新请求处理整个会话乱套。在并发请求场景下CSeq还能帮你在抓包里快速把请求和响应配对。所以把它当成你在RTSP会话里的“身份证号”别乱填。Session字段同样重要。它来自SETUP响应里的Session头是一串服务器分配的会话标识。PLAY请求里如果不带Session服务器会直接返回455 Method Not Valid in This State请求方法在当前状态不合法或者找不到会话。这个也和状态机有关——服务器要知道你是“哪台设备”发过来的PLAY避免串会话。Range是PLAY最核心的参数之一。它控制“从哪个时间点开始播”。最常用的是npt0.000-表示从媒体开始的第0秒一直播到结束。NPT全称是Normal Play Time可以理解为磁带上的绝对播放位置单位是秒。点播场景中做拖动播放就是改这个值比如Range: npt120.5-意思是从120.5秒处开始播到结束。有些服务器也支持Range: clock20240815T120000Z-这种绝对时钟格式但实时监控流的服务器一般更认npt有的压根不解析clock。做客户端的时候最好两种格式都支持但优先用npt。Scale和Speed是另外两个很容易踩坑的字段。Scale代表播放速度的缩放因子1表示正常速度2表示两倍速0.5表示半速负数表示倒放比如Scale: -1。但不是说服务器天然支持任意Scale值很多服务器只支持1遇到Scale不为1直接返回459 Aggregate Operation Not Supported不支持聚合操作或者460 Non-Unique Session Identifier非唯一会话标识这类错误实际表现就是播放失败。所以你在做点播倍速功能前最好先发一个SetParameter或者OPTIONS看看服务器支持哪些特性或者拿一个Scale2的PLAY试一下失败了再退化到Speed字段。那Speed用来干嘛Speed不改变播放方向Scale才代表方向它表示播放的绝对速率。比如Scale1、Speed4意思是以4倍速正向播放。在某些支持变速播的服务器上二者配合可以实现正向快速和倒放。实时流里这两个字段基本不用点播场景才需要认真设计。还有Require和Proxy-Require。如果你的客户端需要某个服务器不一定会支持的功能可以在PLAY里带上Require字段比如某些私有协议扩展。服务器不支持的话会返回551 Option Not Supported。有些服务器实现不严谨会忽略这个字段继续正常播放所以依赖Require做特性探测并不可靠。PlayNow也没那么简单。有些文档里你会看到Range: nptnow-或者Range: npt0.000-被标记为“立即播放”。严格来说“从0开始播”和“立即播”还是有区别的如果是一个已经走了一段时间的实时流从0开始意味着会有很长一段旧内容缓冲而now-则表示从当前直播时刻开始。好在实时流的服务器通常都会做处理把0等价解释成当前时刻。但这个细节在点播文件流上千万不能简化点播文件Range0-就是从头播Rangenow-在很多服务器上会直接报错。3. 响应里的关键信息RTP-Info、会话保持与常见错误码把PLAY发出去以后服务器的响应自然分两种成功和失败。成功的响应长这样RTSP/1.0 200 OK CSeq: 7 Session: 9f8e7d6c5b4a RTP-Info: urlrtsp://192.0.2.100/stream1/trackID1;seq232433;rtptime972433000 Date: Mon, 15 Aug 2024 08:25:31 GMT第一行和CSeq好懂关键是RTP-Info这是PLAY响应里最值钱的信息。它告诉客户端这个URL对应的RTP流接下来的序列号从哪里开始seq字段RTP时间戳的起始值是多少rtptime字段。为什么这两个数字重要因为客户端要拿它们做RTP包的重排序、丢包检测和音画同步也就是所谓的“小起点对齐”。如果你忽略RTP-Info直接用第一个收到的RTP包的seq和时间戳作为基准那播放器在计算延迟、抖动时就会产生偏差。尤其是点播场景时间戳的基准对不上声音画面就对不起位。一个会话里如果有两条流视频音频RTP-Info里会带两行信息分别对应两个track的URL、seq和rtptimeRTP-Info: urlrtsp://192.0.2.100/stream1/trackID1;seq232433;rtptime972433000, urlrtsp://192.0.2.100/stream1/trackID2;seq10203;rtptime21032330客户端拿到这两个基准后各自对齐各自的流互不干扰。注意RTP-Info里的url必须和SETUP阶段用过的track URL对应上不匹配的时候说明服务器实现有差异解释RTP包时要小心。另外响应里Session头保持不变这也是个信号服务器确认会话仍然有效。客户端在后续请求中要继续携带这个Session ID否则下一轮的播放控制命令比如PAUSE、TEARDOWN又会失效。再来说说失败的响应。从服务器返回的错误码能快速定位问题方向这也是排查故障的第一手资料。常见的有这几种404 Not Found请求URI对应的资源不存在。多半是PLAY的URI和SETUP时的控制URL不一致或者干脆少写了一层路径。455 Method Not Valid in This State当前状态下PLAY不合法。最常见的原因就是没SETUP就发PLAY或者SETUP之后就立刻重发PLAY第二次PLAY在一些服务器上会因状态没变化被拒。459 Aggregate Operation Not Supported聚合操作不支持。如果你对会话聚合URL发PLAY而服务器只支持单流控制就会报这个。461 Unsupported Transport传输方式不支持。虽然更多出现在SETUP阶段但有些服务器在验证传输参数时会在PLAY阶段才返回这个。463 Unsupported ScaleScale值不支持。这个和前面讲的一样常见于点播倍速场景。需要强调的是有些服务器实现并不是严格按照标准返回错误码。我遇到过某个设备明明没SETUP就发PLAY结果回的是401 Unauthorized其实是它的鉴权时机没做对。遇到奇怪的错误码不要死抠标准先确认会话状态和URI。4. 实战验证音视频双流会话中PLAY后的时序、端口与时间基准光讲字段不上一段真实的交互过程总觉得少了点什么。我之前用一个模拟项目X一套本地开发用的RTSP服务端自写客户端做过一次完整的拉流验证这里把关键过程复盘一下。环境是这样一个模拟摄像头IPC-2207RTSP服务端口554视频H.264音频AAC。客户端用的是自己开发的测试工具RTSPTestClientUDP传输。完整交互如下C-S: OPTIONS rtsp://192.0.2.100/stream1 RTSP/1.0 CSeq: 1 User-Agent: RTSPTestClient/1.0 S-C: RTSP/1.0 200 OK CSeq: 1 Server: IPCStreamer/2.0 Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETERC-S: DESCRIBE rtsp://192.0.2.100/stream1 RTSP/1.0 CSeq: 2 Accept: application/sdp User-Agent: RTSPTestClient/1.0 S-C: RTSP/1.0 200 OK CSeq: 2 Content-Type: application/sdp Content-Length: 432 // 这里有SDP里面有两个媒体段videos对应trackID1audio对应trackID2接着分别SETUP两个track各自分配客户端端口。我用视频端口10000音频端口10002C-S: SETUP rtsp://192.0.2.100/stream1/trackID1 RTSP/1.0 CSeq: 3 Transport: RTP/AVP;unicast;client_port10000-10001 User-Agent: RTSPTestClient/1.0 S-C: RTSP/1.0 200 OK CSeq: 3 Session: 9f8e7d6c5b4a Transport: RTP/AVP;unicast;client_port10000-10001;server_port20000-20001C-S: SETUP rtsp://192.0.2.100/stream1/trackID2 RTSP/1.0 CSeq: 4 Session: 9f8e7d6c5b4a Transport: RTP/AVP;unicast;client_port10002-10003 User-Agent: RTSPTestClient/1.0 S-C: RTSP/1.0 200 OK CSeq: 4 Session: 9f8e7d6c5b4a Transport: RTP/AVP;unicast;client_port10002-10003;server_port20002-20003到这里两个流都在READY状态。然后发PLAYC-S: PLAY rtsp://192.0.2.100/stream1 RTSP/1.0 CSeq: 5 Session: 9f8e7d6c5b4a Range: npt0.000- User-Agent: RTSPTestClient/1.0 S-C: RTSP/1.0 200 OK CSeq: 5 Session: 9f8e7d6c5b4a RTP-Info: urlrtsp://192.0.2.100/stream1/trackID1;seq232433;rtptime972433000, urlrtsp://192.0.2.100/stream1/trackID2;seq10203;rtptime21032330PLAY返回200之后立刻就能在客户端UDP端口上抓到RTP包。这里有个很关键的时序特征视频流和音频流的RTP包几乎同时到达但两者的seq和时间戳各自独立、互不关联。如果你的播放器是把视频和音频的seq混在一起排序那基本必挂。我在抓包后发现video流第一个包seq232433正好和RTP-Info里的seq对应音频流第一个包seq10203也和RTP-Info对得上。时间戳也一样第一个视频RTP包的时间戳等于972433000。这说明服务器在设计上严格遵循了RTP-Info的指引。后来我故意把RTP-Info里的seq改错客户端果然出现了大量丢包误报——因为播放器在等的seq一直不出现而实际收到的包又因为序号对不上被当成“乱序包”缓存起来最后缓冲区爆掉。还有一个容易被忽视的点Range返回里可能带实际起播时间。比如服务器收到Range: npt0.000-后响应头可能带Range: npt0.000-123.456表示这个媒体实际总时长是123.456秒。有些播放器会拿这个值做进度条总时长。如果服务器没回Range总时长只能靠SDP里解析或者等RTCP统计来推算。这段模拟实验给我最大的体会是PLAY不是“发出去了就完事”你要把响应里的RTP-Info当成一个“基准信令”认真解析之后和后续RTP包做比对。抓包里第一个RTP包的seq和rtptime和RTP-Info对不上基本可以断定服务器实现有缺陷要么就是服务器并不打算好好支持你的播放请求。5. 现场排查在一次OK之外的PLAY失败里我们查了什么聊点真实的排错过程。有一次我们在调试一个客户现场的设备现象是第三方播放器连上去能看到画面但我们自己的客户端一按播放就黑屏。抓包之后请求是这样的C-S: PLAY rtsp://192.0.2.100/cam/real RTSP/1.0 CSeq: 8 Session: 3a2b1c5d Range: npt0.000- User-Agent: RTSPTestClient/1.0服务器回了一个RTSP/1.0 455 Method Not Valid in This State CSeq: 8 Server: IPCStreamer/2.0排除链路是这样的。第一步看这个会话是不是真的在READY状态。把前面几百行的抓包翻出来确认SETUP是否成功。一查发现SETUP的请求URI是rtsp://192.0.2.100/cam/real/trackID1SETUP响应OKSession ID也确实是3a2b1c5d。这一步没问题。第二步核对PLAY的URI。我们当时PLAY发的是rtsp://192.0.2.100/cam/real但是DESCRIBE响应里的聚合控制URL写的是rtsp://192.0.2.100/cam。也就是说播放器用了track的路径但播放控制URL指向的根路径和设备上报的不一致。服务器一看你没有对rtsp://192.0.2.100/cam这个资源做过SETUP凭什么让我播放直接455。第三步改掉URI换成DESCRIBE响应里Control字段给的URL再发PLAY就正常了。这个坑其实特别常见很多客户端的PLAY URL是拿字符串拼接的截断了trackID却没有改成真正的聚合控制URL而是拼了个不存在的地址。还有一个案例更隐蔽。某个设备支持双码流明明视频FLV里拉流正常但RTSP按子码流播放就是没数据。我们抓包发现PLAY之后服务器回了200 OKRTP包却迟迟不来。后来才发现问题出在SETUP时Transport里少了modePLAY参数。有些服务器在SETUP时会根据Transport头里的mode字段决定流是只读PLAY还是可写RECORD不写就默认PLAY但个别服务器实现不写就默认不支持。这类兼容性问题靠查阅设备文档比自己瞎猜快得多。排查PLAY类问题我自己的固定套路是这样的也分享给你先看状态SETUP有没有成功Session ID是否一致CSeq是否连续。再看URIPLAY的URI必须能对应到DESCRIBE响应里的控制URL别自己想当然拼路径。看响应200 OK的话RTP-Info有没有seq和rtptime是否和抓包一致4xx、5xx的话先按错误码定位再回到抓包里验证状态。看数据UDP的话看RTP包有没有从server_port发到client_portTCP的话看有没有在SETUP时协商的interleaved通道里收到数据。看服务器日志最好把服务器端日志级别开高它能直接告诉你为什么拒绝这次PLAY比客户端猜十次都管用。说到这必须提一嘴RTSP over TCP的情况。PLAY之后数据不是从UDP端口来而是在RTSP的TCP连接上以$开头、两位通道号标识的方式夹在信令消息中间传输。有的播放器在TCP模式下时序解析写得不仔细会把RTP数据和RTSP响应混在一个读线程里处理结果响应还没收完就开始解RTP包导致花屏。这个和PLAY本身没关系但确实是PLAY之后最常踩的第二个坑。6. PLAY之后的那些“隐形工作”PAUSE、TEARDOWN与后续PLAYPLAY不是RTSP会话的终点。播放状态还能切换到别的状态而每种切换背后都有语义。PAUSE是最直接的。播放过程中你发PAUSE服务器立刻停止推送RTP包会话回到READY状态。继续播放再发一次PLAY注意要带上原会话的Session ID。有些播放器从PAUSE恢复后忘了重新检查RTP-Info直接拿旧的seq和rtptime当基准结果包序列不连续导致画面卡顿。正确做法是每次PLAY成功后都要以最新返回的RTP-Info为准重新对齐RTP包序列。特别是从PAUSE恢复时服务器完全可能给你一组新的RTP基准值。TEARDOWN则更干脆中断所有数据流释放会话资源。客户端收到200 OK后这个Session基本就废了。有些服务器对TEARDOWN的响应处理得比较随意不回Response也没事但客户端要等一会儿再关闭连接否则服务器可能来不及清理UDP端口导致短时间内新会话复用出现口地址冲突。在同一会话里连续发两次PLAY大部分服务器是允许的第二次PLAY通常会被解释成“停止当前输出并用新的Range重新开始”。这也是做成seek拖动进度条功能的理论基础你发一个Range: npt120.5-的新PLAY服务器就从120.5秒处重新开始推流。不过坑在于有的服务器在第二次PLAY时不会更新RTP-Info有的会。不更新的客户端继续沿用旧基准就行更新的你得跟着切换。这个差异最好在对接文档里记清楚。关于rerun之后的会话健康检查有个小技巧定期发GET_PARAMETER作为保活消息。很多RTSP服务端超过一定时间没收到任何信令就会自动踢掉会话。保活间隔一般取会话超时时间SETUP响应里如果带Timeout字段值就是服务器告诉你的超时秒数的一半比如Timeout: 60就每30秒发一次GET_PARAMETER。注意GET_PARAMETER本身也可能被服务器错误处理为不支持那就退一步用OPTIONS做保活。保活逻辑不影响PLAY本身但实际项目里SESSION被踢导致黑屏的案例远比想象中多。还有一个非标准但广泛使用的行为部分服务器在PLAY后并不会持续输出RTP而是等待客户端发GET_PARAMETER才推进。这个一般出现在某些低功耗设备上官方文档也不会写。如果PLAY之后一直抓不到RTP可以试着周期性发GET_PARAMETER看看能不能“唤醒”数据流。这个方法在个别设备上真的有效也算是个野外偏方。如果是在做服务端比如自己实现一个RTSP Server响应客户端的PLAY务必注意几点第一在返回200 OK之前确保会话状态已经切到PLAYING再开始推RTP否则客户端可能因为响应先到、数据没到而立刻超时第二RTP-Info里的seq和rtptime必须从真实发送的第一个RTP包上取不能瞎编第三如果Range不支持或者Scale不支持不要哑巴吃黄连假装没看到直接返回463或者459让客户端降级。服务端实现得越严谨下游客户端的兼容问题就越少。7. 我实际做播放器时的一些体会写了这么多说点个人操作层面的琐碎经验。第一RTP-Info的解析务必做成强校验。如果服务器在200 OK里给了RTP-Info但你解析失败客户端状态最好直接判错重试。因为后续所有RTP包的seq和时间戳都靠它来校准。我见过有人怕麻烦不解析RTP-Info直接拿第一个收到的RTP包当基准点播场景下一旦服务器在PLAY前还发了重传包基准就乱了。第二Range的生成要区分点播和直播。我自己在代码里做了一套默认规则实时流永远发Range: npt0.000-或者干脆省略Range部分摄像头省略了也能正常播点播流如果用户没有显式指定起点就发Range: npt0.000-这表示从头播放如果用户拖动进度条就用当前拖到的秒数构造Range。千万别用当前系统时间拼clock格式除非你能确定服务器支持。第三UDP和TCP的选择要在SETUP阶段决定PLAY本身不能改传输方式。如果你SETUP的时候用的是UDP之后发现NAT把包丢了想切到TCP那必须重新SETUP整个会话再重新PLAY。这算RTSP设计里一个比较坑的流程性质但多数新人在“播放黑屏”时往往会误判是PLAY的问题白折腾一场。第四尽量给客户端加一个“超时重试”机制。PLAY发出后如果迟迟没有收到响应或者RTP包一般等2-3秒就该主动TEARDOWN重建会话。不要无限等下去否则服务器的会话资源会被无效连接占死。尤其设备端并发能力有限一条无效会话挂着后面可能就建不起新会话了。第五RTSP信令和RTP数据共用同一个TCP连接时读取线程要分开。信令响应要按RTSP帧格式解析CRLF分隔、Content-Length标记RTP数据要按$开头通道号长度帧格式解析。混在一起处理看着省事实际上调度起来很容易卡住。PLAY之后信令和RTP包往往同时到达一个成熟的读取逻辑这时候才算真正被考验。搞好RTSP的PLAY这一环后面无论是接录像回放、接倍速播放、还是做seek都会顺手很多。毕竟整个RTSP协议的最核心目的就是让客户端能精确控制媒体流的起止、方向和速率。PLAY就是这扇门。门开了数据才有意义门没开前面SETUP做得再漂亮也是空转。