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>

三、效果

 

 

posted @ 2022-08-17 16:35  老邝  阅读(3600)  评论(3编辑  收藏  举报