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();  
        }

    }
}  
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
实现RTSP(Real-Time Streaming Protocol)视频点播功能,可以使用Java语言结合第三方库来完成。 首先,需要选择一个适合的RTSP库,例如使用net.sf.fmj.media.rtsp包中的RTSPURLConnection类,它提供了RTSP连接和交互的功能。 在Java中,可以使用Socket类建立与RTSP服务器的TCP连接,并通过Socket的InputStream和OutputStream发送和接收RTSP请求和响应。可以借助RTSPURLConnection类来解析RTSP响应,以获取视频流信息。 接下来,需要发送RTSP SETUP请求,通过RTSP传输控制通道(RTSP-TCP)建立媒体通道,可以选择使用RTP协议或者TCP来传输视频数据。 然后,发送RTSP PLAY请求,开始播放视频。在获取到视频数据后,可以使用FFmpeg、VLCj等库来解码和播放视频。可以利用JavaFX或者Swing等图形库创建视频播放界面,并通过Java的多线程实现实时播放。 另外,为了提供更好的用户体验,可以在播放器中添加暂停、快进、快退等功能,需要发送相应的RTSP PAUSE、FAST FORWARD、REVERSE等请求来控制视频播放。 最后,当视频播放结束或用户手动停止播放时,需要发送RTSP TEARDOWN请求来关闭媒体通道,并断开与RTSP服务器的连接。 总结来说,通过选择适合的RTSP库,建立与RTSP服务器的TCP连接,发送RTSP请求并解析响应,以及使用第三方库来解码和播放视频,就可以实现JavaRTSP视频点播功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦鸢MoYuan

谢谢投喂!!!QWQ!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值