这里写目录标题
- RTSP协议是什么
- 报文实例:
- 1. OPTIONS
- 2. DESCRIBE
- 4. SETUP
- 5. PLAY
- Java实现简单的RTSP报文交换
- 了解RTSP协议
- 使用Java程序编写RTSP客户端 访问 RTSP服务端,实现拉流
RTSP协议是什么
RTSP是一种基于文本的协议,用CRLF(回车换行)作为每一行的结束符,其好处是,在使用过程中可以方便地增加自定义参数,也方便抓包分析。从消息传送方向上来分,RTSP的报文有两类:请求报文和响应报文。请求报文是指从客户端向服务器发送的请求(也有少量从服务器向客户端发送的请求),响应报文是指从服务器到客户端的回应。
RTSP请求报文的常用方法与作用
一次基本的RTSP交互过程如下,C表示客户端,S表示服务端。
- OPTION请求
-> 响应 - DESCRIBE请求
-> 响应
如果响应无权限, 那么需要带上用户名密码 - SETUP请求
-> 响应 - PLAY请求
-> 响应
-> 流数据
-> 流数据
-> 流数据
报文实例:
1. OPTIONS
OPTIONS rtsp://39.170.35.150:1554/h264/ch0/1 RTSP/1.0
CSeq: 2
User-Agent: LibVLC/3.0.16 (LIVE555 Streaming Media v2016.11.28)
RTSP/1.0 200 OK
CSeq: 2
Public: OPTIONS, DESCRIBE, PLAY, PAUSE, SETUP, TEARDOWN, SET_PARAMETER, GET_PARAMETER
Date: Sat, Mar 05 2022 15:39:55 GMT
2. DESCRIBE
DESCRIBE rtsp://39.170.35.150:1554/h264/ch0/1 RTSP/1.0
CSeq: 3
User-Agent: LibVLC/3.0.16 (LIVE555 Streaming Media v2016.11.28)
Accept: application/sdp
RTSP/1.0 401 Unauthorized
CSeq: 3
WWW-Authenticate: Digest realm="2857be191e08", nonce="5b960b3d4673be2908666321f64d2bff", stale="FALSE"
WWW-Authenticate: Basic realm="2857be191e08"
Date: Sat, Mar 05 2022 15:39:55 GMT
发现无权, 使用用户名密码进行授权
DESCRIBE rtsp://39.170.35.150:1554/h264/ch0/1 RTSP/1.0
CSeq: 4
Authorization: Digest username="admin", realm="2857be191e08", nonce="5b960b3d4673be2908666321f64d2bff", uri="rtsp://39.170.35.150:1554/h264/ch0/1", response="3cfc28bcf70670c2120acc3b5d1357d3"
User-Agent: LibVLC/3.0.16 (LIVE555 Streaming Media v2016.11.28)
Accept: application/sdp
RTSP/1.0 200 OK
CSeq: 4
Content-Type: application/sdp
Content-Base: rtsp://39.170.35.150:1554/h264/ch0/1/
Content-Length: 569v=0
o=- 1646494795935543 1646494795935543 IN IP4 10.2.144.4
s=Media Presentation
e=NONE
b=AS:5050
t=0 0
a=control:rtsp://39.170.35.150:1554/h264/ch0/1/
m=video 0 RTP/AVP 96
c=IN IP4 0.0.0.0
b=AS:5000
a=recvonly
a=x-dimensions:1920,1080
a=control:rtsp://39.170.35.150:1554/h264/ch0/1/trackID=1
a=rtpmap:96 H264/90000
a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=Z00AKpWoHgCJ+WbgICAgQA==,aO48gA==
a=Media_header:MEDIAINFO=494D4B48010100000400000100000000000000000000000000000000000000000000000000000000;
a=appversion:1.0
4. SETUP
这里得URL后会带上control后的trackID
这里的Transport表示要使用的传输方式, TCP表示使用tcp传输, 也可以使用UDP
SETUP rtsp://39.170.35.150:1554/h264/ch0/1/trackID=1 RTSP/1.0
CSeq: 5
Authorization: Digest username="admin", realm="2857be191e08", nonce="5b960b3d4673be2908666321f64d2bff", uri="rtsp://39.170.35.150:1554/h264/ch0/1/", response="80324f6c8f797633475816843f329b61"
User-Agent: LibVLC/3.0.16 (LIVE555 Streaming Media v2016.11.28)
Transport: RTP/AVP/TCP;unicast;interleaved=0-1
RTSP/1.0 200 OK
CSeq: 5
Session: 2007782907;timeout=60
Transport: RTP/AVP/TCP;unicast;interleaved=0-1;ssrc=762c0e39;mode="play"
Date: Sat, Mar 05 2022 15:39:55 GMT
5. PLAY
PLAY rtsp://39.170.35.150:1554/h264/ch0/1/ RTSP/1.0
CSeq: 6
Authorization: Digest username="admin", realm="2857be191e08", nonce="5b960b3d4673be2908666321f64d2bff", uri="rtsp://39.170.35.150:1554/h264/ch0/1/", response="f88f707756441a9437f162d68ec5adbb"
User-Agent: LibVLC/3.0.16 (LIVE555 Streaming Media v2016.11.28)
Session: 2007782907
Range: npt=0.000-
RTSP/1.0 200 OK
CSeq: 6
Session: 2007782907
RTP-Info: url=rtsp://39.170.35.150:1554/h264/ch0/1/trackID=1;seq=40895;rtptime=1378373026
Date: Sat, Mar 05 2022 15:39:56 GMT
后续就会有二进制流进来.
Java实现简单的RTSP报文交换
RTSP端口默认为:1554
通过TCP对目标主机端口发起连接,然后使用RTSP格式的报文进行交换信息即可。
下面是报文拼凑的代码,使用建立TCP连接后安装RTSP规定的通讯顺序发送即可。
// 定义协议头和通用信息private String transport = "RTP/AVP/TCP;unicast;interleaved=0-1";private static final String VERSION = " RTSP/1.0";private static final String RTSP_OK = "RTSP/1.0 200 OK"; private String address = "rtsp://admin:shinemo123@39.170.35.150:1554/h264/ch0/1"; // RTSP URI 包含目标ip和账号密码private String sessionid; // RTSP 是有状态的,通讯成功后需要记录 sessionIdprivate void doTeardown() { StringBuilder sb = new StringBuilder(); sb.append("TEARDOWN "); sb.append(this.address); sb.append("/"); sb.append(VERSION);sb.append(System.lineSeparator());sb.append("Cseq: ");sb.append(seq++); sb.append(System.lineSeparator());sb.append("User-Agent: RealMedia Player HelixDNAClient/10.0.0.11279 (win32)");sb.append(System.lineSeparator());sb.append("Session: "); sb.append(sessionid);sb.append(System.lineSeparator());sb.append(System.lineSeparator());send(sb.toString().getBytes()); }private void doPlay() { StringBuilder sb = new StringBuilder(); sb.append("PLAY "); sb.append(this.address); sb.append(VERSION);sb.append(System.lineSeparator());sb.append("Session: ");sb.append(sessionid); sb.append(System.lineSeparator());sb.append("Cseq: ");sb.append(seq++);sb.append(System.lineSeparator());sb.append("Range: npt=0.000-");sb.append(System.lineSeparator());sb.append(System.lineSeparator());send(sb.toString().getBytes()); }private void doSetup() { StringBuilder sb = new StringBuilder(); sb.append("SETUP "); sb.append(this.address); sb.append("/"); sb.append(trackInfo); sb.append(VERSION);sb.append(System.lineSeparator());sb.append("Cseq: "); sb.append(seq++); sb.append(System.lineSeparator());sb.append("Transport: ");sb.append(transport);sb.append(System.lineSeparator());sb.append(System.lineSeparator());send(sb.toString().getBytes()); } private void doOption() { StringBuilder sb = new StringBuilder(); sb.append("OPTIONS "); sb.append(this.address.substring(0, address.lastIndexOf("/"))); sb.append(VERSION);sb.append(System.lineSeparator());sb.append("Cseq: "); sb.append(seq++); sb.append(System.lineSeparator());sb.append(System.lineSeparator());send(sb.toString().getBytes()); } private void doDescribe() { StringBuilder sb = new StringBuilder(); sb.append("DESCRIBE ").append(this.address).append(VERSION).append(System.lineSeparator());sb.append("Cseq: ").append(seq++).append(System.lineSeparator());sb.append("Accept: application/sdp").append(System.lineSeparator());sb.append(System.lineSeparator());System.out.println(sb.toString()); send(sb.toString().getBytes()); } private void doPause() { StringBuilder sb = new StringBuilder(); sb.append("PAUSE "); sb.append(this.address); sb.append("/"); sb.append(VERSION);sb.append(System.lineSeparator());sb.append("Cseq: "); sb.append(seq++); sb.append(System.lineSeparator());sb.append("Session: "); sb.append(sessionid);sb.append(System.lineSeparator());sb.append(System.lineSeparator());send(sb.toString().getBytes()); }
完整的垃圾代码:
这里使用的是netty。
package loki.rtsp;import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicBoolean;public class RTSPClient extends Thread implements IEvent { private static final String VERSION = " RTSP/1.0";private static final String RTSP_OK = "RTSP/1.0 200 OK"; /** *//** 远程地址 */ private final InetSocketAddress remoteAddress; /** *//** * 本地地址 */ private final InetSocketAddress localAddress; /** *//** * 连接通道 */ private SocketChannel socketChannel; /** *//** 发送缓冲区 */ private final ByteBuffer sendBuf; /** *//** 接收缓冲区 */ private final ByteBuffer receiveBuf; private static final int BUFFER_SIZE = 8192; /** *//** 端口选择器 */ private Selector selector; private String address; private Status sysStatus; private String sessionid; private String transport = "RTP/AVP/TCP;unicast;interleaved=0-1";/** *//** 线程是否结束的标志 */ private AtomicBoolean shutdown;private int seq=2;private boolean isSended; private String trackInfo; private enum Status { init, options, describe, setup, play, pause, teardown } public RTSPClient(InetSocketAddress remoteAddress, InetSocketAddress localAddress, String address) { this.remoteAddress = remoteAddress; this.localAddress = localAddress; this.address = address; // 初始化缓冲区 sendBuf = ByteBuffer.allocateDirect(BUFFER_SIZE); receiveBuf = ByteBuffer.allocateDirect(BUFFER_SIZE); if (selector == null) { // 创建新的Selector try { selector = Selector.open(); } catch (final IOException e) { e.printStackTrace(); } } startup(); sysStatus = Status.init; shutdown=new AtomicBoolean(false); isSended=false; } public void startup() { try { // 打开通道 socketChannel = SocketChannel.open(); // 绑定到本地端口 socketChannel.socket().setSoTimeout(30000); socketChannel.configureBlocking(false); socketChannel.socket().bind(localAddress);if (socketChannel.connect(remoteAddress)) {System.out.println("开始建立连接:" + remoteAddress); } socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE, this);System.out.println("端口打开成功" + socketChannel.getLocalAddress() + " -> " + socketChannel.getRemoteAddress());} catch (final IOException e1) { e1.printStackTrace(); } } public void send(byte[] out) { if (out == null || out.length < 1) { return; } synchronized (sendBuf) { sendBuf.clear(); sendBuf.put(out); sendBuf.flip(); } // 发送出去 try { write(); isSended=true; } catch (final IOException e) { e.printStackTrace(); } } public void write() throws IOException { if (isConnected()) { try { socketChannel.write(sendBuf); } catch (final IOException e) { } } else { System.out.println("通道为空或者没有连接上"); } } public byte[] recieve() { if (isConnected()) { try { int len = 0; int readBytes = 0; synchronized (receiveBuf) { receiveBuf.clear(); try { while ((len = socketChannel.read(receiveBuf)) > 0) { readBytes += len; } } finally { receiveBuf.flip(); } if (readBytes > 0) { final byte[] tmp = new byte[readBytes]; receiveBuf.get(tmp); return tmp; } else { System.out.println("接收到数据为空,重新启动连接"); return null; } } } catch (final IOException e) { System.out.println("接收消息错误:"); } } else { System.out.println("端口没有连接"); } return null; } public boolean isConnected() { return socketChannel != null && socketChannel.isConnected(); } private void select() { int n = 0; try { if (selector == null) { return; } n = selector.select(1000); } catch (final Exception e) { e.printStackTrace(); } // 如果select返回大于0,处理事件 if (n > 0) { for (final Iterator<SelectionKey> i = selector.selectedKeys() .iterator(); i.hasNext();) { // 得到下一个Key final SelectionKey sk = i.next(); i.remove(); // 检查其是否还有效 if (!sk.isValid()) { continue; } // 处理事件 final IEvent handler = (IEvent) sk.attachment(); try { if (sk.isConnectable()) { handler.connect(sk); } else if (sk.isReadable()) { handler.read(sk); } else { // System.err.println("Ooops"); } } catch (final Exception e) { handler.error(e); sk.cancel(); } } } } public void shutdown() { if (isConnected()) { try { socketChannel.close(); System.out.println("端口关闭成功"); } catch (final IOException e) { System.out.println("端口关闭错误:"); } finally { socketChannel = null; } } else { System.out.println("通道为空或者没有连接"); } } @Override public void run() {// 启动主循环流程 while (!shutdown.get()) { try { if (isConnected()&&(!isSended)) {switch (sysStatus) {case init:doOption();break;case options:doDescribe();break;case describe:doSetup();break;case setup:if(sessionid==null&&sessionid.length()>0){System.out.println("setup还没有正常返回");}else{doPlay();}break;case play:
// doPause();
// doPlay();System.out.println("PLAY start");break;case pause:doTeardown();break;default:break;}}// do selectselect(); try {Thread.sleep(1000);} catch (final Exception e) {}} catch (final Exception e) {e.printStackTrace(); } } shutdown(); } public void connect(SelectionKey key) throws IOException { if (isConnected()) { return; } // 完成SocketChannel的连接 socketChannel.finishConnect(); while (!socketChannel.isConnected()) { try { Thread.sleep(300); } catch (final InterruptedException e) { e.printStackTrace(); } socketChannel.finishConnect(); } } public void error(Exception e) { e.printStackTrace(); } public void read(SelectionKey key) throws IOException { // 接收消息 final byte[] msg = recieve(); if (msg != null) { handle(msg); } else { key.cancel(); } } private void handle(byte[] msg) { String tmp = new String(msg); System.out.println("返回内容:"); System.out.println(tmp); if (tmp.startsWith(RTSP_OK)) { switch (sysStatus) { case init: sysStatus = Status.options; break; case options: sysStatus = Status.describe; String temp =tmp.substring(tmp.indexOf("trackID"));trackInfo = temp.split("\r\n")[0];break; case describe: String tempSessionId = tmp.substring(tmp.indexOf("Session: ") + 9);sessionid = tempSessionId.split("\r\n")[0];sessionid = tempSessionId.split(";")[0];sessionid = sessionid.trim();if(sessionid!=null&&sessionid.length()>0){sysStatus = Status.setup; } break; case setup: sysStatus = Status.play; break; case play:
// sysStatus = Status.pause;break; case pause: sysStatus = Status.teardown; shutdown.set(true); break; case teardown: sysStatus = Status.init; break; default: break; } isSended=false; } else { System.out.println("返回错误:" + tmp); } } private void doTeardown() { StringBuilder sb = new StringBuilder(); sb.append("TEARDOWN "); sb.append(this.address); sb.append("/"); sb.append(VERSION);sb.append(System.lineSeparator());sb.append("Cseq: ");sb.append(seq++); sb.append(System.lineSeparator());sb.append("User-Agent: RealMedia Player HelixDNAClient/10.0.0.11279 (win32)");sb.append(System.lineSeparator());sb.append("Session: "); sb.append(sessionid);sb.append(System.lineSeparator());sb.append(System.lineSeparator());send(sb.toString().getBytes()); System.out.println(sb.toString()); }private void doPlay() { StringBuilder sb = new StringBuilder(); sb.append("PLAY "); sb.append(this.address); sb.append(VERSION);sb.append(System.lineSeparator());sb.append("Session: ");sb.append(sessionid); sb.append(System.lineSeparator());sb.append("Cseq: ");sb.append(seq++);sb.append(System.lineSeparator());sb.append("Range: npt=0.000-");sb.append(System.lineSeparator());sb.append(System.lineSeparator());System.out.println(sb.toString());send(sb.toString().getBytes()); }private void doSetup() { StringBuilder sb = new StringBuilder(); sb.append("SETUP "); sb.append(this.address); sb.append("/"); sb.append(trackInfo); sb.append(VERSION);sb.append(System.lineSeparator());sb.append("Cseq: "); sb.append(seq++); sb.append(System.lineSeparator());sb.append("Transport: ");sb.append(transport);sb.append(System.lineSeparator());sb.append(System.lineSeparator());System.out.println(sb.toString()); send(sb.toString().getBytes()); } private void doOption() { StringBuilder sb = new StringBuilder(); sb.append("OPTIONS "); sb.append(this.address.substring(0, address.lastIndexOf("/"))); sb.append(VERSION);sb.append(System.lineSeparator());sb.append("Cseq: "); sb.append(seq++); sb.append(System.lineSeparator());sb.append(System.lineSeparator());System.out.println(sb.toString()); send(sb.toString().getBytes()); } private void doDescribe() { StringBuilder sb = new StringBuilder(); sb.append("DESCRIBE ").append(this.address).append(VERSION).append(System.lineSeparator());sb.append("Cseq: ").append(seq++).append(System.lineSeparator());sb.append("Accept: application/sdp").append(System.lineSeparator());sb.append(System.lineSeparator());System.out.println(sb.toString()); send(sb.toString().getBytes()); } private void doPause() { StringBuilder sb = new StringBuilder(); sb.append("PAUSE "); sb.append(this.address); sb.append("/"); sb.append(VERSION);sb.append(System.lineSeparator());sb.append("Cseq: "); sb.append(seq++); sb.append(System.lineSeparator());sb.append("Session: "); sb.append(sessionid);sb.append(System.lineSeparator());sb.append(System.lineSeparator());send(sb.toString().getBytes()); System.out.println(sb.toString());}public static void main(String[] args) { try { // RTSPClient(InetSocketAddress remoteAddress, // InetSocketAddress localAddress, String address)// rtsp://admin:shinemo123@39.170.35.150:1554/h264/ch0/1RTSPClient client = new RTSPClient( new InetSocketAddress("39.170.35.150", 1554),new InetSocketAddress("10.1.65.48", 0),"rtsp://admin:shinemo123@39.170.35.150:1554/h264/ch0/1");client.start(); } catch (Exception e) { e.printStackTrace(); }}
}