rtsp协议转换m3u8
目前项目中使用海康的摄像头,但需要提供实时预览。目前通过转换协议实现预览。同时能够尽量减少服务器的压力,比如生成的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>
三、效果