RTSP,Java实现简单的RTSP报文交换

Posted 梦鸢MoYuan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RTSP,Java实现简单的RTSP报文交换相关的知识,希望对你有一定的参考价值。

这里写目录标题

  1. 了解RTSP协议
  2. 使用Java程序编写RTSP客户端 访问 RTSP服务端,实现拉流

RTSP协议是什么

RTSP是一种基于文本的协议,用CRLF(回车换行)作为每一行的结束符,其好处是,在使用过程中可以方便地增加自定义参数,也方便抓包分析。从消息传送方向上来分,RTSP的报文有两类:请求报文和响应报文。请求报文是指从客户端向服务器发送的请求(也有少量从服务器向客户端发送的请求),响应报文是指从服务器到客户端的回应。

RTSP请求报文的常用方法与作用

一次基本的RTSP交互过程如下,C表示客户端,S表示服务端。

  1. OPTION请求
    -> 响应
  2. DESCRIBE请求
    -> 响应
    如果响应无权限, 那么需要带上用户名密码
  3. SETUP请求
    -> 响应
  4. 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: 569

v=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 是有状态的,通讯成功后需要记录 sessionId
    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());  
    
  
    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 select
                select();  
                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/1
            RTSPClient 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();  
        

    
  

流媒体专家RTSP协议详解II

rtsp的方法有点类似于http的方法,如post,get等,大家在学习时可以做对比学习,加强理解。

本节主要介绍rtsp常用的方法,并串讲rtsp流媒体建立、控制、终止的整个过程。

1、RTSP 重要方法

RTSP请求报文的常用方法与作用:

image

img

  1. OPTIONS:

用于得到服务器提供的可用方法;

如:

OPTIONS rtsp://192.168.20.136:5000/xxx666 RTSP/1.0

CSeq: 1

服务器的回应信息会在Public字段列出提供的方法。如:

RTSP/1.0 200 OK

CSeq: 1 //每个回应消息的cseq数值和请求消息的cseq相对应

Public:

以上是关于RTSP,Java实现简单的RTSP报文交换的主要内容,如果未能解决你的问题,请参考以下文章

用 Java 实现 RTSP 媒体服务器

关于RTSP-Over-HTTP

流媒体专家RTSP协议详解II

RTPS代理与转发服务

JavaCV开发详解之26补充篇1:简单视频截图,视频文件和rtsp/rtmp/flv/hls等直播流截取一张图片

JavaCV开发详解之26补充篇1:简单视频截图,视频文件和rtsp/rtmp/flv/hls等直播流截取一张图片