当前位置:   article > 正文

大华摄像头实时预览(spring boot+websocket+flv.js)Java开发_大华摄像头预览

大华摄像头预览

开发所需


1.大华NetSDK_JAVA; 这里使用的是 Linux64的架包
2.websocket 前端使用的vue框架   
3.flv.js的播放插件    

4.大华摄像头提供的平台(后面称为官方平台)

【实时预览】流程分析

根据大华《NetSDK_JAVA编程指导手册》的流程图

根据图可以得知关键流程为:
初始化sdk——>登录设备——>打开实时预览——>设置视频流的回调函数——>发送视频流到前端

因该需求为内网开发所以需要外网开发实现的可以搜索添加相关主动注册方法来实现连接外网。

【整体流程】

1.Java后端通过NetSDK得到IPC回调的FLV流;

2.后端与前端通过websocket进行数据的传输;

3.前端通过后端转发的FLV流,使用flv.js进行解析并播放。

注:以下代码仅为方便测试写的极简版只为对第一次进行这类开发的可以进行单视频的监控预览来更好的理解进行后续开发。

代码实现:后端

一.导入相关的依赖和资源

lib包与common包可以从大华的demo中直接复制过来第一次开发可以把整个demo也复制进来部分流程直接调用demo中方法后期再剔除掉多余部分。

二.添加websocket

websocket的教程有很多就不过多陈述了。
1.导入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-websocket</artifactId>
  4. </dependency>
2.配置工具类
  1. package com.ruoyi.power.common;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.socket.server.standard.ServerEndpointExporter;
  5. import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
  6. @Configuration
  7. //@EnableWebSocket
  8. public class WebsocketConfig {
  9. @Bean
  10. public ServerEndpointExporter serverEndpoint() {
  11. return new ServerEndpointExporter();
  12. }
  13. /**
  14. * WebSocket 配置信息
  15. *
  16. * @return servletServerContainerFactoryBean
  17. */
  18. @Bean
  19. public ServletServerContainerFactoryBean createWebSocketContainer() {
  20. ServletServerContainerFactoryBean bean = new ServletServerContainerFactoryBean();
  21. // 文本缓冲区大小
  22. bean.setMaxTextMessageBufferSize(8192);
  23. // 字节缓冲区大小
  24. bean.setMaxBinaryMessageBufferSize(8192);
  25. return bean;
  26. }
  27. }
3.编写websocket类
  1. package com.ruoyi.power.service.impl;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.context.ConfigurableApplicationContext;
  5. import org.springframework.stereotype.Controller;
  6. import javax.websocket.OnClose;
  7. import javax.websocket.OnMessage;
  8. import javax.websocket.OnOpen;
  9. import javax.websocket.Session;
  10. import javax.websocket.server.ServerEndpoint;
  11. import java.io.IOException;
  12. import java.nio.ByteBuffer;
  13. import java.util.concurrent.ConcurrentHashMap;
  14. @ServerEndpoint("/ws/monitor/{device}/{channel}")
  15. @Controller
  16. public class Websocket {
  17. private static ConfigurableApplicationContext applicationContext;
  18. public static void setApplicationContext(ConfigurableApplicationContext context) {
  19. applicationContext = context;
  20. }
  21. /**
  22. * 与某个客户端的连接会话,需要通过它来给客户端发送数据
  23. */
  24. public static Session session;
  25. private static Websocket instance;
  26. /**
  27. * 与某个客户端的连接会话,需要通过它来给客户端发送数据
  28. */
  29. private static ConcurrentHashMap<Long, Session> sessions = new ConcurrentHashMap<>();
  30. private static final Logger log = LoggerFactory.getLogger(Websocket.class);
  31. /**
  32. * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
  33. */
  34. private static int onlineCount = 0;
  35. /**
  36. * 连接成功
  37. *
  38. * @param session
  39. */
  40. @OnOpen
  41. public void onOpen(Session session) {
  42. this.session = session; // 保存客户端连接的Session对象
  43. }
  44. /**
  45. * 连接关闭
  46. *
  47. * @param session
  48. */
  49. @OnClose
  50. public void onClose(Session session) {
  51. }
  52. /**
  53. * 接收到消息
  54. *
  55. * @param text
  56. */
  57. @OnMessage
  58. public String onMsg(String text) throws IOException {
  59. System.out.println("连接成功");
  60. return null;
  61. }
  62. /**
  63. * 实现服务器主动推送
  64. * @param realPlayHandler 播放拉流的回调句柄
  65. * @param buffer 发送的数据
  66. * @throws IOException
  67. */
  68. public void sendMessageToOne(long realPlayHandler, ByteBuffer buffer) throws IOException {
  69. session.getBasicRemote().sendBinary(buffer);
  70. }
  71. public static void sendBuffer(byte[] bytes, long realPlayHandler) {
  72. Websocket wsServerEndpoint = new Websocket();
  73. /**
  74. * 发送流数据
  75. * 使用pBuffer.getByteBuffer(0,dwBufSize)得到的是一个指向native pointer的ByteBuffer对象,其数据存储在native,
  76. * 而webSocket发送的数据需要存储在ByteBuffer的成员变量hb,使用pBuffer的getByteBuffer得到的ByteBuffer其hb为null
  77. * 所以,需要先得到pBuffer的字节数组,手动创建一个ByteBuffer
  78. */
  79. ByteBuffer buffer = ByteBuffer.wrap(bytes);
  80. try {
  81. wsServerEndpoint.sendMessageToOne(realPlayHandler, buffer);
  82. } catch (IOException e) {
  83. throw new RuntimeException(e);
  84. }
  85. }
  86. }

websocket的主要作用是将拉流成功后的视频流回调数据发送给前端由flv.js处理成视频呈现。

4.websocket调用其他类

这里注意如果websocket类中想要调用项目其他类内的方法需要再运行类中配置。

  1. public static void main(String[] args) {
  2. // System.setProperty("spring.devtools.restart.enabled", "false");
  3. ConfigurableApplicationContext run = SpringApplication.run(RuoYiApplication.class, args);
  4. Websocket.setApplicationContext(run);
  5. System.out.println("(♥◠‿◠)ノ゙ 项目启动成功 ლ(´ڡ`ლ)゙ \n");
  6. }

三.初始化sdk 并登录用户


按照大华示例,进行先观摩demo中LoginModule的编写,可以复制或直接调用相关方法或者自己按需编写。

1.初始化
  1. /**
  2. * 初始化
  3. * @param disConnect 断线时的回调函数
  4. * @param haveReConnect 断线时的回调用户参数为null时不会回调给用户
  5. * @return
  6. */
  7. public static boolean init(NetSDKLib.fDisConnect disConnect, NetSDKLib.fHaveReConnect haveReConnect) {
  8. bInit = netsdk.CLIENT_Init(disConnect, null);
  9. if(!bInit) {
  10. System.out.println("Initialize SDK failed");
  11. return false;
  12. }
  13. //打开日志,可选
  14. NetSDKLib.LOG_SET_PRINT_INFO setLog = new NetSDKLib.LOG_SET_PRINT_INFO();
  15. File path = new File("./sdklog/");
  16. if (!path.exists()) {
  17. path.mkdir();
  18. }
  19. String logPath = path.getAbsoluteFile().getParent() + "\\sdklog\\" + ToolKits.getDate() + ".log";
  20. setLog.nPrintStrategy = 0;
  21. setLog.bSetFilePath = 1;
  22. System.arraycopy(logPath.getBytes(), 0, setLog.szLogFilePath, 0, logPath.getBytes().length);
  23. System.out.println(logPath);
  24. setLog.bSetPrintStrategy = 1;
  25. bLogopen = netsdk.CLIENT_LogOpen(setLog);
  26. if(!bLogopen ) {
  27. System.err.println("Failed to open NetSDK log");
  28. }
  29. // 设置断线重连回调接口,设置过断线重连成功回调函数后,当设备出现断线情况,SDK内部会自动进行重连操作
  30. // 此操作为可选操作,但建议用户进行设置
  31. netsdk.CLIENT_SetAutoReconnect(haveReConnect, null);
  32. //设置登录超时时间和尝试次数,可选
  33. int waitTime = 5000; //登录请求响应超时时间设置为5S
  34. int tryTimes = 1; //登录时尝试建立链接1次
  35. netsdk.CLIENT_SetConnectTime(waitTime, tryTimes);
  36. // 设置更多网络参数,NET_PARAM的nWaittime,nConnectTryNum成员与CLIENT_SetConnectTime
  37. // 接口设置的登录设备超时时间和尝试次数意义相同,可选
  38. NetSDKLib.NET_PARAM netParam = new NetSDKLib.NET_PARAM();
  39. netParam.nConnectTime = 10000; // 登录时尝试建立链接的超时时间
  40. netParam.nGetConnInfoTime = 3000; // 设置子连接的超时时间
  41. netParam.nGetDevInfoTime = 3000;//获取设备信息超时时间,为0默认1000ms
  42. netsdk.CLIENT_SetNetworkParam(netParam);
  43. return true;
  44. }

综上代码可看出初始化关键部分为bInit = netsdk.CLIENT_Init(disConnect, null);这句代码进行初始话如无回调需求直接参数都为null即可。

2.设备登录
  1. /**
  2. * 登录设备
  3. * @param m_strIp 地址ip如:192.168.2.100(这里为你摄像头官方给的平台ip)
  4. * @param m_nPort 端口37777 (同为你摄像头官方给的平台的端口)
  5. * @param m_strUser 你的用户名
  6. * @param m_strPassword 密码
  7. * @return
  8. */
  9. public static boolean login(String m_strIp, int m_nPort, String m_strUser, String m_strPassword) {
  10. //IntByReference nError = new IntByReference(0);
  11. //入参
  12. NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY pstInParam=new NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY();
  13. pstInParam.nPort=m_nPort;
  14. pstInParam.szIP=m_strIp.getBytes();
  15. pstInParam.szPassword=m_strPassword.getBytes();
  16. pstInParam.szUserName=m_strUser.getBytes();
  17. //出参
  18. NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY pstOutParam=new NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY();
  19. pstOutParam.stuDeviceInfo=m_stDeviceInfo;
  20. //m_hLoginHandle = netsdk.CLIENT_LoginEx2(m_strIp, m_nPort, m_strUser, m_strPassword, 0, null, m_stDeviceInfo, nError);
  21. m_hLoginHandle=netsdk.CLIENT_LoginWithHighLevelSecurity(pstInParam, pstOutParam);
  22. if(m_hLoginHandle.longValue() == 0) {
  23. System.err.printf("Login Device[%s] Port[%d]Failed. %s\n", m_strIp, m_nPort, ToolKits.getErrorCodePrint());
  24. } else {
  25. System.out.println("Login Success [ " + m_strIp + " ]");
  26. }
  27. return m_hLoginHandle.longValue() == 0? false:true;
  28. }

四.拉取实时预览的视频流

同样先看demo中的Realpaly的启动类通过断点来了解其运行的顺序和各功能下的实现方法后

找出此段对视频进行拉流的具体操作。

  1. public void realplay(){
  2. lRealHandle= netSdk.CLIENT_RealPlayEx(loginHandle, 0, null, 0);
  3. if(lRealHandle.longValue()!=0){
  4. System.out.println("realplay success");
  5. netSdk.CLIENT_SetRealDataCallBackEx(lRealHandle, CbfRealDataCallBackEx.getInstance(),null, 31);
  6. }
  7. }

但是要注意的是 netSdk.CLIENT_RealPlayEx的拉流为默认的视频流格式或许纯在不适用与flv.js情况所以根据需要用到另一种自定义设置回调流格式的预览方法接口。

该方法位于官方所提供的sdk类中但并没有在说明文档中具体解释下面是该接口的主观使用方法
这里我们需要将:

回调的数据类型选择为流式FLV

通道号:可以在官方平台下查看什么通道号;

而登录的回调函数是之前登录方法的 m_hLoginHandle=netsdk.CLIENT_LoginWithHighLevelSecurity(pstInParam, pstOutParam);该接口的回调数据 m_hLoginHandle

  1. /**
  2. * 开始实时预览
  3. * @param m_hLoginHandle 登录句柄
  4. * @param nChannelID 通道ID
  5. * @param rType 码流类型 ,参考 NET_RealPlayType
  6. * 0 // 实时预览
  7. * 3 // 实时预览-从码流1
  8. * 7 // 多画面预览-4画面
  9. * 9 // 多画面预览-9画面
  10. * 10 // 多画面预览-16画面
  11. * @param emDataType 回调的数据类型,详见 EM_REAL_DATA_TYPE
  12. * 0; // 私有码流
  13. * 1; // 国标PS码流
  14. * 2; // TS码流
  15. * 3; // MP4文件
  16. * 4; // 裸H264码流
  17. * 5; // 流式FLV`
  18. * @return 预览句柄
  19. */
  20. public static NetSDKLib.LLong startRealPlay(NetSDKLib.LLong m_hLoginHandle,
  21. int nChannelID, int rType, int emDataType) {
  22. NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE inParam = new NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE();
  23. NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE outParam = new NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE();
  24. inParam.nChannelID = nChannelID;
  25. inParam.rType = rType;
  26. inParam.emDataType = emDataType;
  27. NetSDKLib.LLong lRealHandle = netsdk.CLIENT_RealPlayByDataType(m_hLoginHandle,
  28. inParam, outParam, 3000);
  29. }

当然关于inParam 的所有参数使用还需要根据sdk中的这段代码具体理解

  1. // 开始实时预览并指定回调数据格式入参
  2. public static class NET_IN_REALPLAY_BY_DATA_TYPE extends SdkStructure
  3. {
  4. public int dwSize; // 结构体大小
  5. public int nChannelID; // 通道编号
  6. public Pointer hWnd; // 窗口句柄, HWND类型
  7. public int rType; // 码流类型 ,参考 NET_RealPlayType
  8. public fRealDataCallBackEx cbRealData; // 数据回调函数
  9. public int emDataType; // 回调的数据类型,参考 EM_REAL_DATA_TYPE
  10. public Pointer dwUser; // 用户数据
  11. public String szSaveFileName; // 转换后的文件名
  12. public fRealDataCallBackEx2 cbRealDataEx; // 数据回调函数-扩展
  13. public int emAudioType; // 音频格式,对应枚举EM_AUDIO_DATA_TYPE
  14. public Callback cbRealDataEx2;// 数据回调(扩展带时间戳,帧类型),使用fDataCallBackEx
  15. public NET_IN_REALPLAY_BY_DATA_TYPE() {
  16. this.dwSize = this.size();
  17. }
  18. }

五.设置回调的函数接口

在拉取流判断成功后我们需要去接受摄像机向我们发送过来的视频流,而这需要一个对应的接口而设置方法如下:

代码实现为:

  1. /**
  2. * 开始实时预览
  3. * @param m_hLoginHandle 登录句柄
  4. * @param nChannelID 通道ID
  5. * @param rType 码流类型 ,参考 NET_RealPlayType
  6. * 0 // 实时预览
  7. * 3 // 实时预览-从码流1
  8. * 7 // 多画面预览-4画面
  9. * 9 // 多画面预览-9画面
  10. * 10 // 多画面预览-16画面
  11. * @param emDataType 回调的数据类型,详见 EM_REAL_DATA_TYPE
  12. * 0; // 私有码流
  13. * 1; // 国标PS码流
  14. * 2; // TS码流
  15. * 3; // MP4文件
  16. * 4; // 裸H264码流
  17. * 5; // 流式FLV`
  18. * @return 预览句柄
  19. */
  20. public static NetSDKLib.LLong startRealPlay(NetSDKLib.LLong m_hLoginHandle,
  21. int nChannelID, int rType, int emDataType) {
  22. NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE inParam = new NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE();
  23. NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE outParam = new NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE();
  24. inParam.nChannelID = nChannelID;
  25. inParam.rType = rType;
  26. inParam.emDataType = emDataType;
  27. NetSDKLib.LLong lRealHandle = netsdk.CLIENT_RealPlayByDataType(m_hLoginHandle,
  28. inParam, outParam, 3000);
  29. if (lRealHandle.longValue() != 0) {
  30. System.out.println("拉取预览成功 success" + lRealHandle);
  31. //设置回调的接收
  32. //lRealHandle :拉流成功的句柄
  33. //你的接收回调的类里的方法 注:这里使用的是官方demo中的RealplayEx类下的接口
  34. netsdk.CLIENT_SetRealDataCallBackEx(lRealHandle, RealplayEx.CbfRealDataCallBackEx.getInstance(),
  35. null, 31);
  36. return lRealHandle;
  37. } else {
  38. return null;
  39. }
  40. }

六.接收视频流并推送给前端

该方法也在RealplayEx.java内有模板。

1.接收视频流
  1. /**
  2. * 实时预览数据回调函数--扩展(pBuffer内存由SDK内部申请释放)
  3. */
  4. private static class CbfRealDataCallBackEx implements NetSDKLib.fRealDataCallBackEx {
  5. private CbfRealDataCallBackEx() {
  6. }
  7. private static class CallBackHolder {
  8. private static CbfRealDataCallBackEx instance = new CbfRealDataCallBackEx();
  9. }
  10. public static CbfRealDataCallBackEx getInstance() {
  11. return CallBackHolder.instance;
  12. }
  13. @Override
  14. public void invoke(LLong lRealHandle, int dwDataType, Pointer pBuffer,
  15. int dwBufSize, int param, Pointer dwUser) {
  16. int bInput=0;
  17. if(0 != lRealHandle.longValue())
  18. {
  19. switch(dwDataType) {
  20. case 0:
  21. System.out.println("码流大小为" + dwBufSize + "\n" + "码流类型为原始音视频混合数据");
  22. break;
  23. case 1:
  24. //标准视频数据
  25. break;
  26. case 2:
  27. //yuv 数据
  28. break;
  29. case 3:
  30. //pcm 音频数据
  31. break;
  32. case 4:
  33. //原始音频数据
  34. break;
  35. default:
  36. break;
  37. }
  38. }
  39. }
  40. }

在我们开启实时预览后摄像机会把视频流发送给这个方法中其中获得的参数有:

lRealHandle  代表该条流是哪个拉流的回调  ;

dwDataType  回调的视频流的格式是什么前文设置为flv的视频流;

pBuffer  视频流的具体数据;

dwBufSize 视频流的大小;

在知道了这些之后再来对他进行改写为我们需要的形式。

  1. /**
  2. * 实时预览数据回调函数--扩展(pBuffer内存由SDK内部申请释放)
  3. */
  4. public static class CbfRealDataCallBackEx implements NetSDKLib.fRealDataCallBackEx {
  5. @Autowired
  6. private WsServerEndpoint server;
  7. private CbfRealDataCallBackEx() {
  8. }
  9. private static class CallBackHolder {
  10. private static CbfRealDataCallBackEx instance = new CbfRealDataCallBackEx();
  11. }
  12. public static CbfRealDataCallBackEx getInstance() {
  13. return CallBackHolder.instance;
  14. }
  15. @Override
  16. public void invoke(LLong lRealHandle, int dwDataType, Pointer pBuffer,
  17. int dwBufSize, int param, Pointer dwUser) {
  18. //将内容转换为字节数组
  19. byte[] buffer = pBuffer.getByteArray(0, dwBufSize);
  20. if ((dwDataType - 1000) == 5) {//回调格式为flv的流
  21. //通过websocket发送
  22. server.sendBuffer(buffer, lRealHandle.longValue());
  23. }
  24. }
  25. }
2.websocket向前端发送
  1. public static void sendBuffer(byte[] bytes, long realPlayHandler) {
  2. Websocket wsServerEndpoint = new Websocket();
  3. /**
  4. * 发送流数据
  5. * 使用pBuffer.getByteBuffer(0,dwBufSize)得到的是一个指向native pointer的ByteBuffer对象,其数据存储在native,
  6. * 而webSocket发送的数据需要存储在ByteBuffer的成员变量hb,使用pBuffer的getByteBuffer得到的ByteBuffer其hb为null
  7. * 所以,需要先得到pBuffer的字节数组,手动创建一个ByteBuffer
  8. */
  9. ByteBuffer buffer = ByteBuffer.wrap(bytes);
  10. try {
  11. wsServerEndpoint.sendMessageToOne(realPlayHandler, buffer);
  12. } catch (IOException e) {
  13. throw new RuntimeException(e);
  14. }
  15. }
  1. /**
  2. * 实现服务器主动推送
  3. */
  4. public void sendMessageToOne(long realPlayHandler, ByteBuffer buffer) throws IOException {
  5. if (realPlayHandler == 0) {
  6. log.error("loginHandler is invalid.please check.", this);
  7. return;
  8. }
  9. Session session = sessions.get(realPlayHandler);
  10. if (session != null && session.isOpen()) { // 确保session不为null
  11. synchronized (session) {
  12. try {
  13. System.out.println(buffer);
  14. session.getBasicRemote().sendBinary(buffer);
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }else {
  20. log.error("session is null.please check.", this);
  21. }
  22. }

至此后端拉流推流已经完成,但是在断开连接时也要记得停止实时预览的拉流和释放sdk详情产靠官方文档中有介绍

代码实现:前端

使用的是vue的框架结构

vue项目引入flv.js。
npm install --save flv.js
main.js里面引入
import flvjs from ‘flv.js’;
Vue.use(flvjs)

1.js

(1):导入
import flvjs from "flv.js/dist/flv.js";
(2):使用flv.js实现播放flv格式流,获取video节点
  1. videoElement = this.$refs.videoElement
  2. if (flvjs.isSupported()) {
  3. flvPlayer = flvjs.createPlayer({
  4. type: 'flv', //媒体类型
  5. url: 'ws://127.0.0.1:6102/ws/monitor/1/0' //flv格式媒体URL
  6. isLive: true, //数据源是否为直播流
  7. hasAudio: false, //数据源是否包含有音频
  8. hasVideo: true, //数据源是否包含有视频
  9. enableStashBuffer: false //是否启用缓存区
  10. },{
  11. enableWorker: false, //不启用分离线程
  12. enableStashBuffer: false, //关闭IO隐藏缓冲区
  13. autoCleanupSourceBuffer: true //自动清除缓存
  14. });
  15. flvPlayer.attachMediaElement(videoElement); //将播放实例注册到节点
  16. flvPlayer.load(); //加载数据流
  17. flvPlayer.play(); //播放数据流
  18. }
(3):关闭视频流
  1. flvPlayer.pause(); //暂停播放数据流
  2. flvPlayer.unload(); //取消数据流加载
  3. flvPlayer.detachMediaElement(); //将播放实例从节点中取出
  4. flvPlayer.destroy(); //销毁播放实例

2.html

  1. <div class="row" style=" height:500px;width: auto;background-color: #1c84c6">
  2. <video ref="videoElement"
  3. class="centeredVideo"
  4. id="myPlayer"
  5. preload="auto"
  6. type="rtmp/flv"
  7. controls
  8. autoplay
  9. muted
  10. style="width: 100%;height: 100%"
  11. @click="handleClick"
  12. ></video>
  13. </div>

成果展示

功能补充

flv.js的接口介绍文档

flv.js的追帧、断流重连及实时更新的直播优化方案

最后的最后如果你的视频还是不能播放比如显示格式不正确获得的流不是标准flv流时注意检查官方平台下一定要把编码模式改为H.264

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/143491
推荐阅读
相关标签
  

闽ICP备14008679号