当前位置:   article > 正文

spring boot集成javacv + websocket实现实时视频推流回放(延时1-2秒)_海康视频 netty http flv

海康视频 netty http flv

最近项目需要实时直播和回放,集成海康威视摄像头:(适合少量用户,或者内部系统使用)

  1. <!-- 视频处理库 -->
  2. <dependency>
  3. <groupId>org.bytedeco</groupId>
  4. <artifactId>javacv-platform</artifactId>
  5. <version>1.5.1</version>
  6. </dependency>

这里是利用javacv抓取rtsp地址的视频流,通过转换成图片使用websocket实时推送

首先了解rtsp地址,这里举例就用海康威视的规则

  1. 通道号下面详细介绍
  2. # 单播
  3. rtsp://{用户名}:{密码}@{IP地址}:{端口号}/Streaming/Channels/{通道号}?transportmode=unicast
  4. # 多播
  5. rtsp://{用户名}:{密码}@{IP地址}:{端口号}/Streaming/Channels/{通道号}?transportmode=multicast
  6. # 分时获取
  7. rtsp://{用户名}:{密码}@{IP地址}:{端口号}/Streaming/tracks/{通道号}?starttime=20191008t063812z&endtime=20191008t064816z
  8. 上面地址获取不到时可以试试下面的其他版本地址:
  9. rtsp://{用户名}:{密码}@{IP地址}:{端口号}/Streaming/Unicast/channels/{通道号}
  10. rtsp://{用户名}:{密码}@{IP地址}:{端口号}/h264/{通道号}/main/av_stream

这里通道号分类是IP通道号和模拟通道号,如果是12年以前的老机器,地址建议使用最后一个获取实时视频

可以使用官方提供的java demo 来查看,测试是否能获取视频

这里有打包好的jar包和环境可以直接运行jar包使用,解压后将里面内容放置在System32下,cmd执行jar

地址 百度网盘 请输入提取码  密码 67v1

下图就是IP通道号 若只是Camera20 则为模拟通道号

通道号可以参考这里最新海康摄像机、NVR、流媒体服务器、回放取流RTSP地址规则说明 - github.com/starRTC - 博客园

接下来就是java代码:

首先yml配置动态RTSP地址

  1. myconfig:
  2. rtsp: rtsp://%s:%s@%s:%s/Streaming/Channels/%s01
  3. replay-rtsp: rtsp://%s:%s@%s:%s/Streaming/tracks/%s01?starttime=%s&endtime=%s

媒体工具类:

  1. @Slf4j
  2. @Component
  3. public class MediaUtils {
  4. /**
  5. * 直播摄像机id集合,避免重复拉流
  6. */
  7. private static Set<Long> liveSet = new ConcurrentSet<>();
  8. /**
  9. * 用于构造回放rtsp地址
  10. */
  11. @Value("${myconfig.replay-rtsp}")
  12. private String rtspReplayPattern;
  13. /**
  14. * 视频帧率
  15. */
  16. public static int frameRate = 15;
  17. /**
  18. * 视频宽度
  19. */
  20. public static int frameWidth = 480;
  21. /**
  22. * 视频高度
  23. */
  24. public static int frameHeight = 270;
  25. /**
  26. * 摄像机直播
  27. * @param rtsp 摄像机直播地址
  28. * @param cameraName 摄像机名称
  29. * @param cameraId 摄像机id
  30. * @throws Exception e
  31. */
  32. @Async
  33. public void live(String rtsp, String cameraName, Long cameraId) throws Exception {
  34. if (liveSet.contains(cameraId)) {
  35. return;
  36. }
  37. liveSet.add(cameraId);
  38. FFmpegFrameGrabber grabber = createGrabber(rtsp);
  39. startCameraPush(grabber, cameraName, cameraId);
  40. }
  41. /**
  42. * 构造视频抓取器
  43. * @param rtsp 拉流地址
  44. * @return
  45. */
  46. public FFmpegFrameGrabber createGrabber(String rtsp) {
  47. // 获取视频源
  48. FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(rtsp);
  49. grabber.setOption("rtsp_transport","tcp");
  50. //设置帧率
  51. grabber.setFrameRate(frameRate);
  52. //设置获取的视频宽度
  53. grabber.setImageWidth(frameWidth);
  54. //设置获取的视频高度
  55. grabber.setImageHeight(frameHeight);
  56. //设置视频bit率
  57. grabber.setVideoBitrate(2000000);
  58. return grabber;
  59. }
  60. /**
  61. * 推送图片(摄像机直播)
  62. * @param grabber
  63. * @throws Exception
  64. */
  65. @Async
  66. public void startCameraPush(FFmpegFrameGrabber grabber, String cameraName, Long cameraId) throws Exception {
  67. Java2DFrameConverter java2DFrameConverter = new Java2DFrameConverter();
  68. try {
  69. grabber.start();
  70. int i = 1;
  71. while (liveSet.contains(cameraId)) {
  72. Frame frame = grabber.grabImage();
  73. if (null == frame) {
  74. continue;
  75. }
  76. BufferedImage bufferedImage = java2DFrameConverter.getBufferedImage(frame);
  77. byte[] bytes = imageToBytes(bufferedImage, "jpg");
  78. //使用websocket发送图片数据
  79. LiveWebsocket.sendImage(ByteBuffer.wrap(bytes), cameraId);
  80. }
  81. } finally {
  82. if (grabber != null) {
  83. grabber.stop();
  84. }
  85. }
  86. }
  87. /**
  88. * 图片转字节数组
  89. * @param bImage 图片数据
  90. * @param format 格式
  91. * @return 图片字节码
  92. */
  93. private byte[] imageToBytes(BufferedImage bImage, String format) {
  94. ByteArrayOutputStream out = new ByteArrayOutputStream();
  95. try {
  96. ImageIO.write(bImage, format, out);
  97. } catch (IOException e) {
  98. e.printStackTrace();
  99. }
  100. return out.toByteArray();
  101. }
  102. /**
  103. * 回放视频播放超期检查
  104. * @param userId 用户id
  105. * @return
  106. */
  107. private boolean replayOverTime(Integer userId) {
  108. if (replayMap.containsKey(userId)) {
  109. Long updateTime = replayMap.get(userId);
  110. if (updateTime != null) {
  111. if (System.currentTimeMillis() - updateTime < 10000) {
  112. return false;
  113. }
  114. }
  115. }
  116. return true;
  117. }
  118. /**
  119. * 构造监控回放查询字段
  120. * @param date 时间
  121. * @param start
  122. * @return
  123. */
  124. private String formatPullTime(Date date, boolean start) {
  125. Calendar calendar = Calendar.getInstance();
  126. if (date != null) {
  127. calendar.setTime(date);
  128. }
  129. if (start) {
  130. calendar.add(Calendar.SECOND, -10);
  131. } else {
  132. calendar.add(Calendar.SECOND, 10);
  133. }
  134. //海康威视取回放的时间格式
  135. SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd#HHmmss$");
  136. String ret = sdf.format(calendar.getTime());
  137. ret = ret.replace("#", "t");
  138. ret = ret.replace("$", "z");
  139. return ret;
  140. }
  141. /**
  142. * 回放视频播放超期检查
  143. * @param userId 用户id
  144. * @return
  145. */
  146. private boolean replayOverTime(Integer userId) {
  147. if (replayMap.containsKey(userId)) {
  148. Long updateTime = replayMap.get(userId);
  149. if (updateTime != null) {
  150. if (System.currentTimeMillis() - updateTime < 10000) {
  151. return false;
  152. }
  153. }
  154. }
  155. return true;
  156. }
  157. /**
  158. * 监控回放
  159. * @param userId 用户id websocket 用户编号,作为心跳标识检测心跳
  160. * @param startDate 起始时间
  161. * @param endDate 结束时间
  162. * @param channel 通道号
  163. * @param username 用户名
  164. * @param password 密码
  165. * @param ip
  166. * @param port
  167. * @throws Exception e
  168. */
  169. @Async
  170. public void replayVideo(Integer userId, Date startDate, Date endDate,
  171. Integer channel, String username, String password, String ip, Integer port) throws Exception {
  172. Java2DFrameConverter java2DFrameConverter = new Java2DFrameConverter();
  173. FFmpegFrameGrabber grabber = null;
  174. try {
  175. if (grabber != null) {
  176. grabber.stop();
  177. }
  178. if (channel != null) {
  179. String st = formatPullTime(startDate, true);
  180. String et = formatPullTime(endDate, false);
  181. //构造rtsp回放流地址 username password ip port
  182. String rtsp = String.format(
  183. rtspReplayPattern,
  184. username,
  185. password,
  186. ip,
  187. port,
  188. channel,
  189. st,
  190. et
  191. );
  192. if (grabber != null) {
  193. grabber.stop();
  194. }
  195. grabber = createGrabber(rtsp);
  196. grabber.setTimeout(10000);
  197. grabber.start();
  198. //心跳消失停止推流
  199. while (!replayOverTime(userId)) {
  200. Frame frame = grabber.grabImage();
  201. if (null == frame) {
  202. continue;
  203. }
  204. BufferedImage bufferedImage = java2DFrameConverter.getBufferedImage(frame);
  205. byte[] bytes = imageToBytes(bufferedImage, "jpg");
  206. ByteBuffer buffer = ByteBuffer.wrap(bytes);
  207. //使用websocket发送图片数据
  208. ReplayWebsocke.sendImage(buffer, userId);
  209. }
  210. }
  211. } catch (Exception e){
  212. e.printStackTrace();
  213. } finally {
  214. if (grabber != null) {
  215. grabber.stop();
  216. }
  217. }
  218. }
  219. }

视频水印添加:

  1. @Component
  2. public class ImgMarker {
  3. /**
  4. * 视频水印图片
  5. */
  6. BufferedImage logoImg;
  7. private Font font;
  8. private Font font2;
  9. private FontDesignMetrics metrics;
  10. private FontDesignMetrics metrics2;
  11. @PostConstruct
  12. private void init() {
  13. // 加水印图片
  14. try {
  15. ImageIO.read(new File("图片地址"));
  16. } catch (IOException e) {
  17. e.printStackTrace();
  18. }
  19. font = new Font("黑体", Font.BOLD, 16);
  20. font2 = new Font("黑体", Font.BOLD, 24);
  21. metrics = FontDesignMetrics.getMetrics(font);
  22. metrics2 = FontDesignMetrics.getMetrics(font2);
  23. }
  24. /**
  25. * 加水印
  26. * @param bufImg 视频帧
  27. */
  28. public void mark(BufferedImage bufImg) {
  29. if (bufImg == null || logoImg == null) {
  30. return;
  31. }
  32. int width = bufImg.getWidth();
  33. int height = bufImg.getHeight();
  34. Graphics2D graphics = bufImg.createGraphics();
  35. graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
  36. graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
  37. //设置图片背景
  38. graphics.drawImage(bufImg, 0, 0, width, height, null);
  39. //添加右上角水印
  40. graphics.drawImage(logoImg, width - 130, 8, 121, 64, null);
  41. }
  42. /**
  43. *
  44. * @param bufImg 视频帧
  45. */
  46. public void markTag(BufferedImage bufImg, String msg, int videoWidth) {
  47. int width = bufImg.getWidth();
  48. int height = bufImg.getHeight();
  49. Graphics2D graphics = bufImg.createGraphics();
  50. graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
  51. graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
  52. //设置图片背景
  53. graphics.drawImage(bufImg, 0, 0, width, height, null);
  54. //设置由上方标签号
  55. graphics.setColor(Color.orange);
  56. if (videoWidth <= 400) {
  57. graphics.setFont(font2);
  58. graphics.drawString(msg, width - metrics2.stringWidth(tagId) - 24, metrics2.getAscent());
  59. } else {
  60. graphics.setFont(font);
  61. graphics.drawString(msg, width - metrics.stringWidth(msg) - 12, metrics.getAscent());
  62. }
  63. graphics.dispose();
  64. }
  65. }

至此可以调用了(还有其他方式是直接调用SDK和推送视频流媒体服务器,后续文章更新)

websocket 涉及到高并发阻塞情况 后续更新,建议使用netty

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

闽ICP备14008679号