├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── notice ├── rtsp-websocket-starter.jpg ├── rtsp-websocket.png └── structure.png ├── pom.xml ├── rtsp-websocket-server-sample ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── xingshuangs │ │ │ └── rtsp │ │ │ └── server │ │ │ ├── RtspWebsocketServerSampleApplication.java │ │ │ ├── config │ │ │ └── WebSocketConfig.java │ │ │ └── controller │ │ │ ├── PageController.java │ │ │ └── WebSocketServer.java │ └── resources │ │ ├── application.yml │ │ ├── static │ │ └── rtspStream.js │ │ └── templates │ │ └── client.html │ └── test │ └── java │ └── com │ └── github │ └── xingshuangs │ └── rtsp │ └── server │ └── RtspWebsocketServerSampleApplicationTests.java └── rtsp-websocket-server-starter ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── github │ │ └── xingshuangs │ │ └── rtsp │ │ └── starter │ │ ├── RtspWebsocketServerStarterApplication.java │ │ ├── config │ │ ├── PropertiesConfig.java │ │ └── WebsocketConfig.java │ │ ├── controller │ │ ├── PageController.java │ │ └── RtspController.java │ │ ├── enums │ │ └── ERtspMessageType.java │ │ ├── model │ │ ├── RtspAddress.java │ │ ├── RtspConnection.java │ │ ├── RtspMessage.java │ │ └── WebsocketConnection.java │ │ ├── properties │ │ └── RtspProperties.java │ │ └── service │ │ ├── RtspManager.java │ │ └── RtspWebsocketHandler.java └── resources │ ├── application.yml │ ├── static │ └── rtspStream.js │ └── templates │ └── client.html └── test └── java └── com └── github └── xingshuangs └── rtsp └── starter ├── RtspWebsocketServerStarterApplicationTests.java └── model └── RtspMessageTest.java /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/modules.xml 8 | .idea/jarRepositories.xml 9 | .idea/compiler.xml 10 | .idea/libraries/ 11 | *.iws 12 | *.iml 13 | *.ipr 14 | 15 | ### Eclipse ### 16 | .apt_generated 17 | .classpath 18 | .factorypath 19 | .project 20 | .settings 21 | .springBeans 22 | .sts4-cache 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | build/ 31 | !**/src/main/**/build/ 32 | !**/src/test/**/build/ 33 | /logs 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | ### Mac OS ### 39 | .DS_Store 40 | /*/target/ 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2099 Oscura (xingshuang) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RTSP-WEBSOCKET-SERVER 2 | 3 | ![Language-java8](https://img.shields.io/badge/Language-java8-blue) 4 | ![SpringBoot-2.3.4.RELEASE](https://img.shields.io/badge/SpringBoot-2.3.4.RELEASE-yellow) 5 | ![Idea-2022.02.03](https://img.shields.io/badge/Idea-2022.02.03-lightgrey) 6 | ![CopyRight-Oscura](https://img.shields.io/badge/CopyRight-Oscura-yellow) 7 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE) 8 | 9 | ## 简述 10 | 11 | - 技术结构 **RTSP + H264 + FMP4 + WebSocket + MSE + WEB** 12 | - 目前支持**海康、大华摄像头**RTSP视频流在WEB页面上显示,亲测有效 13 | - 视频流获取支持**TCP/UDP**两种方式,任意切换 14 | - **纯JAVA**开发,没有任何其他依赖,**无插件**,**轻量级**,还可以定制化扩展开发 15 | - 视频响应快速,**延时 < 1s**,几乎**无延时**,**实时性强**,即开即用 16 | - 采用的通信库: https://github.com/xingshuangs/iot-communication 17 | 18 | ## 整体结构 19 | 20 | Camera ==> JAVA Server(Proxy) ==> HTML5 Page. 21 | 22 | ![structure.png](https://i.postimg.cc/bw5ZJqGP/structure.png) 23 | 24 | ## 使用指南 25 | 26 | ### 1. rtsp-websocket-server-sample(rtsp地址模式) 27 | 28 | 1. jar包启动 或 IDEA启动 29 | 2. 登录访问地址:http://127.0.0.1:8088 30 | 3. 输入正确的摄像头RTSP地址 31 | 4. 点击页面上的打开按钮 32 | 33 | ![rtsp-websocket.png](https://i.postimg.cc/vBZzrGQB/rtsp-websocket.png) 34 | 35 | ### 2. rtsp-websocket-server-starter(订阅模式) 36 | 37 | 先在配置文件中配置RTSP的访问地址 38 | 39 | ```text 40 | rtsp: 41 | addresses: 42 | - number: 1001 43 | url: rtsp://admin:123456@192.168.3.251:554/h264/ch1/main/av_stream 44 | - number: 1002 45 | url: rtsp://admin:123456@192.168.3.250:554/h264/ch1/main/av_stream 46 | ``` 47 | 48 | 1. jar包启动 或 IDEA启动 49 | 2. 登录访问地址:http://127.0.0.1:8089 50 | 3. 点击websocket的连接 51 | 4. 选择对应的视频通道,点击订阅 52 | 53 | ![rtsp-websocket-starter.png](https://i.postimg.cc/Yqk1SF4v/rtsp-websocket-starter.jpg) 54 | 55 | ## 联系方式 56 | 57 | 如果有任何问题,可以通过以下方式联系作者,作者在空余时间会做解答。 58 | 59 | - QQ群:**759101350** 60 | - 邮件:**xingshuang_cool@163.com** 61 | 62 | ## 许可证 63 | 64 | 根据MIT许可证发布,更多信息请参见[`LICENSE`](./LICENSE)。
65 | @2019 - 2099 Oscura版权所有。 66 | 67 | ## 赞助 68 | 69 | 一杯奶茶足矣
70 | **微信** (请备注上你的姓名)
71 | ![微信](https://i.postimg.cc/brBG5vx8/image.png) -------------------------------------------------------------------------------- /notice/rtsp-websocket-starter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xingshuangs/rtsp-websocket-server/0fa61a788a3677b26f44c8dab95a37b4ff7b18e9/notice/rtsp-websocket-starter.jpg -------------------------------------------------------------------------------- /notice/rtsp-websocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xingshuangs/rtsp-websocket-server/0fa61a788a3677b26f44c8dab95a37b4ff7b18e9/notice/rtsp-websocket.png -------------------------------------------------------------------------------- /notice/structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xingshuangs/rtsp-websocket-server/0fa61a788a3677b26f44c8dab95a37b4ff7b18e9/notice/structure.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.github.xingshuangs 6 | rtsp-websocket-server 7 | 1.0-SNAPSHOT 8 | pom 9 | 10 | rtsp-websocket-server 11 | RTSP的websocket服务器 12 | https://github.com/xingshuangs/rtsp-websocket-server 13 | 14 | 15 | rtsp-websocket-server-sample 16 | rtsp-websocket-server-starter 17 | 18 | 19 | -------------------------------------------------------------------------------- /rtsp-websocket-server-sample/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.3.4.RELEASE 9 | 10 | 11 | com.github.xingshuangs 12 | rtsp-websocket-server-sample 13 | 0.0.1-SNAPSHOT 14 | rtsp-websocket-server-sample 15 | rtsp-websocket-server-sample 16 | https://github.com/xingshuangs/rtsp-websocket-server 17 | 18 | 1.8 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-test 24 | test 25 | 26 | 27 | 28 | 29 | org.projectlombok 30 | lombok 31 | 1.18.26 32 | provided 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-thymeleaf 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-web 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-websocket 47 | 48 | 49 | 50 | com.github.xingshuangs 51 | iot-communication 52 | 1.5.3 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-maven-plugin 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /rtsp-websocket-server-sample/src/main/java/com/github/xingshuangs/rtsp/server/RtspWebsocketServerSampleApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.server; 26 | 27 | import org.springframework.boot.SpringApplication; 28 | import org.springframework.boot.autoconfigure.SpringBootApplication; 29 | 30 | @SpringBootApplication 31 | public class RtspWebsocketServerSampleApplication { 32 | 33 | public static void main(String[] args) { 34 | SpringApplication.run(RtspWebsocketServerSampleApplication.class, args); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /rtsp-websocket-server-sample/src/main/java/com/github/xingshuangs/rtsp/server/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.server.config; 26 | 27 | 28 | import org.springframework.context.annotation.Bean; 29 | import org.springframework.context.annotation.Configuration; 30 | import org.springframework.web.socket.server.standard.ServerEndpointExporter; 31 | 32 | /** 33 | * @author xingshuang 34 | */ 35 | @Configuration 36 | public class WebSocketConfig { 37 | @Bean 38 | public ServerEndpointExporter serverEndpointExporter() { 39 | return new ServerEndpointExporter(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /rtsp-websocket-server-sample/src/main/java/com/github/xingshuangs/rtsp/server/controller/PageController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.server.controller; 26 | 27 | 28 | import org.springframework.stereotype.Controller; 29 | import org.springframework.web.bind.annotation.RequestMapping; 30 | 31 | /** 32 | * @author xingshuang 33 | */ 34 | @Controller 35 | public class PageController { 36 | 37 | @RequestMapping("") 38 | public String client() { 39 | return "client"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /rtsp-websocket-server-sample/src/main/java/com/github/xingshuangs/rtsp/server/controller/WebSocketServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.server.controller; 26 | 27 | import com.github.xingshuangs.iot.protocol.rtsp.authentication.DigestAuthenticator; 28 | import com.github.xingshuangs.iot.protocol.rtsp.authentication.UsernamePasswordCredential; 29 | import com.github.xingshuangs.iot.protocol.rtsp.enums.ERtspTransportProtocol; 30 | import com.github.xingshuangs.iot.protocol.rtsp.service.RtspClient; 31 | import com.github.xingshuangs.iot.protocol.rtsp.service.RtspFMp4Proxy; 32 | import lombok.extern.slf4j.Slf4j; 33 | import org.springframework.stereotype.Component; 34 | 35 | import javax.websocket.*; 36 | import javax.websocket.server.ServerEndpoint; 37 | import java.io.IOException; 38 | import java.net.URI; 39 | import java.nio.ByteBuffer; 40 | import java.util.concurrent.ConcurrentHashMap; 41 | 42 | /** 43 | * WS代理 44 | * 45 | * @author xingshuang 46 | */ 47 | @Slf4j 48 | @Component 49 | @ServerEndpoint("/rtsp") 50 | public class WebSocketServer { 51 | 52 | /** 53 | * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识 54 | */ 55 | private static final ConcurrentHashMap SESSION_MAP = new ConcurrentHashMap<>(); 56 | 57 | /** 58 | * RTSP+FMP4的代理服务器 59 | */ 60 | private RtspFMp4Proxy rtspFMp4Proxy; 61 | 62 | /** 63 | * 连接建立成功调用的方法 64 | * 65 | * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据 66 | */ 67 | @OnOpen 68 | public void onOpen(Session session) { 69 | SESSION_MAP.put(session.getId(), session); 70 | log.info("有新连接加入!,当前在线人数为{},sessionId={}", SESSION_MAP.size(), session.getId()); 71 | } 72 | 73 | /** 74 | * 连接关闭调用的方法 75 | * 76 | * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据 77 | */ 78 | @OnClose 79 | public void onClose(Session session) { 80 | //从map中删除 81 | SESSION_MAP.remove(session.getId()); 82 | this.closeRtspFmp4Proxy(); 83 | log.info("有一连接关闭!,当前在线人数为{},sessionId={}", SESSION_MAP.size(), session.getId()); 84 | } 85 | 86 | @OnMessage 87 | public void onMessage(String message, Session session) { 88 | if (!message.startsWith("rtsp://")) { 89 | log.error("只支持rtsp://开头的消息,来自客户端的消息:{},sessionId={}", message, session.getId()); 90 | try { 91 | session.close(); 92 | } catch (IOException e) { 93 | log.error(e.getMessage(), e); 94 | } 95 | return; 96 | } 97 | log.info("来自客户端的消息:{},sessionId={}", message, session.getId()); 98 | this.openRtspFmp4Proxy(message, session); 99 | } 100 | 101 | /** 102 | * 打开FMP4代理 103 | * 104 | * @param message RTSP地址 105 | * @param session session 106 | */ 107 | private void openRtspFmp4Proxy(String message, Session session) { 108 | // 关闭之前的代理 109 | this.closeRtspFmp4Proxy(); 110 | try { 111 | URI srcUri = URI.create(message); 112 | int i = message.indexOf("@"); 113 | URI uri = i < 0 ? srcUri : URI.create("rtsp://" + message.substring(i + 1)); 114 | 115 | DigestAuthenticator authenticator = null; 116 | if (srcUri.getUserInfo() != null) { 117 | UsernamePasswordCredential credential = UsernamePasswordCredential.createBy(srcUri.getUserInfo()); 118 | authenticator = new DigestAuthenticator(credential); 119 | } 120 | RtspClient client = new RtspClient(uri, authenticator, ERtspTransportProtocol.UDP); 121 | this.rtspFMp4Proxy = new RtspFMp4Proxy(client); 122 | this.rtspFMp4Proxy.onFmp4DataHandle(x -> { 123 | ByteBuffer wrap = ByteBuffer.wrap(x); 124 | try { 125 | if (session.isOpen()) { 126 | session.getBasicRemote().sendBinary(wrap); 127 | } 128 | } catch (IOException e) { 129 | log.error(e.getMessage(), e); 130 | } 131 | }); 132 | this.rtspFMp4Proxy.onCodecHandle(x -> { 133 | try { 134 | session.getBasicRemote().sendText(x); 135 | } catch (IOException e) { 136 | log.error(e.getMessage(), e); 137 | } 138 | }); 139 | this.rtspFMp4Proxy.onDestroyHandle(() -> this.closeSession(session)); 140 | this.rtspFMp4Proxy.start(); 141 | } catch (Exception e) { 142 | log.error(e.getMessage(), e); 143 | this.closeRtspFmp4Proxy(); 144 | this.closeSession(session); 145 | } 146 | } 147 | 148 | /** 149 | * 关闭代理 150 | */ 151 | private void closeRtspFmp4Proxy() { 152 | if (this.rtspFMp4Proxy != null) { 153 | this.rtspFMp4Proxy.stop(); 154 | this.rtspFMp4Proxy = null; 155 | } 156 | } 157 | 158 | /** 159 | * 关闭session 160 | * 161 | * @param session 会话 162 | * @return true 163 | */ 164 | private boolean closeSession(Session session) { 165 | if (session.isOpen()) { 166 | try { 167 | session.close(); 168 | } catch (IOException e) { 169 | log.error(e.getMessage(), e); 170 | } 171 | } 172 | return true; 173 | } 174 | 175 | /** 176 | * 发生错误时调用 177 | * 178 | * @param session session对话 179 | * @param error 错误消息 180 | */ 181 | @OnError 182 | public void onError(Session session, Throwable error) { 183 | log.error("发生错误,sessionId={},错误信息={}", session.getId(), error.getMessage()); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /rtsp-websocket-server-sample/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8088 3 | 4 | spring: 5 | mvc: 6 | static-path-pattern: /static/** 7 | 8 | logging: 9 | pattern: 10 | file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%15thread] %-50.50(%logger{39}.%method:%-3line) - %msg%n" 11 | console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%15thread] %-50.50(%logger{39}.%method:%-3line) - %msg%n" 12 | file: 13 | name: ./logs/rtsp-websocket-server-sample/rtsp-websocket-server-sample.log 14 | level: 15 | com.github.xingshuangs: debug -------------------------------------------------------------------------------- /rtsp-websocket-server-sample/src/main/resources/static/rtspStream.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | class RtspStream { 26 | 27 | constructor(wsUrl, rtspUrl, videoId) { 28 | this.wsUrl = wsUrl; 29 | this.rtspUrl = rtspUrl; 30 | this.videoId = videoId; 31 | this.queue = []; 32 | this.canFeed = false; 33 | this.lastTime = new Date(); 34 | } 35 | 36 | onopen(evt) { 37 | console.log("ws连接成功") 38 | this.websocket.send(this.rtspUrl) 39 | } 40 | 41 | onClose(evt) { 42 | console.log("ws连接关闭") 43 | } 44 | 45 | onMessage(evt) { 46 | if (typeof (evt.data) == "string") { 47 | this.init(evt.data) 48 | } else { 49 | const data = new Uint8Array(evt.data); 50 | // console.log(data) 51 | this.queue.push(data); 52 | if (this.canFeed) this.feedNext(); 53 | } 54 | } 55 | 56 | onError(evt) { 57 | console.log("ws连接错误") 58 | } 59 | 60 | open() { 61 | if (this.websocket) this.websocket.close(); 62 | 63 | this.websocket = new WebSocket(this.wsUrl); 64 | this.websocket.binaryType = "arraybuffer"; 65 | this.websocket.onopen = this.onopen.bind(this); 66 | this.websocket.onmessage = this.onMessage.bind(this); 67 | this.websocket.onclose = this.onClose.bind(this); 68 | this.websocket.onerror = this.onError.bind(this); 69 | this.queue = []; 70 | this.canFeed = false; 71 | } 72 | 73 | close() { 74 | if (this.websocket) this.websocket.close(); 75 | } 76 | 77 | /** 78 | * 初始化 79 | * @param codecStr 编解码信息 80 | */ 81 | init(codecStr) { 82 | this.codec = 'video/mp4; codecs=\"' + codecStr + '\"'; 83 | console.log("call play:", this.codec); 84 | if (MediaSource.isTypeSupported(this.codec)) { 85 | this.mediaSource = new MediaSource; 86 | this.mediaSource.addEventListener('sourceopen', this.onMediaSourceOpen.bind(this)); 87 | this.mediaPlayer = document.getElementById(this.videoId); 88 | this.mediaPlayer.src = URL.createObjectURL(this.mediaSource); 89 | } else { 90 | console.log("Unsupported MIME type or codec: ", +this.codec); 91 | } 92 | } 93 | 94 | /** 95 | * MediaSource已打开事件 96 | * @param e 事件 97 | */ 98 | onMediaSourceOpen(e) { 99 | // URL.revokeObjectURL 主动释放引用 100 | URL.revokeObjectURL(this.mediaPlayer.src); 101 | this.mediaSource.removeEventListener('sourceopen', this.onMediaSourceOpen.bind(this)); 102 | 103 | // console.log("MediaSource已打开") 104 | this.sourceBuffer = this.mediaSource.addSourceBuffer(this.codec); 105 | this.sourceBuffer.addEventListener('about', e => console.log(`about `, e)); 106 | this.sourceBuffer.addEventListener('error', e => console.log(`error `, e)); 107 | this.sourceBuffer.addEventListener('updateend', e => { 108 | this.removeBuffer(); 109 | this.processDelay(); 110 | this.canFeed = true; 111 | this.feedNext(); 112 | }); 113 | this.canFeed = true; 114 | } 115 | 116 | /** 117 | * 喂数据 118 | * append的时候遇到The HTMLMediaElement.error attribute is not null就是数据时间戳有问题 119 | */ 120 | feedNext() { 121 | if (!this.queue || !this.queue.length) return 122 | if (!this.sourceBuffer || this.sourceBuffer.updating) return; 123 | if (!this.canFeed) return; 124 | 125 | // const now = new Date(); 126 | // if (now.getTime() - this.lastTime.getTime() > 120 * 1000) { 127 | // console.log("喂数据进行中", now, this.queue.length, this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length - 1)); 128 | // this.lastTime = now; 129 | // } 130 | 131 | try { 132 | const data = this.queue.shift(); 133 | this.sourceBuffer.appendBuffer(data); 134 | this.canFeed = false; 135 | } catch (e) { 136 | console.log(e); 137 | this.reset(); 138 | } 139 | } 140 | 141 | /** 142 | * 处理延时或画面卡主 143 | */ 144 | processDelay() { 145 | if (!this.sourceBuffer || !this.sourceBuffer.buffered.length || this.sourceBuffer.updating) return; 146 | 147 | const end = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length - 1); 148 | const current = this.mediaPlayer.currentTime; 149 | // 解决延迟并防止画面卡主 150 | if (Math.abs(end - current) >= 1.8) { 151 | this.mediaPlayer.currentTime = end - 0.01; 152 | // console.log("画面存在延迟", this.sourceBuffer.buffered.length, current, end); 153 | } 154 | } 155 | 156 | /** 157 | * 移除缓存 158 | */ 159 | removeBuffer() { 160 | if (!this.sourceBuffer || !this.sourceBuffer.buffered.length || this.sourceBuffer.updating) return; 161 | 162 | const length = this.sourceBuffer.buffered.length; 163 | const firstStart = this.sourceBuffer.buffered.start(0); 164 | const firstEnd = this.sourceBuffer.buffered.end(0); 165 | const lastStart = this.sourceBuffer.buffered.start(this.sourceBuffer.buffered.length - 1); 166 | const lastEnd = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length - 1); 167 | const currentTime = this.mediaPlayer.currentTime; 168 | 169 | if (Math.abs(firstStart - lastEnd) > 47000) { 170 | this.sourceBuffer.remove(firstEnd + 10, lastEnd); 171 | // console.log("时间戳存在溢出", length, firstStart, firstEnd, currentTime, lastStart, lastEnd); 172 | } else if (currentTime - firstStart > 120 && lastEnd > currentTime) { 173 | this.sourceBuffer.remove(firstStart, lastEnd - 10) 174 | // console.log("正常移除缓存数据", length, firstStart, firstEnd, currentTime, lastStart, lastEnd); 175 | } 176 | } 177 | 178 | reset() { 179 | this.close(); 180 | this.open(); 181 | console.log("触发websocket进行重连"); 182 | } 183 | } -------------------------------------------------------------------------------- /rtsp-websocket-server-sample/src/main/resources/templates/client.html: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | websocket 31 | 32 | 33 | 34 |

RTSP + H264 + FMP4 + WebSocket + MSE + WEB

35 |

采用的通信库: https://github.com/xingshuangs/iot-communication
36 |

37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | 45 |
46 | 47 | 48 | 49 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /rtsp-websocket-server-sample/src/test/java/com/github/xingshuangs/rtsp/server/RtspWebsocketServerSampleApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.github.xingshuangs.rtsp.server; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class RtspWebsocketServerSampleApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.3.4.RELEASE 9 | 10 | 11 | com.github.xingshuangs 12 | rtsp-websocket-server-starter 13 | 0.0.1-SNAPSHOT 14 | rtsp-websocket-server-starter 15 | rtsp-websocket-server-starter 16 | https://github.com/xingshuangs/rtsp-websocket-server 17 | 18 | 8 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-test 24 | test 25 | 26 | 27 | 28 | 29 | org.projectlombok 30 | lombok 31 | 1.18.26 32 | provided 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-configuration-processor 38 | true 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-thymeleaf 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-web 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-websocket 53 | 54 | 55 | 56 | 57 | org.apache.commons 58 | commons-lang3 59 | 3.12.0 60 | 61 | 62 | 63 | com.github.xingshuangs 64 | iot-communication 65 | 1.5.3 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-maven-plugin 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/RtspWebsocketServerStarterApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter; 26 | 27 | import org.springframework.boot.SpringApplication; 28 | import org.springframework.boot.autoconfigure.SpringBootApplication; 29 | 30 | @SpringBootApplication 31 | public class RtspWebsocketServerStarterApplication { 32 | 33 | public static void main(String[] args) { 34 | SpringApplication.run(RtspWebsocketServerStarterApplication.class, args); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/config/PropertiesConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter.config; 26 | 27 | 28 | import com.github.xingshuangs.rtsp.starter.properties.RtspProperties; 29 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 30 | import org.springframework.context.annotation.Bean; 31 | import org.springframework.context.annotation.Configuration; 32 | 33 | /** 34 | * @author xingshuang 35 | */ 36 | @EnableConfigurationProperties 37 | @Configuration 38 | public class PropertiesConfig { 39 | 40 | @Bean 41 | public RtspProperties rtspProperties(){ 42 | return new RtspProperties(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/config/WebsocketConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter.config; 26 | 27 | 28 | import com.github.xingshuangs.rtsp.starter.service.RtspWebsocketHandler; 29 | import org.springframework.context.annotation.Configuration; 30 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 31 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 32 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 33 | 34 | /** 35 | * websocket的配置 36 | * 37 | * @author xingshuang 38 | */ 39 | @Configuration 40 | @EnableWebSocket 41 | public class WebsocketConfig implements WebSocketConfigurer { 42 | 43 | private final RtspWebsocketHandler rtspWebsocketHandler; 44 | 45 | public WebsocketConfig(RtspWebsocketHandler rtspWebsocketHandler) { 46 | this.rtspWebsocketHandler = rtspWebsocketHandler; 47 | } 48 | 49 | @Override 50 | public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) { 51 | webSocketHandlerRegistry.addHandler(rtspWebsocketHandler, "/rtsp") 52 | .setAllowedOrigins("*"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/controller/PageController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter.controller; 26 | 27 | 28 | import com.github.xingshuangs.rtsp.starter.model.RtspAddress; 29 | import com.github.xingshuangs.rtsp.starter.properties.RtspProperties; 30 | import org.springframework.stereotype.Controller; 31 | import org.springframework.ui.Model; 32 | import org.springframework.web.bind.annotation.RequestMapping; 33 | 34 | import java.util.List; 35 | import java.util.stream.Collectors; 36 | 37 | /** 38 | * @author xingshuang 39 | */ 40 | @Controller 41 | public class PageController { 42 | 43 | private final RtspProperties rtspProperties; 44 | 45 | public PageController(RtspProperties rtspProperties) { 46 | this.rtspProperties = rtspProperties; 47 | } 48 | 49 | @RequestMapping("") 50 | public String client(Model map) { 51 | List channelNumbers = this.rtspProperties.getAddresses().stream().map(RtspAddress::getNumber).collect(Collectors.toList()); 52 | map.addAttribute("channelNumbers", channelNumbers); 53 | return "client"; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/controller/RtspController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter.controller; 26 | 27 | 28 | import com.github.xingshuangs.rtsp.starter.model.RtspAddress; 29 | import com.github.xingshuangs.rtsp.starter.properties.RtspProperties; 30 | import lombok.extern.slf4j.Slf4j; 31 | import org.springframework.http.ResponseEntity; 32 | import org.springframework.web.bind.annotation.GetMapping; 33 | import org.springframework.web.bind.annotation.RestController; 34 | 35 | import java.util.List; 36 | import java.util.stream.Collectors; 37 | 38 | /** 39 | * @author xingshuang 40 | */ 41 | @Slf4j 42 | @RestController 43 | public class RtspController { 44 | 45 | private final RtspProperties rtspProperties; 46 | 47 | public RtspController(RtspProperties rtspProperties) { 48 | this.rtspProperties = rtspProperties; 49 | } 50 | 51 | @GetMapping("/channel/number") 52 | public ResponseEntity> getChannelNumber() { 53 | List numbers = this.rtspProperties.getAddresses().stream().map(RtspAddress::getNumber).collect(Collectors.toList()); 54 | return ResponseEntity.ok(numbers); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/enums/ERtspMessageType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter.enums; 26 | 27 | 28 | /** 29 | * RTSP消息类型 30 | * 31 | * @author xingshuang 32 | */ 33 | public enum ERtspMessageType { 34 | /** 35 | * 订阅 36 | */ 37 | SUBSCRIBE, 38 | 39 | /** 40 | * 取消订阅 41 | */ 42 | UNSUBSCRIBE, 43 | 44 | /** 45 | * 查询 46 | */ 47 | QUERY, 48 | 49 | /** 50 | * 错误 51 | */ 52 | ERROR 53 | } 54 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/model/RtspAddress.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter.model; 26 | 27 | 28 | import lombok.Data; 29 | 30 | /** 31 | * RTSP的地址 32 | * 33 | * @author xingshuang 34 | */ 35 | @Data 36 | public class RtspAddress { 37 | 38 | /** 39 | * 视频通道编号 40 | */ 41 | private Integer number; 42 | 43 | /** 44 | * 视频地址路由 45 | */ 46 | private String url; 47 | } 48 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/model/RtspConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter.model; 26 | 27 | 28 | import com.github.xingshuangs.iot.protocol.rtsp.authentication.UsernamePasswordCredential; 29 | import com.github.xingshuangs.iot.protocol.rtsp.service.RtspClient; 30 | import com.github.xingshuangs.iot.protocol.rtsp.service.RtspFMp4Proxy; 31 | 32 | import java.net.URI; 33 | import java.time.LocalDateTime; 34 | import java.util.HashSet; 35 | import java.util.Set; 36 | import java.util.function.Consumer; 37 | 38 | /** 39 | * rtsp的连接器 40 | * 41 | * @author xingshuang 42 | */ 43 | public class RtspConnection { 44 | 45 | private final Object objLock = new Object(); 46 | 47 | /** 48 | * 最完整的RTSP连接地址 49 | */ 50 | private URI rawUri; 51 | 52 | /** 53 | * 处理过的RTSP连接地址,没有账号和密码 54 | */ 55 | private URI ripUri; 56 | 57 | /** 58 | * 用户名和密码的凭证,可能没有 59 | */ 60 | private UsernamePasswordCredential credential; 61 | 62 | /** 63 | * rtsp连接的客户端 64 | */ 65 | private RtspClient client; 66 | 67 | /** 68 | * rtsp-fmp4转换的代理器 69 | */ 70 | private RtspFMp4Proxy rtspFMp4Proxy; 71 | 72 | /** 73 | * 开始时间 74 | */ 75 | private LocalDateTime startTime; 76 | 77 | /** 78 | * 对应的session连接 79 | */ 80 | private final Set connections = new HashSet<>(); 81 | 82 | public URI getRawUri() { 83 | return rawUri; 84 | } 85 | 86 | public void setRawUri(URI rawUri) { 87 | this.rawUri = rawUri; 88 | } 89 | 90 | public URI getRipUri() { 91 | return ripUri; 92 | } 93 | 94 | public void setRipUri(URI ripUri) { 95 | this.ripUri = ripUri; 96 | } 97 | 98 | public UsernamePasswordCredential getCredential() { 99 | return credential; 100 | } 101 | 102 | public void setCredential(UsernamePasswordCredential credential) { 103 | this.credential = credential; 104 | } 105 | 106 | public RtspClient getClient() { 107 | return client; 108 | } 109 | 110 | public void setClient(RtspClient client) { 111 | this.client = client; 112 | } 113 | 114 | public RtspFMp4Proxy getRtspFMp4Proxy() { 115 | return rtspFMp4Proxy; 116 | } 117 | 118 | public void setRtspFMp4Proxy(RtspFMp4Proxy rtspFMp4Proxy) { 119 | this.rtspFMp4Proxy = rtspFMp4Proxy; 120 | } 121 | 122 | public byte[] getMp4Header() { 123 | return this.rtspFMp4Proxy.getMp4Header().toByteArray(); 124 | } 125 | 126 | public String getCodec() { 127 | return this.rtspFMp4Proxy.getMp4TrackInfo().getCodec(); 128 | } 129 | 130 | public LocalDateTime getStartTime() { 131 | return startTime; 132 | } 133 | 134 | public void setStartTime(LocalDateTime startTime) { 135 | this.startTime = startTime; 136 | } 137 | 138 | /** 139 | * 是否有websocket连接 140 | * 141 | * @return true:有,false:没有 142 | */ 143 | public boolean hasWebsocketConnection() { 144 | synchronized (this.objLock) { 145 | return !this.connections.isEmpty(); 146 | } 147 | } 148 | 149 | /** 150 | * 添加websocket连接 151 | * 152 | * @param connection websocket连接 153 | */ 154 | public void addWebsocketConnection(WebsocketConnection connection) { 155 | synchronized (this.objLock) { 156 | this.connections.add(connection); 157 | } 158 | } 159 | 160 | /** 161 | * 移除websocket连接 162 | * 163 | * @param connection websocket连接 164 | */ 165 | public void removeWebsocketConnection(WebsocketConnection connection) { 166 | synchronized (this.objLock) { 167 | this.connections.remove(connection); 168 | } 169 | } 170 | 171 | /** 172 | * 是否包含指定websocket连接 173 | * 174 | * @param connection websocket连接 175 | * @return true:包含,false:不包含 176 | */ 177 | public boolean containWebsocketConnection(WebsocketConnection connection) { 178 | synchronized (this.objLock) { 179 | return this.connections.stream().anyMatch(x -> x.getSession().getId().equals(connection.getSession().getId())); 180 | } 181 | } 182 | 183 | /** 184 | * 遍历连接执行动作 185 | * 186 | * @param consumer 动作 187 | */ 188 | public void foreachConnections(Consumer consumer) { 189 | synchronized (this.objLock) { 190 | for (WebsocketConnection connection : this.connections) { 191 | consumer.accept(connection); 192 | } 193 | } 194 | } 195 | 196 | /** 197 | * 移除所有的websocket连接 198 | */ 199 | public void removeAllWebsocketConnection() { 200 | synchronized (this.objLock) { 201 | this.connections.clear(); 202 | } 203 | } 204 | 205 | /** 206 | * 获取websocket连接数量 207 | * 208 | * @return 数量 209 | */ 210 | public int getWebsocketConnectionCount() { 211 | synchronized (this.objLock) { 212 | return this.connections.size(); 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/model/RtspMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter.model; 26 | 27 | 28 | import com.github.xingshuangs.rtsp.starter.enums.ERtspMessageType; 29 | import lombok.Data; 30 | 31 | /** 32 | * 指令消息 33 | * 34 | * @author xingshuang 35 | */ 36 | @Data 37 | public class RtspMessage { 38 | 39 | /** 40 | * 类型,目前分为两种,1、订阅类型,2、查询类型 41 | */ 42 | private ERtspMessageType type; 43 | 44 | /** 45 | * 视频通道编号,4字节 46 | */ 47 | private Integer number; 48 | 49 | /** 50 | * 内容 51 | */ 52 | private T content; 53 | 54 | /** 55 | * 创建查询消息 56 | * 57 | * @param number 视频通道编号 58 | * @param content 内容 59 | * @param 类型 60 | * @return RtspMessage 61 | */ 62 | public static RtspMessage createQuery(Integer number, T content) { 63 | RtspMessage resMessage = new RtspMessage<>(); 64 | resMessage.setType(ERtspMessageType.QUERY); 65 | resMessage.setNumber(number); 66 | resMessage.setContent(content); 67 | return resMessage; 68 | } 69 | 70 | /** 71 | * 创建订阅消息 72 | * 73 | * @param number 视频通道编号 74 | * @param content 内容 75 | * @param 类型 76 | * @return RtspMessage 77 | */ 78 | public static RtspMessage createSubscribe(Integer number, T content) { 79 | RtspMessage resMessage = new RtspMessage<>(); 80 | resMessage.setType(ERtspMessageType.SUBSCRIBE); 81 | resMessage.setNumber(number); 82 | resMessage.setContent(content); 83 | return resMessage; 84 | } 85 | 86 | /** 87 | * 创建错误消息 88 | * 89 | * @param number 视频通道编号 90 | * @param content 内容 91 | * @param 类型 92 | * @return RtspMessage 93 | */ 94 | public static RtspMessage createError(Integer number, T content) { 95 | RtspMessage resMessage = new RtspMessage<>(); 96 | resMessage.setType(ERtspMessageType.ERROR); 97 | resMessage.setNumber(number); 98 | resMessage.setContent(content); 99 | return resMessage; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/model/WebsocketConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter.model; 26 | 27 | import lombok.Data; 28 | import org.springframework.web.socket.WebSocketMessage; 29 | import org.springframework.web.socket.WebSocketSession; 30 | import org.apache.commons.lang3.StringUtils; 31 | 32 | import java.io.IOException; 33 | import java.time.LocalDateTime; 34 | import java.util.HashSet; 35 | import java.util.Objects; 36 | import java.util.Set; 37 | import java.util.function.IntConsumer; 38 | import java.util.stream.Collectors; 39 | 40 | /** 41 | * websocket的连接器 42 | * 43 | * @author xingshuang 44 | */ 45 | @Data 46 | public class WebsocketConnection { 47 | 48 | private final Object objLock = new Object(); 49 | 50 | /** 51 | * session对象 52 | */ 53 | private WebSocketSession session; 54 | 55 | /** 56 | * 初次连入的时间 57 | */ 58 | private LocalDateTime lastConnectionTime; 59 | 60 | /** 61 | * 最新订阅触发时间 62 | */ 63 | private LocalDateTime lastSubscribeTime; 64 | 65 | /** 66 | * 订阅的视频通道编号 67 | */ 68 | private final Set subscribeChannelNumbers = new HashSet<>(); 69 | 70 | /** 71 | * 获取所有视频通道编号的字符串 72 | * 73 | * @return 通道编号字符串 74 | */ 75 | public String getAllChannelNumbers() { 76 | synchronized (this.objLock) { 77 | return StringUtils.join(this.subscribeChannelNumbers.stream().map(String::valueOf).collect(Collectors.toList()), ","); 78 | } 79 | } 80 | 81 | /** 82 | * 添加视频通道编号 83 | * 84 | * @param number 视频通道编号 85 | */ 86 | public void addChannelNumber(Integer number) { 87 | synchronized (this.objLock) { 88 | this.subscribeChannelNumbers.add(number); 89 | } 90 | } 91 | 92 | /** 93 | * 移除视频通道编号 94 | * 95 | * @param number 视频通道编号 96 | */ 97 | public void removeChannelNumber(Integer number) { 98 | synchronized (this.objLock) { 99 | this.subscribeChannelNumbers.remove(number); 100 | } 101 | } 102 | 103 | /** 104 | * 移除所有视频通道编号 105 | */ 106 | public void removeAllChannelNumbers() { 107 | synchronized (this.objLock) { 108 | this.subscribeChannelNumbers.clear(); 109 | } 110 | } 111 | 112 | /** 113 | * 是否包含视频通道编号 114 | * 115 | * @param number 视频通道编号 116 | * @return true:包含,false:不包含 117 | */ 118 | public boolean containChannelNumber(Integer number) { 119 | synchronized (this.objLock) { 120 | return this.subscribeChannelNumbers.contains(number); 121 | } 122 | } 123 | 124 | /** 125 | * 是否有视频通道编号 126 | * 127 | * @return true:有,false:没有 128 | */ 129 | public boolean hasChannelNumber() { 130 | synchronized (this.objLock) { 131 | return !this.subscribeChannelNumbers.isEmpty(); 132 | } 133 | } 134 | 135 | /** 136 | * 遍历所有视频通道编号 137 | * 138 | * @param consumer 对应的事件 139 | */ 140 | public void foreachChannelNumbers(IntConsumer consumer) { 141 | synchronized (this.objLock) { 142 | for (Integer number : this.subscribeChannelNumbers) { 143 | consumer.accept(number); 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * 发送消息 150 | * 151 | * @param message 消息数据 152 | * @throws IOException 异常 153 | */ 154 | public synchronized void sendMessage(WebSocketMessage message) throws IOException { 155 | if (this.session.isOpen()) { 156 | this.session.sendMessage(message); 157 | } 158 | } 159 | 160 | @Override 161 | public boolean equals(Object o) { 162 | if (this == o) return true; 163 | if (o == null || getClass() != o.getClass()) return false; 164 | WebsocketConnection that = (WebsocketConnection) o; 165 | return session.getId().equals(that.session.getId()); 166 | } 167 | 168 | @Override 169 | public int hashCode() { 170 | return Objects.hash(session.getId()); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/properties/RtspProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter.properties; 26 | 27 | 28 | import com.github.xingshuangs.rtsp.starter.model.RtspAddress; 29 | import lombok.Data; 30 | import org.springframework.boot.context.properties.ConfigurationProperties; 31 | 32 | import java.util.List; 33 | 34 | /** 35 | * RTSP的配置 36 | * 37 | * @author xingshuang 38 | */ 39 | 40 | @Data 41 | @ConfigurationProperties(RtspProperties.PREFIX) 42 | public class RtspProperties { 43 | 44 | public static final String PREFIX = "rtsp"; 45 | 46 | /** 47 | * RTSP的地址列表 48 | */ 49 | private List addresses; 50 | } 51 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/service/RtspManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter.service; 26 | 27 | 28 | import com.fasterxml.jackson.core.type.TypeReference; 29 | import com.fasterxml.jackson.databind.ObjectMapper; 30 | import com.github.xingshuangs.iot.common.buff.ByteWriteBuff; 31 | import com.github.xingshuangs.iot.protocol.rtsp.authentication.DigestAuthenticator; 32 | import com.github.xingshuangs.iot.protocol.rtsp.authentication.UsernamePasswordCredential; 33 | import com.github.xingshuangs.iot.protocol.rtsp.enums.ERtspTransportProtocol; 34 | import com.github.xingshuangs.iot.protocol.rtsp.service.RtspClient; 35 | import com.github.xingshuangs.iot.protocol.rtsp.service.RtspFMp4Proxy; 36 | import com.github.xingshuangs.rtsp.starter.model.RtspAddress; 37 | import com.github.xingshuangs.rtsp.starter.model.RtspConnection; 38 | import com.github.xingshuangs.rtsp.starter.model.RtspMessage; 39 | import com.github.xingshuangs.rtsp.starter.model.WebsocketConnection; 40 | import com.github.xingshuangs.rtsp.starter.properties.RtspProperties; 41 | import lombok.extern.slf4j.Slf4j; 42 | import org.springframework.stereotype.Component; 43 | import org.springframework.web.socket.BinaryMessage; 44 | import org.springframework.web.socket.TextMessage; 45 | 46 | import java.net.URI; 47 | import java.nio.ByteBuffer; 48 | import java.time.LocalDateTime; 49 | import java.util.ArrayList; 50 | import java.util.List; 51 | import java.util.Map; 52 | import java.util.Optional; 53 | import java.util.concurrent.ConcurrentHashMap; 54 | import java.util.stream.Collectors; 55 | 56 | /** 57 | * RTSP的管理器 58 | * 59 | * @author xingshuang 60 | */ 61 | @Slf4j 62 | @Component 63 | public class RtspManager { 64 | 65 | /** 66 | * key:视频通道编号 67 | * value:rtsp连接对象 68 | */ 69 | private final ConcurrentHashMap rtspConnectionMap = new ConcurrentHashMap<>(); 70 | 71 | /** 72 | * rtsp的地址 73 | */ 74 | private final List rtspAddresses; 75 | 76 | /** 77 | * json转换对象 78 | */ 79 | private final ObjectMapper objectMapper; 80 | 81 | public RtspManager(RtspProperties rtspProperties, ObjectMapper objectMapper) { 82 | this.rtspAddresses = rtspProperties.getAddresses(); 83 | this.objectMapper = objectMapper; 84 | } 85 | 86 | /** 87 | * 移除所有视频通道指定的websocket连接 88 | * 89 | * @param websocketConnection websocket连接 90 | */ 91 | public void remove(WebsocketConnection websocketConnection) { 92 | List channelNumbers = this.getChannelNumberByWebsocket(websocketConnection); 93 | if (channelNumbers.isEmpty()) { 94 | return; 95 | } 96 | // 存在该websocket连接,先移除该连接,再查看rtsp是否还有订阅连接,若没有则断开rtsp 97 | channelNumbers.forEach(channelNumber -> this.remove(websocketConnection, channelNumber)); 98 | } 99 | 100 | /** 101 | * 移除指定通道的指定websocket连接 102 | * 103 | * @param websocketConnection websocket连接 104 | * @param channelNumber 通道编号 105 | */ 106 | public void remove(WebsocketConnection websocketConnection, Integer channelNumber) { 107 | // 移除websocket连接订阅的通道编号,移除rtsp关联的websocket连接 108 | websocketConnection.removeChannelNumber(channelNumber); 109 | // 移除指定通道的websocket连接,先移除该连接,再查看rtsp是否还有订阅连接,若没有则断开rtsp 110 | RtspConnection rtspConnection = this.rtspConnectionMap.get(channelNumber); 111 | if (rtspConnection == null) { 112 | return; 113 | } 114 | rtspConnection.removeWebsocketConnection(websocketConnection); 115 | if (!rtspConnection.hasWebsocketConnection()) { 116 | rtspConnection.getRtspFMp4Proxy().stop(); 117 | this.rtspConnectionMap.remove(channelNumber); 118 | log.info("由于视频通道编号[{}]没有websocket订阅,关闭RTSP连接", channelNumber); 119 | } 120 | } 121 | 122 | /** 123 | * 处理消息,有订阅和查询公恩那个 124 | * 125 | * @param websocketConnection 连接 126 | * @param message 消息 127 | */ 128 | public void handleMessage(WebsocketConnection websocketConnection, TextMessage message) { 129 | RtspMessage rtspMessage; 130 | try { 131 | log.debug("接收数据信息:{}", message.getPayload()); 132 | rtspMessage = this.objectMapper.readValue(message.getPayload(), new TypeReference>() { 133 | }); 134 | } catch (Exception e) { 135 | log.error(e.getMessage(), e); 136 | this.sendTextMessage(websocketConnection, RtspMessage.createError(-1, "不是标准的视频交互的标准数据格式,json转换错误")); 137 | return; 138 | } 139 | 140 | try { 141 | switch (rtspMessage.getType()) { 142 | case SUBSCRIBE: 143 | this.handleSubscribe(websocketConnection, rtspMessage); 144 | break; 145 | case UNSUBSCRIBE: 146 | this.handleUnsubscribe(websocketConnection, rtspMessage); 147 | break; 148 | case QUERY: 149 | this.handleQuery(websocketConnection, rtspMessage); 150 | break; 151 | default: 152 | this.sendTextMessage(websocketConnection, RtspMessage.createError(rtspMessage.getNumber(), "无法识别指定消息指令")); 153 | break; 154 | } 155 | } catch (Exception e) { 156 | log.error(e.getMessage(), e); 157 | this.sendTextMessage(websocketConnection, RtspMessage.createError(rtspMessage.getNumber(), e.getMessage())); 158 | } 159 | } 160 | 161 | /** 162 | * 订阅事件处理 163 | * 164 | * @param websocketConnection websocket连接 165 | * @param rtspMessage rtsp消息 166 | */ 167 | private void handleSubscribe(WebsocketConnection websocketConnection, RtspMessage rtspMessage) { 168 | Integer channelNumber = rtspMessage.getNumber(); 169 | 170 | // 1. 判定视频通道编号有没有,若没有返回错误消息 171 | boolean existSubscribeName = this.rtspAddresses.stream().anyMatch(x -> x.getNumber().equals(channelNumber)); 172 | if (!existSubscribeName) { 173 | log.error("websocket[{}],没有该通道编号[{}],无法订阅", websocketConnection.getSession().getId(), channelNumber); 174 | this.sendTextMessage(websocketConnection, RtspMessage.createError(rtspMessage.getNumber(), "不存在该视频通道编号:" + channelNumber)); 175 | return; 176 | } 177 | 178 | // 2. 判定该websocket之前是否已经订阅过视频通道编号 179 | if (websocketConnection.containChannelNumber(channelNumber)) { 180 | // 已经订阅过,且同名,直接返回,无需重复订阅 181 | log.info("websocket[{}],已订阅过该视频通道编号[{}],无需重复订阅", websocketConnection.getSession().getId(), channelNumber); 182 | this.sendTextMessage(websocketConnection, RtspMessage.createError(rtspMessage.getNumber(), "已经订阅该通道,无需重复订阅")); 183 | return; 184 | } 185 | // 未订阅过,添加新的通道编号 186 | websocketConnection.setLastSubscribeTime(LocalDateTime.now()); 187 | websocketConnection.addChannelNumber(channelNumber); 188 | 189 | // 3. 有没有rtsp对应的通道编号,有则添加websocket的连接,没有则重新创建新代理连接 190 | RtspConnection rtspConnection = this.rtspConnectionMap.get(channelNumber); 191 | if (rtspConnection != null) { 192 | // 已有rtsp连接,第一步需要立即返回codec,第二步需要接收视频头,第三步需要接收视频流内容 193 | log.info("websocket[{}],已构建了视频通道编号[{}],直接发送codec和mp4视频头", websocketConnection.getSession().getId(), channelNumber); 194 | RtspMessage codecMessage = RtspMessage.createSubscribe(rtspMessage.getNumber(), rtspConnection.getCodec()); 195 | this.sendTextMessage(websocketConnection, codecMessage); 196 | this.sendBinaryMessage(websocketConnection, channelNumber, rtspConnection.getMp4Header()); 197 | rtspConnection.addWebsocketConnection(websocketConnection); 198 | this.printConnectionChannelInfo(); 199 | return; 200 | } 201 | 202 | // 4. 开始订阅新视频通道 203 | log.info("websocket[{}],订阅视频通道编号[{}]", websocketConnection.getSession().getId(), channelNumber); 204 | RtspConnection newRtspConnection = this.openRtspFmp4Proxy(websocketConnection, channelNumber); 205 | if (newRtspConnection != null) { 206 | this.rtspConnectionMap.put(channelNumber, newRtspConnection); 207 | } else { 208 | websocketConnection.removeChannelNumber(channelNumber); 209 | } 210 | this.printConnectionChannelInfo(); 211 | } 212 | 213 | /** 214 | * 取消订阅 215 | * 216 | * @param websocketConnection websocket连接 217 | * @param rtspMessage rtsp消息 218 | */ 219 | private void handleUnsubscribe(WebsocketConnection websocketConnection, RtspMessage rtspMessage) { 220 | log.info("websocket[{}],取消订阅视频通道编号[{}]", websocketConnection.getSession().getId(), rtspMessage.getNumber()); 221 | this.remove(websocketConnection, rtspMessage.getNumber()); 222 | this.printConnectionChannelInfo(); 223 | } 224 | 225 | 226 | /** 227 | * 处理查询消息 228 | * 229 | * @param websocketConnection websocket连接 230 | * @param rtspMessage rtsp消息 231 | */ 232 | private void handleQuery(WebsocketConnection websocketConnection, RtspMessage rtspMessage) { 233 | RtspMessage message; 234 | if (!rtspMessage.getContent().equals("channel")) { 235 | message = RtspMessage.createError(rtspMessage.getNumber(), "查询的content参数目前只能是channel"); 236 | } else { 237 | List names = this.rtspAddresses.stream().map(RtspAddress::getNumber).collect(Collectors.toList()); 238 | message = RtspMessage.createQuery(rtspMessage.getNumber(), names); 239 | } 240 | this.sendTextMessage(websocketConnection, message); 241 | } 242 | 243 | /** 244 | * 发送文本消息 245 | * 246 | * @param websocketConnection websocket连接 247 | * @param message 消息对象 248 | */ 249 | private void sendTextMessage(WebsocketConnection websocketConnection, RtspMessage message) { 250 | try { 251 | String res = this.objectMapper.writeValueAsString(message); 252 | TextMessage textMessage = new TextMessage(res); 253 | websocketConnection.sendMessage(textMessage); 254 | } catch (Exception e) { 255 | log.error(e.getMessage(), e); 256 | } 257 | } 258 | 259 | /** 260 | * 发送二进制消息 261 | * 262 | * @param websocketConnection websocket连接 263 | * @param channelNumber 通道编号 264 | * @param fmp4Data 消息内容,主要是字节内容 265 | */ 266 | private void sendBinaryMessage(WebsocketConnection websocketConnection, Integer channelNumber, byte[] fmp4Data) { 267 | try { 268 | // 前4个字节是视频通道编号,后面的是视频数据 269 | byte[] buff = ByteWriteBuff.newInstance(4 + fmp4Data.length) 270 | .putInteger(channelNumber) 271 | .putBytes(fmp4Data) 272 | .getData(); 273 | ByteBuffer wrap = ByteBuffer.wrap(buff); 274 | BinaryMessage message = new BinaryMessage(wrap); 275 | websocketConnection.sendMessage(message); 276 | } catch (Exception e) { 277 | log.error(e.getMessage(), e); 278 | } 279 | } 280 | 281 | /** 282 | * 获取订阅的视频通道编号 283 | * 284 | * @param websocketConnection websocket连接 285 | * @return 视频通道编号 286 | */ 287 | private List getChannelNumberByWebsocket(WebsocketConnection websocketConnection) { 288 | List res = new ArrayList<>(); 289 | for (Map.Entry entry : rtspConnectionMap.entrySet()) { 290 | if (entry.getValue().containWebsocketConnection(websocketConnection)) { 291 | res.add(entry.getKey()); 292 | } 293 | } 294 | return res; 295 | } 296 | 297 | /** 298 | * 开启rtsp-fmp4的代理 299 | * 300 | * @param websocketConnection websocket的连接 301 | * @param channelNumber 通道编号 302 | * @return RtspConnection 303 | */ 304 | private RtspConnection openRtspFmp4Proxy(WebsocketConnection websocketConnection, Integer channelNumber) { 305 | RtspFMp4Proxy rtspFMp4Proxy = null; 306 | try { 307 | Optional optRtspAddress = this.rtspAddresses.stream().filter(x -> x.getNumber().equals(channelNumber)).findFirst(); 308 | if (!optRtspAddress.isPresent()) { 309 | return null; 310 | } 311 | RtspAddress rtspAddress = optRtspAddress.get(); 312 | RtspConnection rtspConnection = new RtspConnection(); 313 | 314 | URI srcUri = URI.create(rtspAddress.getUrl()); 315 | int i = rtspAddress.getUrl().indexOf("@"); 316 | URI uri = i < 0 ? srcUri : URI.create("rtsp://" + rtspAddress.getUrl().substring(i + 1)); 317 | 318 | DigestAuthenticator authenticator = null; 319 | UsernamePasswordCredential credential = null; 320 | if (srcUri.getUserInfo() != null) { 321 | credential = UsernamePasswordCredential.createBy(srcUri.getUserInfo()); 322 | authenticator = new DigestAuthenticator(credential); 323 | } 324 | RtspClient client = new RtspClient(uri, authenticator, ERtspTransportProtocol.UDP); 325 | rtspFMp4Proxy = new RtspFMp4Proxy(client, true); 326 | // 初始统一更新rtsp连接信息 327 | rtspConnection.setRawUri(srcUri); 328 | rtspConnection.setRipUri(uri); 329 | rtspConnection.setCredential(credential); 330 | rtspConnection.setClient(client); 331 | rtspConnection.setRtspFMp4Proxy(rtspFMp4Proxy); 332 | rtspConnection.addWebsocketConnection(websocketConnection); 333 | // 绑定事件 334 | rtspFMp4Proxy.onFmp4DataHandle(x -> this.fmp4DataHandle(rtspConnection, channelNumber, x)); 335 | rtspFMp4Proxy.onCodecHandle(x -> this.codecHandle(rtspConnection, channelNumber, x)); 336 | rtspFMp4Proxy.onDestroyHandle(() -> this.destroyHandle(channelNumber)); 337 | rtspFMp4Proxy.start(); 338 | return rtspConnection; 339 | } catch (Exception e) { 340 | log.error(e.getMessage(), e); 341 | if (rtspFMp4Proxy != null) { 342 | rtspFMp4Proxy.stop(); 343 | } 344 | this.sendTextMessage(websocketConnection, RtspMessage.createError(channelNumber, e.getMessage())); 345 | return null; 346 | } 347 | } 348 | 349 | /** 350 | * 处理FMP4的数据帧 351 | * 352 | * @param rtspConnection rtsp连接 353 | * @param channelNumber 通道编号 354 | * @param fmp4Data FMP4的数据帧 355 | */ 356 | private void fmp4DataHandle(RtspConnection rtspConnection, Integer channelNumber, byte[] fmp4Data) { 357 | rtspConnection.foreachConnections(x -> { 358 | if (x.containChannelNumber(channelNumber)) { 359 | this.sendBinaryMessage(x, channelNumber, fmp4Data); 360 | } 361 | }); 362 | } 363 | 364 | /** 365 | * 处理codec 366 | * 367 | * @param rtspConnection rtsp连接 368 | * @param channelNumber 通道编号 369 | * @param codec codec编码 370 | */ 371 | private void codecHandle(RtspConnection rtspConnection, Integer channelNumber, String codec) { 372 | rtspConnection.setStartTime(LocalDateTime.now()); 373 | rtspConnection.foreachConnections(x -> { 374 | if (x.containChannelNumber(channelNumber)) { 375 | RtspMessage codecMessage = RtspMessage.createSubscribe(channelNumber, codec); 376 | this.sendTextMessage(x, codecMessage); 377 | log.debug("给通道[{}]发送codec[{}]", channelNumber, codec); 378 | } 379 | }); 380 | } 381 | 382 | /** 383 | * rtsp断开后,触发关闭所有websocket通道 384 | * 385 | * @param channelNumber 通道编号 386 | */ 387 | private void destroyHandle(Integer channelNumber) { 388 | RtspConnection rtspConnection = this.rtspConnectionMap.get(channelNumber); 389 | if (rtspConnection == null) { 390 | return; 391 | } 392 | rtspConnection.foreachConnections(x -> { 393 | if (x.containChannelNumber(channelNumber)) { 394 | RtspMessage codecMessage = RtspMessage.createError(channelNumber, "RTSP连接断开"); 395 | this.sendTextMessage(x, codecMessage); 396 | } 397 | }); 398 | // 移除websocket关联的视频通道,移除rtsp关联的websocket 399 | rtspConnection.foreachConnections(WebsocketConnection::removeAllChannelNumbers); 400 | rtspConnection.removeAllWebsocketConnection(); 401 | this.rtspConnectionMap.remove(channelNumber); 402 | } 403 | 404 | /** 405 | * 打印当前通道信息 406 | */ 407 | private void printConnectionChannelInfo() { 408 | log.info(String.format("当前构建的视频通道数量[%d]", this.rtspConnectionMap.size())); 409 | 410 | for (Map.Entry entry : rtspConnectionMap.entrySet()) { 411 | StringBuilder sb = new StringBuilder(); 412 | sb.append(String.format("视频通道编号[%s]被订阅的Websocket数量[%d]", entry.getKey(), entry.getValue().getWebsocketConnectionCount())); 413 | entry.getValue().foreachConnections(x -> sb.append(String.format(",Websocket[%s]订阅了通道编号[%s]", x.getSession().getId(), x.getAllChannelNumbers()))); 414 | log.info(sb.toString()); 415 | } 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/java/com/github/xingshuangs/rtsp/starter/service/RtspWebsocketHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package com.github.xingshuangs.rtsp.starter.service; 26 | 27 | 28 | import com.github.xingshuangs.rtsp.starter.model.WebsocketConnection; 29 | import lombok.extern.slf4j.Slf4j; 30 | import org.springframework.stereotype.Component; 31 | import org.springframework.web.socket.CloseStatus; 32 | import org.springframework.web.socket.TextMessage; 33 | import org.springframework.web.socket.WebSocketSession; 34 | import org.springframework.web.socket.handler.TextWebSocketHandler; 35 | 36 | import java.time.LocalDateTime; 37 | import java.util.concurrent.ConcurrentHashMap; 38 | 39 | /** 40 | * RTSP的Websocket处理器 41 | * 42 | * @author xingshuang 43 | */ 44 | @Slf4j 45 | @Component 46 | public class RtspWebsocketHandler extends TextWebSocketHandler { 47 | 48 | private final ConcurrentHashMap connectionMap = new ConcurrentHashMap<>(); 49 | 50 | private final RtspManager rtspManager; 51 | 52 | public RtspWebsocketHandler(RtspManager rtspManager) { 53 | this.rtspManager = rtspManager; 54 | } 55 | 56 | @Override 57 | public void afterConnectionEstablished(WebSocketSession session) { 58 | WebsocketConnection websocketConnection = new WebsocketConnection(); 59 | websocketConnection.setSession(session); 60 | websocketConnection.setLastConnectionTime(LocalDateTime.now()); 61 | this.connectionMap.putIfAbsent(session.getId(), websocketConnection); 62 | log.info("有新连接加入!当前在线人数为[{}],sessionId[{}]", this.connectionMap.size(), session.getId()); 63 | } 64 | 65 | @Override 66 | protected void handleTextMessage(WebSocketSession session, TextMessage message) { 67 | WebsocketConnection websocketConnection = this.connectionMap.get(session.getId()); 68 | this.rtspManager.handleMessage(websocketConnection, message); 69 | } 70 | 71 | @Override 72 | public void handleTransportError(WebSocketSession session, Throwable exception) { 73 | WebsocketConnection websocketConnection = this.connectionMap.get(session.getId()); 74 | this.rtspManager.remove(websocketConnection); 75 | this.connectionMap.remove(session.getId()); 76 | log.error("发生错误,sessionId[{}],错误信息={}", session.getId(), exception.getMessage()); 77 | } 78 | 79 | @Override 80 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { 81 | WebsocketConnection websocketConnection = this.connectionMap.get(session.getId()); 82 | this.rtspManager.remove(websocketConnection); 83 | this.connectionMap.remove(session.getId()); 84 | log.info("有一连接关闭!当前在线人数为[{}],sessionId[{}]", this.connectionMap.size(), session.getId()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8089 3 | 4 | spring: 5 | mvc: 6 | static-path-pattern: /static/** 7 | 8 | logging: 9 | pattern: 10 | file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%15thread] %-50.50(%logger{39}.%method:%-3line) - %msg%n" 11 | console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%15thread] %-50.50(%logger{39}.%method:%-3line) - %msg%n" 12 | file: 13 | name: ./logs/rtsp-websocket-server-starter/rtsp-websocket-server-starter.log 14 | level: 15 | com.github.xingshuangs: debug 16 | 17 | rtsp: 18 | addresses: 19 | - number: 1001 20 | url: rtsp://admin:hb123456@192.168.3.251:554/h264/ch1/main/av_stream 21 | - number: 1002 22 | url: rtsp://admin:hb123456@192.168.3.250:554/h264/ch1/main/av_stream 23 | - number: 1003 24 | url: rtsp://admin:123456@192.168.3.142:554/h264/ch1/main/av_stream 25 | - number: 1004 26 | url: rtsp://admin:123456@192.168.3.142:554/h264/ch2/main/av_stream -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/resources/static/rtspStream.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2021-2099 Oscura (xingshuang) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | class RtspStream { 26 | 27 | constructor(wsUrl, openCallback) { 28 | this.wsUrl = wsUrl; 29 | this.channelMap = new Map(); 30 | this.openCallback = openCallback; 31 | } 32 | 33 | send(data) { 34 | this.websocket.send(data); 35 | } 36 | 37 | /** 38 | * 触发订阅 39 | * @param number 通道编号 40 | * @param videoId video标签Id 41 | */ 42 | subscribe(number, videoId) { 43 | if (this.channelMap.get(number)) { 44 | console.warn("can't resubscribe same channel"); 45 | return; 46 | } 47 | console.debug(`subscribe channel[${number}],videoId[${videoId}]`) 48 | // 构建订阅参数,json的形式交互 49 | const params = {}; 50 | params.type = "SUBSCRIBE"; 51 | params.number = number; 52 | params.content = ""; 53 | const channelMedia = new ChannelMedia(number, videoId); 54 | channelMedia.onReset = (number, videoId) => this.resubscribe(number, videoId); 55 | this.channelMap.set(number, channelMedia) 56 | this.websocket.send(JSON.stringify(params)) 57 | } 58 | 59 | /** 60 | * 重新订阅 61 | * @param number 视频通道编号 62 | * @param videoId video标签Id 63 | */ 64 | resubscribe(number, videoId) { 65 | console.debug(`resubscribe channel[${number}],videoId[${videoId}]`) 66 | this.unsubscribe(number); 67 | this.subscribe(number, videoId); 68 | } 69 | 70 | /** 71 | * 取消订阅 72 | * @param number 视频通道编号 73 | */ 74 | unsubscribe(number) { 75 | console.debug(`取消订阅通道[${number}]`) 76 | // 构建订阅参数,json的形式交互 77 | const params = {}; 78 | params.type = "UNSUBSCRIBE"; 79 | params.number = number; 80 | params.content = ""; 81 | this.websocket.send(JSON.stringify(params)); 82 | this.channelMap.delete(number); 83 | } 84 | 85 | /** 86 | * 打开事件 87 | * @param evt 数据 88 | */ 89 | onopen(evt) { 90 | console.log("ws连接成功", this.wsUrl); 91 | this.openCallback(evt); 92 | } 93 | 94 | /** 95 | * 关闭事件 96 | * @param evt 数据 97 | */ 98 | onClose(evt) { 99 | console.log("ws连接关闭", this.wsUrl); 100 | } 101 | 102 | /** 103 | * 接收消息事件 104 | * @param evt 数据 105 | */ 106 | onMessage(evt) { 107 | if (typeof (evt.data) == "string") { 108 | let data = JSON.parse(evt.data); 109 | if (data.type === "SUBSCRIBE") this.channelMap.get(data.number).init(data.content); 110 | else if (data.type === "QUERY") console.log(`channel[${data.number}]: ${data.content}`); 111 | else if (data.type === "ERROR") console.error(`channel[${data.number}]: ${data.content}`); 112 | else console.log(data.content); 113 | } else { 114 | const data = new Uint8Array(evt.data); 115 | // 解析通道编号 116 | const numberSrc = data.slice(0, 4); 117 | const view = new DataView(numberSrc.buffer); 118 | const number = view.getUint32(0); 119 | // 向指定通道编号添加数据 120 | const videoData = data.slice(4); 121 | // console.log("通道编号:", number, videoData.length) 122 | this.channelMap.get(number).pushData(videoData); 123 | } 124 | } 125 | 126 | /** 127 | * 错误 128 | * @param evt 数据 129 | */ 130 | onError(evt) { 131 | console.log("ws连接错误"); 132 | } 133 | 134 | /** 135 | * 打开 136 | */ 137 | open() { 138 | this.close(); 139 | 140 | this.websocket = new WebSocket(this.wsUrl); 141 | this.websocket.binaryType = "arraybuffer"; 142 | this.websocket.onopen = this.onopen.bind(this); 143 | this.websocket.onmessage = this.onMessage.bind(this); 144 | this.websocket.onclose = this.onClose.bind(this); 145 | this.websocket.onerror = this.onError.bind(this); 146 | } 147 | 148 | /** 149 | * 关闭 150 | */ 151 | close() { 152 | if (this.websocket) this.websocket.close(); 153 | } 154 | } 155 | 156 | class ChannelMedia { 157 | 158 | constructor(number, videoId) { 159 | this.number = number; 160 | this.videoId = videoId; 161 | this.queue = []; 162 | this.canFeed = false; 163 | this.onReset = null; 164 | } 165 | 166 | /** 167 | * 初始化 168 | * @param codecStr 视频编码 169 | */ 170 | init(codecStr) { 171 | this.codec = 'video/mp4; codecs=\"' + codecStr + '\"'; 172 | console.log(`channel[${this.number}] call play [${this.codec}]`); 173 | if (MediaSource.isTypeSupported(this.codec)) { 174 | this.mediaSource = new MediaSource; 175 | this.mediaSource.addEventListener('sourceopen', this.onMediaSourceOpen.bind(this)); 176 | this.mediaPlayer = document.getElementById(this.videoId); 177 | this.mediaPlayer.src = URL.createObjectURL(this.mediaSource); 178 | } else { 179 | console.log("Unsupported MIME type or codec: ", +this.codec); 180 | } 181 | } 182 | 183 | /** 184 | * MediaSource已打开事件 185 | * @param e 事件 186 | */ 187 | onMediaSourceOpen(e) { 188 | // URL.revokeObjectURL 主动释放引用 189 | URL.revokeObjectURL(this.mediaPlayer.src); 190 | this.mediaSource.removeEventListener('sourceopen', this.onMediaSourceOpen.bind(this)); 191 | 192 | // console.log("MediaSource已打开") 193 | this.sourceBuffer = this.mediaSource.addSourceBuffer(this.codec); 194 | this.sourceBuffer.addEventListener('about', e => console.log(`about `, e)); 195 | this.sourceBuffer.addEventListener('error', e => console.log(`error `, e)); 196 | this.sourceBuffer.addEventListener('updateend', e => { 197 | this.removeBuffer(); 198 | this.processDelay(); 199 | this.canFeed = true; 200 | this.feedNext(); 201 | }); 202 | this.canFeed = true; 203 | } 204 | 205 | /** 206 | * 压入数据 207 | * @param data 数据 208 | */ 209 | pushData(data) { 210 | this.queue.push(data); 211 | if (this.canFeed) this.feedNext(); 212 | } 213 | 214 | /** 215 | * 喂数据 216 | * append的时候遇到The HTMLMediaElement.error attribute is not null就是数据时间戳有问题 217 | */ 218 | feedNext() { 219 | if (!this.queue || !this.queue.length) return 220 | if (!this.sourceBuffer || this.sourceBuffer.updating) return; 221 | if (!this.canFeed) return; 222 | 223 | try { 224 | const data = this.queue.shift(); 225 | this.sourceBuffer.appendBuffer(data); 226 | this.canFeed = false; 227 | } catch (e) { 228 | console.log(e); 229 | this.queue = []; 230 | this.onReset(this.number, this.videoId); 231 | } 232 | } 233 | 234 | /** 235 | * 处理延时或画面卡主 236 | */ 237 | processDelay() { 238 | if (!this.sourceBuffer || !this.sourceBuffer.buffered.length || this.sourceBuffer.updating) return; 239 | 240 | const end = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length - 1); 241 | const current = this.mediaPlayer.currentTime; 242 | // 解决延迟并防止画面卡主 243 | if (Math.abs(end - current) >= 1.8) { 244 | this.mediaPlayer.currentTime = end - 0.01; 245 | } 246 | } 247 | 248 | /** 249 | * 移除缓存 250 | */ 251 | removeBuffer() { 252 | if (!this.sourceBuffer || !this.sourceBuffer.buffered.length || this.sourceBuffer.updating) return; 253 | 254 | const length = this.sourceBuffer.buffered.length; 255 | const firstStart = this.sourceBuffer.buffered.start(0); 256 | const firstEnd = this.sourceBuffer.buffered.end(0); 257 | const lastStart = this.sourceBuffer.buffered.start(this.sourceBuffer.buffered.length - 1); 258 | const lastEnd = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length - 1); 259 | const currentTime = this.mediaPlayer.currentTime; 260 | 261 | if (Math.abs(firstStart - lastEnd) > 47000) { 262 | this.sourceBuffer.remove(firstEnd + 10, lastEnd); 263 | } else if (currentTime - firstStart > 120 && lastEnd > currentTime) { 264 | this.sourceBuffer.remove(firstStart, lastEnd - 10) 265 | } 266 | } 267 | } -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/main/resources/templates/client.html: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | websocket 31 | 32 | 33 | 34 |

RTSP + H264 + FMP4 + WebSocket + MSE + WEB

35 |

采用的通信库: https://github.com/xingshuangs/iot-communication
36 |

37 | 38 | 39 | 40 | 41 |
42 |
43 |
44 |
45 | 视频通道选择: 46 | 51 | 52 | 53 |
54 | 55 |
56 |
57 |
58 | 视频通道选择: 59 | 64 | 65 | 66 |
67 | 68 |
69 |
70 |
71 | 视频通道选择: 72 | 77 | 78 | 79 |
80 | 81 |
82 |
83 |
84 | 视频通道选择: 85 | 90 | 91 | 92 |
93 | 94 |
95 |
96 | 97 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/test/java/com/github/xingshuangs/rtsp/starter/RtspWebsocketServerStarterApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.github.xingshuangs.rtsp.starter; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class RtspWebsocketServerStarterApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /rtsp-websocket-server-starter/src/test/java/com/github/xingshuangs/rtsp/starter/model/RtspMessageTest.java: -------------------------------------------------------------------------------- 1 | package com.github.xingshuangs.rtsp.starter.model; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.github.xingshuangs.rtsp.starter.enums.ERtspMessageType; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.junit.Assert; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.util.Arrays; 12 | import java.util.HashSet; 13 | import java.util.Set; 14 | import java.util.stream.Collectors; 15 | 16 | import static org.junit.jupiter.api.Assertions.*; 17 | 18 | 19 | class RtspMessageTest { 20 | 21 | @Test 22 | void getType() throws JsonProcessingException { 23 | 24 | ObjectMapper objectMapper = new ObjectMapper(); 25 | RtspMessage message = new RtspMessage<>(); 26 | message.setType(ERtspMessageType.QUERY); 27 | message.setContent("channel"); 28 | String res = objectMapper.writeValueAsString(message); 29 | RtspMessage readValue = objectMapper.readValue(res, new TypeReference>() { 30 | }); 31 | assertEquals(readValue.getType(), message.getType()); 32 | assertEquals(readValue.getContent(), message.getContent()); 33 | } 34 | 35 | @Test 36 | void test1() { 37 | Set list = new HashSet<>(); 38 | list.add(1); 39 | list.add(2); 40 | String join = StringUtils.join(list.stream().map(String::valueOf).collect(Collectors.toList()), ","); 41 | System.out.println(join); 42 | } 43 | } --------------------------------------------------------------------------------