通过java代码实现HTML页面实时预览海康威视摄像头监控视频
一、背景
项目中在水泵站安装了两个海康摄像头,现在想要将海康摄像头的监控在系统web页面中实时预览展示,方便运维人员查看。
二、开发准备
1、网络
摄像头必须能连接网络上网,这里还有两种情况:
(1)可以给摄像头设置独立ip,这样就可以直接访问摄像头取流。本文采用这种方式
(2)摄像头不能设置独立ip,则必须使用海康威视基于私有ISUP协议开发的SDK,动态库形式提供,适用于“硬件设备没有固定IP地址”的网络环境。这里暂时不讨论。
2、取流
从摄像头取流主要两种方法:
(1)海康RTSP协议取流格式:采用这种方式不需要特别准备工具,本文采用这种方式,因为最简单易上手。
(2)海康SDK取流:采用此方式需要下载海康SDK。本文暂时不讨论,实际上大多数基于海康摄像头的功能都应该使用海康SDK。
海康开放平台
3、视频码流转流
取流得到的是RTSP协议,需要转换成web页面可以播放的HLS流,这里使用转码工具:FFmpeg。FFmpeg 是一个用于处理视频、音频等多媒体文件的开源工具包。它支持几乎所有的多媒体格式转换、剪辑和编辑,是开发者和多媒体工作者必备的工具。
这里可以参考该文章进行安装:FFmpeg 超级详细安装与配置教程(Windows 系统)_windows安装ffmpeg-CSDN博客
三、整体方案
1、海康摄像头设置
现场通过网线连接摄像头,通过海康的视频工具连接登录并对基础信息进行设置
1.设置登录账号密码、端口号、以及摄像头的ip(用于系统取流访问摄像头的必要信息)
2.调整视频格式为H.264
2、海康RTSP协议取流格式
【前提条件】第三方直接对接网络摄像机、硬盘录像机等视频类设备,且设备支持RTSP协议取流。
【对接说明】RTSP是标准协议,拼接好的预览或回放URL可以使用VLC测试,VLC测试正常,说明连接是没有问题的,后续开发集成需要三方自己实现。
【问题排查】如果RTSP协议取流有问题:
1) 先确认取流URL格式是否正确;
2) 建议局域网同网段下直接使用海康播放器测试,播放器下载地址:https://partners.hikvision.com/tools/tooldetail?id=599911712033406976,如果还是不行,可以抓包分析,Windows系统下使用wireshark抓包,Linux系统下使用tcpdump抓包。
【取流说明】
1) 【新版本】新版本URL,通道号全部按顺序从1开始。
URL中携带特殊字符,如”#、@”等。替换成转义字符,#——>%23 @——>%40,在线转换网址:HTML URL Encoding Reference
URL规定:rtsp://username:password@[address]:[port]/Streaming/Channels/[id](?parm1=value1&parm2-=value2…)
注:VLC可以支持解析URL里的用户名密码,实际发给设备的RTSP请求不支持带用户名密码。
详细描述:
举例说明:
通道01主码流:
rtsp://admin:abc12345@172.6.22.234:554/Streaming/Channels/101?transportmode=unicast
通道01子码流:
rtsp://admin:abc12345@172.6.22.234:554/Streaming/Channels/102?transportmode=unicast(单播)
rtsp://admin:abc12345@172.6.22.106:554/Streaming/Channels/102?transportmode=multicast (多播)
rtsp://admin:abc12345@172.6.22.106:554/Streaming/Channels/102 (?后面可省略,默认单播)
通道01第3码流:
rtsp://admin:abc12345@172.6.22.234:554/Streaming/Channels/103?transportmode=unicast
零通道主码流(零通道无子码流):
rtsp://admin:12345@172.6.22.106:554/Streaming/Channels/001
2) 【老版本】老版本URL,64路以下的NVR的IP通道的通道号从33开始,64路以及以上路数的NVR的IP通道的通道号从1开始。
URL规定:
rtsp://username:password@[ipaddress]/[videotype]/ch[number]/[streamtype]
注:VLC可以支持解析URL里的用户名密码,实际发给设备的RTSP请求不支持带用户名密码。
详细描述:
举例说明:
通道01主码流:
rtsp://admin:test1234@172.6.22.106:554/h264/ch01/main/av_stream
通道01子码流:
rtsp://admin:test1234@172.6.22.106:554/h264/ch01/sub/av_stream
通道01第3码流:
rtsp://admin:test1234@172.6.22.106:554/h264/ch01/stream3/av_stream
IP通道01的主码流:
rtsp://admin:test1234@172.6.22.106:554/h264/ch33/main/av_stream
IP通道01的子码流:
rtsp://admin:test1234@172.6.22.106:554/h264/ch33/sub/av_stream
零通道主码流(零通道无子码流):
rtsp://admin:test1234@172.6.22.106:554/h264/ch0/main/av_stream
3、java代码实现海康RTSP取流以及FFmpeg转码
1、海康设备账号和密码配置
import java.io.UnsupportedEncodingException;/** * @author: xxm * @description:海康设备账号和密码 * @date: 2025/2/20 16:49 */public class HCNetDeviceConUtil { // 登录IP public static final String m_sDeviceIP = \"192.168.0.12\"; // (登录IP 例如 192.168.0.1,它可以用来组网,可以在海康后台组建由这个ip控制的某几个海康摄像头) // 登录名 public static final String USERNAME = \"admin\"; // (例如 admin) // 密码 public static final String PASSWORD = \"123456\"; // (例如 123456) // 设备端口号 public static final Integer PORT = 8000; // 加载海康HCNetSDK.dll文件的路径 public static final String loadLibrary = HCNetSDKPath.DLL_PATH; public static class HCNetSDKPath { public static String DLL_PATH; /*下面这个是加载dll文件的 ,也就是上面的第3步(做了第3步可以不要这个static里面的内容,但是用这个把第3步换成工具类加载更加的方便后续的维护,所以我们把第3步的加载路径换成: HCNetSDK INSTANCE = (HCNetSDK) Native.loadLibrary(HCNetDeviceConUtil.loadLibrary, HCNetSDK.class); */ static { String path = (HCNetSDKPath.class.getResource(\"/HCNetSDK/HCNetSDK.dll\").getPath()) .replaceAll(\"%20\", \" \") .substring(1) .replace(\"/\", \"\\\\\"); try { DLL_PATH = java.net.URLDecoder.decode(path, \"utf-8\"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } }}
2、取流和转流转码综合类
代码主要实现通过RTSP协议从海康摄像头取流,此处可以像我一样,主动调用的时候才去取流,也可以在服务启动的时候后台服务启动自动读取,这个根据自己的需求来。取流以后需要启动FFmpeg程序,启动之前最好先强制关闭FFmpeg程序,防止重复启动多个程序。
import com.ggnykj.smartems.cloud.hcnetsdk.Common.FFmpegUtils;import com.ggnykj.smartems.cloud.hcnetsdk.Common.HCNetDeviceConUtil;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import java.io.IOException;import java.util.concurrent.TimeUnit;/** * @author: xxm * @description:取流和转流转码 * @date: 2025/3/21 17:42 */@RestController(value = \"/stream\")public class StreamController { @Autowired private FFmpegUtils FFmpegUtils; public StreamController() { // 初始化 FFmpegUtils 实例 this.FFmpegUtils = new FFmpegUtils(); } /** * 启动转码 * http://localhost:8080/start?rtspUrl=rtsp://admin:12345@192.168.1.64/Streaming/Channels/1&outputDir=/tmp/hls */ @GetMapping(\"/start\") public String startStream() { // 停止已有进程(防止重复启动) FFmpegUtils.stopFFmpeg(); try { // 延迟 1 秒后执行任务 语义更清晰 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } //摄像头RTSP地址 通道:101/1 String rtspUrl = \"rtsp://\"+HCNetDeviceConUtil.USERNAME+\":\"+HCNetDeviceConUtil.PASSWORD+\"@\"+ HCNetDeviceConUtil.m_sDeviceIP+\":554/Streaming/Channels/101\"; //视频转码流输出目录 String outputDir = \"D:/temp/hls\"; // 启动新转码任务 try { FFmpegUtils.startRtspToHls(rtspUrl, outputDir); } catch (IOException e) { throw new RuntimeException(e); } return \"转码已启动,HLS地址:http://your-server/hls/stream.m3u8\"; } /** * 停止转码 */ @GetMapping(\"/stop\") public String stopStream() { FFmpegUtils.stopFFmpeg(); return \"转码已停止\"; }}
配置文件记得配置视频文件输出目录:
spring: resources: static-locations: classpath:/static/,classpath:/views/,file:///D:/temp/hls/ #linux写法 #file:/tmp/hls/
3、FFmpeg转码工具类
工具类中,首先要配置FFmpeg可执行文件路径,linux和windos根据实际情况使用调整,然后是关闭进程方法和转码方法。转码方法将RTSP流转成HLS流,后者可以在web页面播放。转码方法中的几个需要注意的参数:
-hls_list_size 30
+ delete_segments
MEDIA-SEQUENCE
动态递增import org.springframework.stereotype.Component;import java.io.IOException;import java.util.ArrayList;import java.util.List;/** * @author: xxm * @description:FFmpeg转码工具类 * @date: 2025/3/21 17:41 */@Componentpublic class FFmpegUtils { // FFmpeg可执行文件路径(根据系统调整) // Linux/macOS private static final String FFMPEG_PATH = \"ffmpeg\"; // Windows //private static final String FFMPEG_PATH = \"D:\\\\worksoft\\\\ffmpeg-7.0.2-full_build\\\\bin\\\\ffmpeg.exe\"; private Process ffmpegProcess; /** * 启动RTSP转HLS任务 * @param rtspUrl RTSP地址(需包含账号密码,如rtsp://admin:password@192.168.1.64:554/...) * @param outputDir HLS输出目录(如/var/www/html/hls) * @return 进程对象(用于后续管理) */ public Process startRtspToHls(String rtspUrl, String outputDir) throws IOException { List command = new ArrayList(); command.add(FFMPEG_PATH); command.add(\"-i\"); command.add(rtspUrl); // 输入RTSP地址 command.add(\"-c:v\"); command.add(\"libx264\"); // 视频编码器 command.add(\"-preset\"); command.add(\"ultrafast\"); // 编码速度优先(降低延迟) command.add(\"-tune\"); command.add(\"zerolatency\"); // 零延迟优化 command.add(\"-c:a\"); command.add(\"aac\"); // 音频编码器 command.add(\"-f\"); command.add(\"hls\"); // 输出格式为HLS command.add(\"-hls_time\"); //command.add(\"2\"); // 每个分片2秒 command.add(\"4\"); // 每个分片4秒 command.add(\"-hls_list_size\"); command.add(\"30\"); // 保持30个分片在列表中 //command.add(\"0\"); // 保持全部个分片在列表中 command.add(\"-hls_flags\"); command.add(\"delete_segments\"); // 自动删除旧分片 //command.add(\"delete_segments+append_list\"); // 自动删除旧分片+动态更新播放列表 //command.add(\"delete_segments\"); // 自动删除旧分片+动态更新播放列表 //command.add(\"append_list\"); // 自动删除旧分片+动态更新播放列表 //command.add(\"-hls_playlist_type\"); //command.add(\"event\"); // 禁用直播模式限制 强制解除分片数量限制 command.add(outputDir + \"/stream.m3u8\"); // 输出文件路径 command.add(\"-loglevel\"); command.add(\"debug\"); // 输出内部解析过程:ml-citation{ref=\"2,8\" data=\"citationList\"} ProcessBuilder pb = new ProcessBuilder(command); pb.redirectErrorStream(true); // 合并错误流到标准输出 ffmpegProcess = pb.start(); return ffmpegProcess; } /** * 停止转码进程 */ public void stopFFmpeg() { System.out.println(\"=关闭stopFFmpeg===\"); if (ffmpegProcess != null && ffmpegProcess.isAlive()) { System.out.println(\"=关闭stopFFmpeg===成功\"); ffmpegProcess.destroyForcibly(); } // 补充系统级清理 killOrphanedFFmpeg(); } /** * 系统级清理残留进程(跨平台) */ private void killOrphanedFFmpeg() { try { String os = System.getProperty(\"os.name\").toLowerCase(); if (os.contains(\"win\")) { Runtime.getRuntime().exec(\"taskkill /F /IM ffmpeg.exe\"); } else { Runtime.getRuntime().exec(\"pkill -9 ffmpeg\"); } } catch (IOException e) { // 日志记录异常 System.out.println(\"=关闭killOrphanedFFmpeg===异常:\"+e.getMessage()); } }}
4、web页面播放器播放视频
1、HTML 页面播放 HLS 流
使用 hls.js
库在网页中播放 HLS 流:
<!----> #video::-webkit-media-controls-timeline { display: none; } #video::-webkit-media-controls-current-time { display: none !important; /* 隐藏当前时间、剩余时间、静音按钮和全屏按钮 */ } #video::-webkit-media-controls-time-remaining { display: none !important; /* 隐藏当前时间、剩余时间、静音按钮和全屏按钮 */ } /*#video::-webkit-media-controls-mute-button,*/ /*#video::-webkit-media-controls-fullscreen-button */ /*{*/ /* display: none; !* 隐藏当前时间、剩余时间、静音按钮和全屏按钮 *!*/ /*}*/ const video = document.getElementById(\'video\'); //const hlsUrl = \'/stream.m3u8?t=\'+ Date.now(); const hlsUrl = \'/stream.m3u8\'; if (Hls.isSupported()) { const hls = new Hls( { //拦截所有请求 xhrSetup: (xhr, url) => { xhr.setRequestHeader(\'Cache-Control\', \'no-cache\'); // 针对 .m3u8 分片请求追加时间戳 if (url.endsWith(\'.m3u8\')) { const newTsUrl = url+\'?t=\'+Date.now(); xhr.open(\'GET\', newTsUrl, true); } } } ); hls.loadSource(hlsUrl); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => video.play()); // //添加缓存禁用头 // video.addEventListener(\'progress\', () => { // if (hls) hls.on(Hls.Events.FRAG_CHANGED, () => hls.loadSource(hlsUrl)); // }); } else if (video.canPlayType(\'application/vnd.apple.mpegurl\')) { video.src = hlsUrl; video.addEventListener(\'loadedmetadata\', () => video.play()); }
2、关键点
- 引入 hls.js:通过
标签加载最新版
hls.js(可以将hls.js下载下来,避免每次网络加载)
- 播放器逻辑:
- 检测浏览器是否支持 HLS
- 使用
hls.js
或原生 HTML5 播放器 - 自动播放视频流
- 注意视频缓存问题:
因为是直播预览播放,所以不能一直保存转码完成的视频,所以在转码时设置了只保留30个分片,也就是视频转码完成30个分片后,会从最开始的视频分片开始删除,然后重新生成新的视频,这是一个循环往复的过程。
但是因为浏览器的缓存原因,会导致播放stream.m3u8视频时,第一次加载以后,他的文件中只包含了一开始的30个视频分片,后续加载播放stream.m3u8视频一直在加载这个缓存文件,而实际文件生成的视频分片,并没有被浏览器加载,导致无法正常播放。所以必须在加载的stream.m3u8文件加一个时间后缀,这样每次播放加载stream.m3u8文件时都会是最新的文件,而不是浏览器缓存。
stream.m3u8文件内容:
#EXTM3U#EXT-X-VERSION:3#EXT-X-TARGETDURATION:10#EXT-X-MEDIA-SEQUENCE:36#EXTINF:10.033444,stream36.ts#EXTINF:10.033444,stream37.ts#EXTINF:10.033444,stream38.ts#EXTINF:10.033444,stream39.ts#EXTINF:10.033444,stream40.ts#EXTINF:10.033444,stream41.ts#EXTINF:10.033444,stream42.ts#EXTINF:10.033444,stream43.ts#EXTINF:10.033456,stream44.ts#EXTINF:10.033444,stream45.ts#EXTINF:10.033444,stream46.ts#EXTINF:10.033444,stream47.ts#EXTINF:10.033444,stream48.ts#EXTINF:10.033444,stream49.ts#EXTINF:10.033444,stream50.ts#EXTINF:10.033444,stream51.ts#EXTINF:10.033444,stream52.ts#EXTINF:10.033444,stream53.ts#EXTINF:10.033444,stream54.ts#EXTINF:10.033444,stream55.ts#EXTINF:10.033444,stream56.ts#EXTINF:10.033444,stream57.ts#EXTINF:10.033444,stream58.ts#EXTINF:10.033444,stream59.ts#EXTINF:10.033444,stream60.ts#EXTINF:10.033444,stream61.ts#EXTINF:10.033444,stream62.ts#EXTINF:10.033444,stream63.ts#EXTINF:10.033444,stream64.ts#EXTINF:9.511600,stream65.ts#EXT-X-ENDLIST
3、转码完成的视频文件
播放stream.m3u8视频,它会根据下边的分片一个一个加载视频,生成完整的预览视频流。
四、总结
本文实现海康摄像头的监控在系统web页面中实时预览展示,主要通过java代码实现海康摄像头RTSP取流以及FFmpeg转码,最终HTML 页面基于hls.js库
播放 HLS 流,实现视频实时预览。
最终web页面的视频如下: