目前项目中使用海康的摄像头,但需要提供实时预览。目前通过转换协议实现预览。同时能够尽量减少服务器的压力,比如生成的ts文件个数。

思路

通过ffmpeg 将rtsp协议转换成hls协议

具体步骤

1、java调用FFmpeg 命令进行协议转换

2、解决java调用runtime时,无法自主结束子进程问题

3、解决videojs中播放m3u8时出现 (CODE:3 MEDIA_ERR_DECODE) The media playback was aborted due to a corruption problem or because the media used features your browser did not support)

一、java部分

  1 package com.hxq.device.rtsp;
2
3 import com.hxq.common.config.RuoYiConfig;
4 import com.hxq.common.utils.StringUtils;
5 import com.hxq.common.utils.SystemCmdHelper;
6 import com.hxq.common.utils.http.HttpUtils;
7 import lombok.extern.slf4j.Slf4j;
8 import org.apache.commons.compress.utils.IOUtils;
9 import org.apache.commons.io.FileUtils;
10 import org.apache.http.client.utils.DateUtils;
11 import org.springframework.scheduling.annotation.EnableScheduling;
12 import org.springframework.scheduling.annotation.Scheduled;
13 import org.springframework.stereotype.Service;
14 import org.springframework.transaction.annotation.Transactional;
15
16 import javax.annotation.PreDestroy;
17 import java.io.BufferedReader;
18 import java.io.File;
19 import java.io.IOException;
20 import java.io.InputStreamReader;
21 import java.util.Date;
22 import java.util.concurrent.ConcurrentHashMap;
23
24 /**
25 * rtsp 转 hlv协议
26 *
27 * @author kzw
28 */
29 @Service
30 @Slf4j
31 @EnableScheduling
32 public class RtspConvert {
33 //转换map
34 private static ConcurrentHashMap<String, CoverThread> coverMap = new ConcurrentHashMap<>();
35 //每次转换3小时,3小时之后自动停止
36 private static final String ffmpegCmd = "ffmpeg -timeout 3000000 -i \"%s\" -c copy -t 03:00:00 -f hls -hls_time 5.0 -hls_list_size 5 -hls_flags 2 %s";
37
38 @PreDestroy
39 public void closeProcess() {
40 log.info("关闭ffmpeg转换进程,java程序不一定关闭process进程");
41 for (String ip : coverMap.keySet()) {
42 try {
43 log.error("开始停止{}", ip);
44 coverMap.get(ip).stopTask();
45 } catch (Exception e) {
46 e.printStackTrace();
47 }
48 }
49 }
50
51 /**
52 * ffmpeg -i "rtsp://admin:xxx@192.168.0.251:554/Streaming/Channels/101" -c copy -f hls -hls_time 5.0 -hls_list_size 5 -hls_flags 2 F:\resources\hls\2000\live.m3u8
53 */
54 /**
55 * 检查设备ip是否能正常访问
56 */
57 private boolean checkDeviceOnline(String ip) {
58 String res = HttpUtils.sendGet("http://" + ip);
59 if (StringUtils.isNotBlank(res)) {
60 return true;
61 }
62 return false;
63 }
64
65 /**
66 * 转换rtsp并获取hls文件路径
67 */
68 public String rtsp2Hls(String ip, String userName, String pwd) {
69 if (coverMap.containsKey(ip)) {
70 CoverThread thread = coverMap.get(ip);
71 if (thread == null || thread.getTaskState() != CoverThread.running) {
72 } else {
73 return StringUtils.replace(thread.getM3U8File(), RuoYiConfig.getProfile(), "");
74 }
75 }
76 String rtspUrl = "rtsp://" + userName + ":" + pwd + "@" + ip + ":554/Streaming/Channels/101";
77 String m3u8File = RuoYiConfig.getProfile() + File.separator + "hls"
78 + File.separator + ip.replaceAll("\\.", "_") + File.separator + DateUtils.formatDate(new Date(), "yyyyMMddHHmm") + "live.m3u8";
79 startTransform(ip, rtspUrl, m3u8File, userName, pwd);
80 CoverThread thread = coverMap.get(ip);
81 if (thread != null) {
82 return StringUtils.replace(thread.getM3U8File(), RuoYiConfig.getProfile(), "");
83 }
84 return null;
85 }
86
87 /**
88 * 开启转换
89 */
90 private void startTransform(String ip, String rtspUrl, String m3u8Path, String userName, String pwd) {
91 log.info("转换rtsp, {},{},{}", ip, rtspUrl, m3u8Path);
92 String memKey = "startLive" + ip;
93 synchronized (memKey.intern()) {
94 if (coverMap.containsKey(ip)) {
95 stopTransform(ip);
96 }
97 CoverThread thread = new CoverThread(ip, rtspUrl, m3u8Path, userName, pwd);
98 coverMap.put(ip, thread);
99 thread.start();
100 }
101 }
102
103 /**
104 * 停止转换
105 */
106 public void stopTransform(String ip) {
107 String memKey = "startLive" + ip;
108 synchronized (memKey.intern()) {
109 if (coverMap.containsKey(ip)) {
110 CoverThread thread = coverMap.get(ip);
111 if (thread != null && thread.getTaskState() != CoverThread.fail) {
112 thread.stopTask();
113 }
114 }
115 }
116 }
117
118 /**
119 * 监控所有的转换线程
120 */
121 @Scheduled(cron = "0 0/8 * * * ?")
122 public synchronized void monitorThreads() {
123 for (String ip : coverMap.keySet()) {
124 CoverThread thread = coverMap.get(ip);
125 if (thread != null && thread.getTaskState() != CoverThread.running) {
126 //线程出现异常
127 rtsp2Hls(ip, thread.getUserName(), thread.getPwd());
128 }
129 }
130 }
131
132 /**
133 * 执行命令线程
134 */
135 private class CoverThread extends Thread {
136 private String ip;
137 private String rtspUrl;
138 private String m3u8File;
139 private String userName;
140 private String pwd;
141 private int taskState = 0; //运行状态 0未开始 1进行中 -1失败
142 private static final int notStart = 0;
143 private static final int running = 1;
144 private static final int fail = -1;
145 private Process process = null;
146
147 CoverThread(String ip, String rtspUrl, String m3u8File, String userName, String pwd) {
148 this.ip = ip;
149 this.rtspUrl = rtspUrl;
150 this.m3u8File = m3u8File;
151 this.userName = userName;
152 this.pwd = pwd;
153 setName("m3u8-" + ip);
154 this.taskState = notStart;
155 }
156
157 @Override
158 public void run() {
159 try {
160 FileUtils.forceMkdir(new File(m3u8File).getParentFile());
161 if (!checkDeviceOnline(ip)) {
162 log.warn("设备{},离线", ip);
163 this.taskState = fail;
164 return;
165 }
166 String command = String.format(ffmpegCmd, rtspUrl, m3u8File);
167 this.taskState = running;
168
169 //判断是操作系统是linux还是windows
170 String[] comds;
171 if (SystemCmdHelper.isWin()) {
172 comds = new String[]{"cmd", "/c", command};
173 // comds = new String[]{"cmd", "/c", "start", "/b", "cmd.exe", "/k", command};
174 } else {
175 comds = new String[]{"/bin/sh", "-c", command};
176 }
177
178 // 开始执行命令
179 log.info("执行命令:" + command);
180 process = Runtime.getRuntime().exec(comds);
181
182 //开启线程监听(此处解决 waitFor() 阻塞/锁死 问题)
183 new SystemCmdHelper.RunThread(process.getInputStream(), "INFO").start();
184 new SystemCmdHelper.RunThread(process.getErrorStream(), "ERROR").start();
185 int flag = process.waitFor();
186 log.info("结束{}", ip);
187 } catch (Exception e) {
188 log.error("出现异常" + e.getMessage(), e);
189 this.taskState = fail;
190 } finally {
191 if (process != null) {
192 try {
193 process.exitValue();
194 } catch (Exception e) {
195 }
196 try {
197 process.destroyForcibly();
198 } catch (Exception e) {
199 }
200 }
201 }
202 }
203
204 /**
205 * 获取任务执行状态
206 */
207 public int getTaskState() {
208 return taskState;
209 }
210
211 /**
212 * 立即停止任务
213 */
214 public void stopTask() {
215 if (process != null) {
216 try {
217 process.destroy();
218 } catch (Exception e) {
219 e.printStackTrace();
220 }
221 }
222 }
223
224 public String getM3U8File() {
225 return this.m3u8File;
226 }
227
228 public String getUserName() {
229 return userName;
230 }
231
232 public String getPwd() {
233 return pwd;
234 }
235 }
236
237 public static void main(String[] args) throws Exception {
238 RtspConvert convert = new RtspConvert();
239 String ip = "192.168.0.251";
240 String userName = "xxx";
241 String pwd = "xxx";
242 String m3u8 = convert.rtsp2Hls(ip, userName, pwd);
243 System.out.println("***********************************" + m3u8);
244 Thread.sleep(10 * 1000);
245 convert.stopTransform(ip);
246 System.out.println("************************************结束**************");
247 }
248 }

二、前端vue部分

<template>
<div class="app-container"> <!-- 视频播放 -->
<el-dialog title="实时播放" :visible.sync="openPlay" width="800px" :before-close="closePlayDialog" append-to-body>
<el-row>
<video-player v-if="hlsUrl != null" class="video-player vjs-custom-skin" ref="videoPlayer"
:playsinline="true" :options="playerOptions" @error="playError"></video-player>
</el-row>
</el-dialog>
</div>
</template> <script>
import { listDevice, getDevice, delDevice, addDevice, updateDevice,startTransform } from "@/api/biz/device";
import { queryAllClassRoom } from '@/api/biz/classRoom';
import { skipAiCamera, skipHikCamera } from '@/utils/ruoyi';
import 'videojs-contrib-hls' export default {
name: "Device",
dicts: ['device_type', 'device_states'],
data() {
return { playerOptions: {
// playbackRates: [0.5, 1.0, 1.5, 2.0], //可选择的播放速度
autoplay: false, //如果true,浏览器准备好时开始回放。
muted: false, // 默认情况下将会消除任何音频。
loop: false, // 视频一结束就重新开始。
preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
language: 'zh-CN',
aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
sources: [{
type: "application/x-mpegURL",
src: ''//url地址
}],
hls: true,
poster: "", //你的封面地址
// width: document.documentElement.clientWidth,
notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
controlBar: {
timeDivider: false,//当前时间和持续时间的分隔符
durationDisplay: false,//显示持续时间
remainingTimeDisplay: false,//是否显示剩余时间功能
fullscreenToggle: true //全屏按钮
}
},
hlsUrl: '',
openPlay: false,
};
},
created() { },
methods: { //播放监控画面
handlePlay(row) {
startTransform(row.id).then(res => {
let url = process.env.VUE_APP_BASE_API + res.msg.replaceAll("\\", "/");
// console.log("播放url",url);
this.openPlay = true;
this.playerOptions.sources[0].src = url;
// this.playerOptions.sources[0].src = "http://192.168.0.249:10000/hls/192_168_0_251/live.m3u8";
this.hlsUrl = url;
})
},
//关闭播放弹窗
closePlayDialog(done) {
try {
this.$refs.videoPlayer.player.pause();
this.$refs.videoPlayer.reset();
} catch(e) {
}
done();
},
playError(e) {
console.log("播放异常,自动重启播放器", e)
if (e.error_ && e.error_.code == 4) { } else { //当出现m3u8文件加载错误时,自动重新播放
this.$refs.videoPlayer.player.src(this.hlsUrl);
this.$refs.videoPlayer.player.load(this.hlsUrl);
this.$refs.videoPlayer.player.play();
}
}
}
};
</script>

三、效果

最新文章

  1. (一)开篇—杂谈WebGIS
  2. javase基础复习攻略《三》
  3. SQL2008 强烈要求限制最大内存
  4. LA 4731
  5. objective-c之各种数值
  6. 节点遍历 element traversal
  7. SiftGPU在Ubuntu和Windows下的编译与使用
  8. Python入门(白银篇)
  9. c# double decimal
  10. js 基础-&amp;&amp; || 逻辑与和逻辑或
  11. vue项目,npm install后,npm run dev报错问题
  12. 【转】android系统常用URI
  13. 字节顺序标记BOM
  14. [ IE浏览器兼容问题 ] Web Uploader 在IE、FireFox下点击上传没反应
  15. PostgreSQL学习----命令或问题小结
  16. web前端开发常用的几种图片格式及其使用规范
  17. anaconda不错的
  18. Unity3D动画面板编辑器状态属性对照表
  19. 进程Process之join、daemon(守护)、terminate(关闭)
  20. 《Python入门》Windows 7下Python Web开发环境搭建笔记

热门文章

  1. 使用VMware Converter Standalone P2V(物理机转换虚拟机)
  2. 《Terraform 101 从入门到实践》 Functions函数
  3. springcloud 10 spring cloud gateway02 基本使用
  4. ubuntu 备份系统
  5. 认识Spring MVC-概念-小demo
  6. dvwa靶场
  7. SX【2020.01.09】NOIP提高组模拟赛(day1)
  8. 一文吃透 Go 内置 RPC 原理
  9. C#获取enum描述信息
  10. pycharm软件基本使用python语法的注释变量的使用常量的使用变量的命名规范python的优化垃圾回收机制数据类型