记录监控摄像头的接入过程及web端播放
1.rtsp视频流网页播放概述
需求:当我们通过ONVIF协议,获取到了摄像头的rtsp流地址(长这样:rtsp://admin:123456789@192.168.9.16:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif)后,通过vlc播放器,我们可以查看监控视频内容,可是,我们应该如何在网页上查看视频内容呢?因为现在的浏览器都不支持rtsp流,因此我所选用的解决方案便是推流 + 转码
(1)转码推流工具ffmpeg(安装教程详见:https://www.cnblogs.com/h2285409/p/16982120.html),安装好之后,便可使用命令 ffmpeg -re -rtsp_transport tcp -i rtsp://admin:123456789@192.168.9.16:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif -c:v copy -c:a copy -f flv rtmp://127.0.0.1/live/16 将我们的rtsp视频流转码并推至流媒体服务器上,在这个命令中含有两个URL,前面的是我们的rtsp流地址,而后面的URL便是我们流媒体服务器的地址,以及一个-f参数,指定了我们视频流转码后的格式为flv
(2)流媒体服务器,主要调研了2款,一是整合了Rtmp模块的Nginx,二是SRS视频服务器,而我所选用的是SRS(官方文档:http://ossrs.net/lts/zh-cn/),在使用ffmpeg推流上SRS后,便可直接从SRS获得webRTC流地址(如本例:webrtc://192.168.4.23/live/16 ),为什么选用webRTC呢,因为它的延迟很低(也可以根据自己的需求选择其他的如http-flv或hls),然后,前端再通过webRTC播放器便可直接在页面上播放该直播流
SRS与ffmpeg参考:https://blog.csdn.net/diyangxia/article/details/120172920
ffmpeg进阶参考:https://segmentfault.com/a/1190000039782685
webRTC参考:https://www.cnblogs.com/ziyue7575/p/13927894.html
2.rtsp推流转码以及关闭推流相关代码实现
@Service
@Slf4j
public class MonitorServiceImpl {
//ffmpeg安装路径
@Value("${ffmpegPath}")
private String ffmpegPathPrefix;
//srs视频服务器地址
@Value("${srsAddress}")
private String srsAddress;
//srs-http-api端口,默认为1985
@Value("${srsHttpApiPort}")
private String srsHttpApiPort;
@Resource
private MonitorMapper monitorMapper;
@Resource
private RestTemplate restTemplate;
@Resource
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
private ConcurrentMap<String, TranscodeModel> transcodeMap = new ConcurrentHashMap<>();
private CopyOnWriteArrayList<ProcessOutputModel> processOutputList = new CopyOnWriteArrayList<>();
/**
* 进行推流转码
*/
public String transcode(String id) {
boolean success = false;
try {
if(transcodeMap.putIfAbsent(id, new TranscodeModel()) == null) {
Ipc ipc = monitorMapper.getIpcInfoById(id);
if(ipc == null) {
throw new RuntimeException("id为" + id + "的ipc不存在");
}
String rtspUrls = ipc.getRtspUrls();
if(!StringUtils.hasText(rtspUrls)) {
try {
//如果流地址不存在,则输入ipc的ip地址,端口号,帐号,密码以及传输协议进行获取
rtspUrls = OnvifUtil.getRTSPUrl(ipc.getIp(), ipc.getPort(), ipc.getAccount(), ipc.getPassword(), TransportProtocol.TCP);
} catch (Exception e) {
throw new RuntimeException("获取id为 " + id + " 的ipc的rtsp流地址失败,请检查该ipc是否开启或其配置信息是否正确");
}
}
if (!StringUtils.hasText(rtspUrls)) {
throw new RuntimeException("获取id为 " + id + "的ipc的rtsp流地址失败,请检查该摄像机是否开启或其配置信息是否正确");
}
//一般情况下获取到的ipc的rtsp流地址存在多个(主码流,辅码流或第三方码流等),我这里用了#号将它们进行分隔
String[] allRtspUrl = rtspUrls.split("#");
for (String rtspUrl : allRtspUrl) {
//使用ffprobe来获取流的一些信息,比如该流当前是否可用(在线),它的编码格式,分辨率以及fps等基本信息 将ffmpeg与ffprobe置于同一目录下
String requestStreamInfoCommand = String.format("%sffprobe %s", this.ffmpegPathPrefix, rtspUrl);
//用于判断该ipc是否在线
boolean online = true;
//用于判断该获取流信息的进程是否应该结束
long lastReadTime = System.currentTimeMillis();
log.info("准备请求流 {} 的信息,其命令为 {}", rtspUrl, requestStreamInfoCommand);
Process requestStreamInfoProcess = Runtime.getRuntime().exec(requestStreamInfoCommand);
BufferedReader reader = new BufferedReader(new InputStreamReader(requestStreamInfoProcess.getErrorStream()));
while (requestStreamInfoProcess.isAlive()) {
while (reader.ready()) {
String msg = reader.readLine().trim();
lastReadTime = System.currentTimeMillis();
log.info("流 {} 的信息为: {}", rtspUrl, msg);
if (msg.contains("Server returned 404 Not Found")) {
log.error("流 {} 不在线", rtspUrl);
online = false;
break;
}
}
if (!online) {
requestStreamInfoProcess.destroy();
} else if (System.currentTimeMillis() - lastReadTime > 6000) {
//预防异常情况,超过6s未读取到数据,结束
online = false;
requestStreamInfoProcess.destroy();
break;
}
}
if (online) {
//如果视频编码格式不为h264,则将其转码为h264格式,网页端才能播放,因此这里选用了libx264编码库
//-preset的参数主要调节编码速度和质量的平衡 -tune zerolatency表示设置零延时
String transcodeCommand = String.format("%sffmpeg -re -rtsp_transport tcp -i %s -c:a copy -c:v libx264 -preset ultrafast -tune zerolatency -f flv %s",this.ffmpegPathPrefix, rtspUrl, "rtmp://" + srsAddress + "/live/" + id);
//通过命令行执行推流转码
log.info("启动对ipc {} 的推流转码, 其命令为: {}", id, transcodeCommand);
Process process = Runtime.getRuntime().exec(transcodeCommand);
transcodeMap.put(id, new TranscodeModel(id, process, transcodeCommand));
//观察推流进程的输出日志
this.addProcessOutput(new ProcessOutputModel(id, process, new BufferedReader(new InputStreamReader(process.getErrorStream()))));
success = true;
break;
}
}
}
} catch (Exception e) {
this.removeProcessOutput(id);
this.closeTranscode(id);
throw new RuntimeException("启动对ipc " + id + " 推流转码失败,原因: " + e.getMessage());
}
if (!success) {
throw new RuntimeException("启动转码推流失败,原因: 设备不在线");
}
//返回转码后的webrtc流地址,前端可用该地址 + webRTC播放器直接播放了
return "webrtc://" + srsAddress + "/live/" + id;
}
private synchronized void addProcessOutput(ProcessOutputModel processOutputModel) {
processOutputList.add(processOutputModel);
//当有一个以上的推流进程的时候,启动一个线程,来轮询观察所有推流进程的输出日志,并对无效的流进行关闭
if(processOutputList.size() == 1) {
watchProcessOutput();
}
}
private void watchProcessOutput() {
threadPoolTaskExecutor.execute(() -> {
Map<String, ProcessLastOutputModel> outputDetailMap = new HashMap<>();
while(!processOutputList.isEmpty()) {
for (ProcessOutputModel processOutput : processOutputList) {
BufferedReader output = processOutput.getOutput();
try {
ProcessLastOutputModel lastReportDetail = outputDetailMap.get(processOutput.getId());
if (lastReportDetail != null) {
long lastReadTime = lastReportDetail.getLastReadTime();
//超过30s未读到推流进程的输出日志或frame不再变化,则认为该流已无效,需关闭该推流进程
if(System.currentTimeMillis() / 1000 - lastReadTime > 30) {
log.error("推流进程 {} 超过30s未接收到输出日志", processOutput.getId());
closeTranscode(processOutput.getId());
processOutputList.remove(processOutput);
}
} else {
lastReportDetail = new ProcessLastOutputModel(System.currentTimeMillis() / 1000, null);
outputDetailMap.put(processOutput.getId(), lastReportDetail);
}
if(output != null && output.ready()) {
String msg = output.readLine();
log.info("读取到推流进程 {} 的输出日志为: {}", processOutput.getId(), msg);
//如何判断推流是否正常: 通过frame,如果正常推流,frame肯定是递增的,如果前后的frame相等,则说明当前流已无效,即可关闭推流
if (msg.startsWith("frame=")) {
String frame = msg.substring("frame=".length(), msg.indexOf("fps=")).trim();
Integer nowFrame = Integer.parseInt(frame);
Integer lastFrame = lastReportDetail.getLastFrame();
//一旦nowFrame和lastFrame相等,则lastReadTime不再更新,这样过30s后结束对该进程的推流
if(lastFrame == null || nowFrame > lastFrame) {
lastReportDetail.setLastFrame(nowFrame);
lastReportDetail.setLastReadTime(System.currentTimeMillis() / 1000);
}
}
}
} catch (IOException e) {
log.error(e.getMessage());
closeTranscode(processOutput.getId());
processOutputList.remove(processOutput);
}
}
if (processOutputList.size() != outputDetailMap.size()) {
Iterator<String> iterator = outputDetailMap.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
boolean found = false;
for (ProcessOutputModel processOutput : processOutputList) {
if (key.equals(processOutput.getId())) {
found = true;
break;
}
}
if (!found) {
iterator.remove();
}
}
}
try {
Thread.sleep(400);
} catch (Exception e) {
log.error("监听推流线程 {} 停止执行", Thread.currentThread().getName());
for (ProcessOutputModel processOutput : processOutputList) {
closeTranscode(processOutput.getId());
processOutputList.remove(processOutput);
}
break;
}
}
});
}
private void removeProcessOutput(String id) {
processOutputList.forEach(outputModel -> {
if (outputModel.getId().equals(id)) {
processOutputList.remove(outputModel);
}
});
}
/**
* 关闭推流进程
*/
private void closeTranscode(String id) {
TranscodeModel transcodeModel = null;
if((transcodeModel = transcodeMap.get(id)) != null) {
//停止推流转码进程
if(transcodeModel.getProcess() != null) {
transcodeModel.getProcess().destroy();
}
transcodeMap.remove(id);
log.info("关闭对ipc {} 的推流转码", id);
}
}
/**
* 补充功能:客户端结束播放流后,srs可配置触发一个on_stop回调,通过该回调,我们就可以知道哪些流可能没人看了,然后结束对该流进行的推流转码工作
*/
public void stopPlay(CallbackOnStopPlay data) {
int currentPage = 0,pageSize = 100;
try {
while (true) {
JSONArray streams = this.requestSrsStream(currentPage, pageSize);
boolean found = false;
for (int i = 0;i < streams.size();i++) {
JSONObject stream = streams.getJSONObject(i);
//哪个ipc所推的流
String streamName = stream.getString("name");
if(data.getStream().equals(streamName)) {
//clients = 推流的人数 + 拉流的人数, 当clients人数等于1时,说明没有客户端观看该流,停止对该流的推流转码
Integer clients = stream.getInteger("clients");
if(clients <= 1) {
this.closeTranscode(streamName);
this.removeProcessOutput(streamName);
}
found = true;
}
}
if (found || streams.size() < pageSize) {
break;
}
currentPage += pageSize;
}
} catch (Exception e) {
log.error("关闭视频流 {} 失败, 原因: {}", data.getStream(), e.getMessage());
}
}
//srs的stream接口需要传递分页参数,不能直接请求到它下面的所有流,渐进式遍历srs当前所有的流
private JSONArray requestSrsStream(int currentPage, int pageSize) {
String url = "http://" + srsAddress + ":" + this.srsHttpApiPort + "/api/v1/streams?" + "start=" + currentPage + "count=" + pageSize;
ResponseEntity<JSONObject> exchange = null;
try {
exchange = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, new HttpHeaders()), JSONObject.class);
} catch (Exception e) {
log.error("请求srs下所有的流失败,原因: {}", e.getMessage());
return new JSONArray();
}
if (exchange.getBody() == null || exchange.getBody().getInteger("code") != 0) {
log.error("请求srs下所有的流失败");
return new JSONArray();
}
log.info("请求到此时srs的视频流为: {}, 起始值为: {}, 偏移量为: {}", exchange.getBody(), currentPage, pageSize);
return exchange.getBody().getJSONArray("streams");
}
//转码推流类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TranscodeModel {
private String id;
private Process process;
private String command;
}
//推流进程日志输出类
@Data
@AllArgsConstructor
public class ProcessOutputModel {
private String id;
private Process process;
private BufferedReader output;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProcessOutputModel that = (ProcessOutputModel) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProcessLastOutputModel {
private Long lastReadTime;
private Integer lastFrame;
}
//客户端关闭流时触发的回调所传递的参数
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CallbackOnStopPlay {
private String server_id;
private String action;
private String client_id;
private String ip;
private String vhost;
private String app;
private String stream;
private String param;
}
//ipc类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Ipc {
//ipc的主键id
private String id;
//ipc的rtsp流地址
private String rtspUrls;
//ipc的ip地址
private String ip;
//ipc的帐号
private String account;
//ipc的密码
private String password;
//ipc的端口号
private Integer port;
}
}
对推流进程的关闭,可以选择定时任务轮询srs中流的信息,然后对那些没人看的流进行关闭,也可以选择配置srs客户端关闭流时的回调,来进行关闭,至于回调如何配置使用,可以详看官方文档中开放接口相关内容和这篇文章:https://blog.csdn.net/weixin_44341110/article/details/120829847
3.获取大华NVR通道下的IPC并下载指定时间段内的回放录像
(1) 到大华官网https://support.dahuatech.com/tools/sdkExploit,下载Linux64 + Java版本的SDK,使用IDEA打开后将其打成jar包,引入项目,打包的过程略,需要的话可直接留言或私信,我会发给你
(2) 在文档中找到网络SDK开发手册,如下图,它提供了大华sdk中所支持的所有方法的详细说明,使用sdk前必看

(3) 通过sdk,我们就可以获取大华NVR下的IPC信息或下载指定时间段内的回放录像,具体实现代码如下,其实就是调用sdk中所提供的方法,并对方法返回的结果进行解析处理而已,如果有看到不懂的方法,可前往网络SDK开发手册中进行搜索
@Service
@Slf4j
public class DaHuaSdk {
//ffmpeg安装路径
@Value("${config.positioning.ffmpeg.path}")
private String ffmpegPathPrefix;
//项目端口,用于访问下载后的回放录像
@Value("${server.port:8130}")
private Integer port;
//本项目部署的服务器ip
private final String serverIp = "192.168.4.23";
//回放录像的下载路径
private final String recordFilePath = "/usr/local/recordFile/";
@Resource
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
private static final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss");
//获取nvr下的通道的ipc信息
public List<IpcModel> getNvrIpc(NVR nvr) {
//使用大华提供的netsdk所提供的方法
//1.初始化方法CLIENT_Init 参数为:
// 设备断线回调: 通过 CLIENT_Init 设置该回调函数,当设备出现断线时,SDK会调用该函数
// 设备重连成功回调: 通过 CLIENT_SetAutoReconnect 设置该回调函数,当已断线的设备重连成功时,SDK会调用该函数
if(!netsdk.CLIENT_Init(null, null)) {
throw new RuntimeException("加载大华SDK失败,原因: (0x80000000|" + (netsdk.CLIENT_GetLastError() & 0x7fffffff) + ")");
}
//2.若未登录,则先登录
NetSDKLib.NET_DEVICEINFO_Ex deviceInfo = new NetSDKLib.NET_DEVICEINFO_Ex();
NetSDKLib.LLong dhLoginHandle = loginDaHua(nvr.getIp(), nvr.getPort(), nvr.getAccount(), nvr.getPassword(), deviceInfo);
if (dhLoginHandle.longValue() == 0) {
throw new RuntimeException("登录大华nvr " + nvr.getIp() + " 失败,原因: (0x80000000|" + (netsdk.CLIENT_GetLastError() & 0x7fffffff) + ")");
}
int maxChannelNumber = deviceInfo.byChanNum;
log.info("大华NVR {} 的最大通道数量为 {}", nvr.getIp(), maxChannelNumber);
if (maxChannelNumber <= 0) {
throw new RuntimeException("大华NVR " + nvr.getIp() + " 的最大通道数量为0");
}
NetSDKLib.AV_CFG_RemoteDevice[] remoteDevices = new NetSDKLib.AV_CFG_RemoteDevice[maxChannelNumber];
for (int i = 0; i < remoteDevices.length; ++i) {
remoteDevices[i] = new NetSDKLib.AV_CFG_RemoteDevice();
}
IntByReference result = new IntByReference(0);
int memorySize = remoteDevices[0].size() * remoteDevices.length;
byte[] szBuffer =new byte[memorySize];
Pointer pointer = new Memory(memorySize);
pointer.clear(memorySize);
ToolKits.SetStructArrToPointerData(remoteDevices, pointer);
//3.获取ipc配置信息
if(!netsdk.CLIENT_GetNewDevConfig(dhLoginHandle, NetSDKLib.CFG_CMD_REMOTEDEVICE, -1, szBuffer, memorySize, result,5000)) {
throw new RuntimeException("获取大华NVR下通道信息失败,原因: (0x80000000|" + (netsdk.CLIENT_GetLastError() & 0x7fffffff) + ")");
}
//4.解析获取到的数据
if(!configsdk.CLIENT_ParseData(NetSDKLib.CFG_CMD_REMOTEDEVICE, szBuffer, pointer, memorySize, null)) {
throw new RuntimeException("解析大华NVR下通道信息失败,原因: (0x80000000|" + (netsdk.CLIENT_GetLastError() & 0x7fffffff) + ")");
}
//使用大华jar包中的方法
ToolKits.GetPointerDataToStructArr(pointer, remoteDevices);
//ipc的id前缀,去掉前缀后,就是通道号(从0开始)
String idPrefix = "uuid:System_CONFIG_NETCAMERA_INFO_";
List<IpcModel> ipcList = new ArrayList<>();
Arrays.stream(remoteDevices).forEach(ipc -> {
//如果该通道启用
if(ipc.bEnable == 1) {
IpcModel ipcModel = new IpcModel();
String ip = new String(ipc.szIP).trim();
String account = new String(ipc.szUser).trim();
String password = new String(ipc.szPassword).trim();
ipcModel.setIp(ip);
ipcModel.setVendor((short) 2);
ipcModel.setAccount(account);
ipcModel.setPassword(password);
int channel = Integer.parseInt(new String(ipc.szID).trim().substring(idPrefix.length()));
ipcModel.setChannel(channel + 1);
ipcModel.setPort(ipc.nPort);
NetSDKLib.CFG_VIDEO_IN_INFO videoInfo = new NetSDKLib.CFG_VIDEO_IN_INFO();
//使用大华jar包中的方法获取通道描述信息
ToolKits.GetDevConfig(dhLoginHandle, channel, NetSDKLib.CFG_CMD_VIDEOIN, videoInfo);
String channelName = new String(videoInfo.szChnName).trim();
ipcModel.setChannelName(channelName);
log.info("通道 {} 启用, 获得到的摄像机 ip 为 {},帐号为 {},密码为 {},端口为 {},通道描述为 {}", channel + 1, ip, account, password, ipc.nPort, channelName);
ipcList.add(ipcModel);
}
});
// 5.退出登录
logout();
// 6.释放资源
netsdk.CLIENT_Cleanup();
return ipcList;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class IpcModel {
private Long id;
/**
* ip地址
*/
private String ip;
/**
* 设备厂商(1 海康,2 大华)
*/
private Short vendor;
/**
* 帐号
*/
private String account;
/**
* 密码
*/
private String password;
/**
* ONVIF端口
*/
private Integer port;
/**
* 该ipc的通道号
*/
private Integer channel;
/**
* 通道描述信息
*/
private String channelName;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class NVR {
private Long id;
/**
* 名称
*/
private String nvrName;
/**
* 厂商
*/
private String vendor;
/**
* ip地址
*/
private String ip;
/**
* 帐号
*/
private String account;
/**
* 密码
*/
private String password;
/**
* 端口
*/
private Integer port;
}
//登录大华设备
private NetSDKLib.LLong loginDaHua(String ip, int port, String user, String password, NetSDKLib.NET_DEVICEINFO_Ex deviceInfo) {
//入参
NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY pstInParam = new NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY();
pstInParam.nPort = port;
pstInParam.szIP = ip.getBytes();
pstInParam.szPassword = password.getBytes();
pstInParam.szUserName = user.getBytes();
//出参
NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY pstOutParam = new NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY();
pstOutParam.stuDeviceInfo = deviceInfo;
//使用CLIENT_LoginWithHighLevelSecurity进行登录
NetSDKLib.LLong dhLoginHandle = netsdk.CLIENT_LoginWithHighLevelSecurity(pstInParam, pstOutParam);
if(dhLoginHandle.longValue() == 0) {
log.error("Login Device {} Port {} Failed. {}", ip, port, ToolKits.getErrorCodePrint());
} else {
log.info("Login Success [ {} ]", ip);
}
return dhLoginHandle;
}
/**
* 下载某个NVR下某个通道的ipc的回放录像
* @param ipcList nvr下的ipc信息,是通过上面的getNvrIpc方法获取到的
* @param nvr
* @param startTime 指定需要下载的回放录像的开始时间
* @param endTime 指定需要下载的回放录像的结束时间
* @return
*/
public List<RecordFileModel> downloadRecordFileByTime(List<IpcModel> ipcList, NVR nvr, LocalDateTime startTime, LocalDateTime endTime) {
if(!netsdk.CLIENT_Init(null, null)) {
throw new RuntimeException("加载大华SDK失败,原因: (0x80000000|" + (netsdk.CLIENT_GetLastError() & 0x7fffffff) + ")");
}
//2.若未登录,则先登录
NetSDKLib.NET_DEVICEINFO_Ex deviceInfo = new NetSDKLib.NET_DEVICEINFO_Ex();
NetSDKLib.LLong dhLoginHandle = loginDaHua(nvr.getIp(), nvr.getPort(), nvr.getAccount(), nvr.getPassword(), deviceInfo);
if (dhLoginHandle.longValue() == 0) {
throw new RuntimeException("登录大华nvr " + nvr.getIp() + " 失败,原因: (0x80000000|" + (netsdk.CLIENT_GetLastError() & 0x7fffffff) + ")");
}
//3.设置码流类型 0-主辅码流,1-主码流,2-辅码流
IntByReference steamType = new IntByReference(0);
boolean bret = netsdk.CLIENT_SetDeviceMode(dhLoginHandle, NetSDKLib.EM_USEDEV_MODE.NET_RECORD_STREAM_TYPE, steamType.getPointer());
if (!bret) {
throw new RuntimeException("设置大华nvr " + nvr.getIp() + " 码流类型失败,原因: (0x80000000|" + (netsdk.CLIENT_GetLastError() & 0x7fffffff) + ")");
}
//4.查找指定时间范围内的回放录像是否存在
List<Future<String>> downloadRecordFileWorks = new ArrayList<>();
for (int i = 0; i < ipcList.size(); i++) {
IpcModel ipc = ipcList.get(i);
Integer channel = ipc.getChannel();
Future<String> downloadDaHuaRecordFileResult = threadPoolTaskExecutor.submit(() -> {
String fileNamePrefix = "nvr_" + nvr.getIp().replace('.', '-') + "_" + channel + "_" + startTime.format(fmt);
File file = new File(recordFilePath);
long totalSpace = file.getTotalSpace();
long freeSpace = file.getFreeSpace();
long usedSpace = totalSpace - freeSpace;
if(usedSpace > (long) (totalSpace * 0.95)) {
throw new RuntimeException("下载录像 " + fileNamePrefix + " 失败,原因: 磁盘使用量已超过95%,请先清理磁盘");
}
//指定录像的开始时间
NetSDKLib.NET_TIME stTimeStart = new NetSDKLib.NET_TIME();
stTimeStart.setTime(startTime.getYear(), startTime.getMonthValue(), startTime.getDayOfMonth(), startTime.getHour(), startTime.getMinute(), startTime.getSecond());
//指定录像的结束时间
NetSDKLib.NET_TIME stTimeEnd = new NetSDKLib.NET_TIME();
stTimeEnd.setTime(endTime.getYear(), endTime.getMonthValue(), endTime.getDayOfMonth(), endTime.getHour(), endTime.getMinute(), endTime.getSecond());
IntByReference recordFileCount = new IntByReference();
//通道号从0开始
if (!netsdk.CLIENT_QueryRecordTime(dhLoginHandle, channel - 1, 0, stTimeStart, stTimeEnd, null, recordFileCount, 5000)) {
throw new RuntimeException("查找录像 " + fileNamePrefix + " 失败,原因: (0x80000000|" + (netsdk.CLIENT_GetLastError() & 0x7fffffff) + ")");
}
if (recordFileCount.getValue() <= 0) {
throw new RuntimeException("录像 " + fileNamePrefix + " 不存在,请检查nvr该通道下的录像配置是否开启");
}
//5.下载指定时间范围内的录像
CountDownLatch countDownLatch = new CountDownLatch(1);
//下载进度回调,用于确定录像文件下载是否完成
DaHuaTimeDownloadPosCallback daHuaTimeDownloadPosCallback = new DaHuaTimeDownloadPosCallback(fileNamePrefix, countDownLatch);
NetSDKLib.LLong dhDownLoadHandle = netsdk.CLIENT_DownloadByTimeEx(dhLoginHandle, channel - 1, 0, stTimeStart, stTimeEnd, recordFilePath + fileNamePrefix + ".dav",
daHuaTimeDownloadPosCallback, null, null, null, null);
if (dhDownLoadHandle.longValue() == 0) {
throw new RuntimeException("下载录像 " + fileNamePrefix + " 失败,原因: (0x80000000|" + (netsdk.CLIENT_GetLastError() & 0x7fffffff) + ")");
}
//等待回放录像下载完毕
try {
countDownLatch.await();
} catch (Exception e) {
log.error(e.getMessage());
}
if (!daHuaTimeDownloadPosCallback.isSuccess()) {
throw new RuntimeException("录像 " + fileNamePrefix + " 写入失败");
}
//回放录像下载完毕后,得到的是.dav视频文件,需使用ffmpeg将其转码为.mp4视频文件,便于观看
String command = String.format("%sffmpeg -i %s.dav -c:v copy -c:a copy %s.mp4", ffmpegPathPrefix, recordFilePath + "/" + fileNamePrefix, recordFilePath + "/" + fileNamePrefix);
log.info("对录像{}.dav进行推流转码,其命令为 {}", fileNamePrefix, command);
//通过命令行执行推流转码
try {
Process process = Runtime.getRuntime().exec(command);
while (process.isAlive()) {
Thread.sleep(200);
}
log.info("完成对录像{}.dav的推流转码工作", fileNamePrefix);
} catch (Exception e) {
log.error(e.getMessage());
}
return fileNamePrefix;
});
downloadRecordFileWorks.add(downloadDaHuaRecordFileResult);
}
List<String> deleteDavFileList = new ArrayList<>();
List<RecordFileModel> downloads = new ArrayList<>();
for (int i = 0; i < ipcList.size(); i++) {
IpcModel ipc = ipcList.get(i);
Future<String> downloadResult = downloadRecordFileWorks.get(i);
try {
String fileNamePrefix = downloadResult.get(30, TimeUnit.SECONDS);
deleteDavFileList.add(fileNamePrefix);
//需配置springboot静态资源读取
downloads.add(new RecordFileModel(ipc.getId(), ipc.getIp(), nvr.getIp(), ipc.getChannel(), "http://" + serverIp + ":" + port + "/vidicon/playback/" + fileNamePrefix + ".mp4", true, ""));
}catch (TimeoutException e) {
downloads.add(new RecordFileModel(ipc.getId(), ipc.getIp(), nvr.getIp(), ipc.getChannel(),"", false, "下载回放录像文件超时, 请稍后再试"));
} catch (Exception e) {
log.error(e.getMessage());
downloads.add(new RecordFileModel(ipc.getId(), ipc.getIp(), nvr.getIp(), ipc.getChannel(),"", false, e.getMessage()));
}
}
//最后,再异步删除掉.dav视频文件,只保留.mp4视频文件
threadPoolTaskExecutor.execute(() -> {
for (String fileNamePrefix : deleteDavFileList) {
String davFile = recordFilePath + "/" + fileNamePrefix + ".dav";
File file = new File(davFile);
if (file.exists() && file.delete()) {
log.info("{} 删除成功", davFile);
} else {
log.info("{} 删除失败", davFile);
}
}
});
//退出登录
logout();
//释放资源
netsdk.CLIENT_Cleanup();
return downloads;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RecordFileModel {
//ipc的id
private Long ipcId;
//摄像机的ip
private String ipcIp;
//nvr的ip
private String nvrIp;
//ipc的通道号
private Integer channel;
//成功下载后得到的回放录像文件
private String url;
//是否下载成功,为true,则errorMessage为空; 为false,则url为空
private boolean success;
//下载失败得到的异常信息
private String errorMessage;
}
@Slf4j
public class DaHuaTimeDownloadPosCallback implements NetSDKLib.fTimeDownLoadPosCallBack {
private CountDownLatch countDownLatch;
private volatile boolean success;
private String fileNamePrefix;
public DaHuaTimeDownloadPosCallback(String fileNamePrefix, CountDownLatch countDownLatch){
this.fileNamePrefix = fileNamePrefix;
this.countDownLatch = countDownLatch;
this.success = false;
}
@Override
public void invoke(NetSDKLib.LLong lPlayHandle, int dwTotalSize, int dwDownLoadSize, int index, NetSDKLib.NET_RECORDFILE_INFO.ByValue recordfileinfo, Pointer dwUser) {
log.info("回放录像 {}.dav 已经下载: {}",fileNamePrefix, dwDownLoadSize);
if (dwDownLoadSize == -1) {
log.info("录像{}.dav下载完毕", fileNamePrefix);
success = true;
countDownLatch.countDown();
} else if (dwDownLoadSize == -2) {
log.info("录像{}.dav写入文件失败", fileNamePrefix);
countDownLatch.countDown();
}
}
public boolean isSuccess() {
return success;
}
}
}
4.获取海康NVR通道下的IPC并下载指定时间段内的回放录像
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)