1. 下载大华sdk
- 链接: https://pan.baidu.com/s/1Nqc5wRACHVcJOhBmroKnwQ 提取码: ucdf
1.1 下载流媒体服务器
- 链接: https://pan.baidu.com/s/1dkLE3vAGr1hVAWDpVY2WkQ 提取码: 2fnw
- 使用NODE-MEDIA-SERVER搭建一个简易的流媒体服务器(WINDOWS环境) https://www.freesion.com/article/75861003790/
1.2 引用
- maven引入javacv
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.2.2-1.5.3</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.2.2-1.5.3</version>
<classifier>windows-x86_64</classifier>
//linux环境下使用
<!--<classifier>linux-x86_64</classifier>-->
</dependency>
1.3 再windows下引用大华sdk的dll文件(解决java打成jar包无法读取dll文件)
- 把大华sdk中的dll文件复制到 C:\Windows\System32中
- java引用dll文件会从(Windows\System32)读取
- 只适合windows系统,linux系统为.so文件
- C:\Windows\System32 大都存储dll文件
1.4 配置大华平台的设备
- 把中心服务器和监控设备都插在交换机网口,存储服务器插再中心服务器网口
- 192.168.1.108一般为中心服务器端口 192.168.1.109一般为存储服务器端口
- 监控设备默认为和中心服务器端口一样需要登大华平台进行修改ip地址(登录地址为当前设备ip,不能重复)
- 可以登上中心服务器下载 Dss客户端查看实时视频.
- 下载ConfigTool软件搜索设备信息(方便调试设备)
1.5 在大华平台配置设备
- 登录智慧园区综合管理平台在设备管理中的基础应用----设备管理-----编码器中新增存储服务器设备然后把摄像头都注册到存储设备中.
- 注册到存储设备中的摄像头通道为存储设备编码+010,100001000代表通道号,依次递增
2. 开始根据sdk开发
2.1 在大华平台配置设备
- 所有大华设备和调试都在线了大华平台可以播放实时视频,开启调试sdk
- idea 在libraries设置native的访问目录
- 大华给的测试类TestDPSDKMain其中核心类为IDpsdkCore,这个类中都是大华Native的方法
- OnCreate() 初始化sdk方法;
- OnLogin() 登录中心服务器;
- LoadGroup() 加载组织结构;
- GetGroupStr() 获取组织结构串(如果需要则用解析xml);
- GetReal () 请求开启实时视频;
- GetExternUrl() 获取实时视频 rtsp地址
- CloseReal() 关闭流
- startDownLoadRecordByTime() 开启录像下载请求Node-Media-Server.zip
- app.OnLogout() 登出;
- app.OnDestroy() 释放内存 (释放内存很重要,服务关后需要调用)
2.2 进行拉流,推流,转封装成rtmp中flv格式
参考地址 -https://blog.csdn.net/qq_40741855/article/details/103878683;
建议自己测试的时候可以下载 ffmpeg 通过cmd命令进行推流测试,javacv底层也是用
ffmpeg进行操作,进行编解码
命令:
ffmpeg -re -i ./video.mp4 -c copy -f flv rtmp://localhost:1935/live/STREAM_NAME #把MP4转换成rtmp流
ffmpeg -re -rtsp_transport tcp -i "rtsp://192.168.1.108:9090/dss/monitor/param?cameraid=1000003%240&substream=1" -c copy -f flv "rtmp://localhost:1935/live/STREAM_NAME" #rtsp转换成rtmp流
public class CameraPush {
FFmpegFrameGrabber grabber = null;
FFmpegFrameRecorder record = null;
int width = -1, height = -1;
// 视频参数
protected int audiocodecid;
protected int codecid;
// 帧率
protected double framerate;
// 比特率
protected int bitrate;
// 退出状态码:0-正常退出;1-手动中断
protected int exitcode;
public void setExitcode(int exitcode) {
this.exitcode = exitcode;
}
public int getExitcode() {
return exitcode;
}
/**
* 选择视频源.
*/
public CameraPush from(String src) throws Exception {
// 采集器
try {
grabber = new FFmpegFrameGrabber(src);
if(src.indexOf("rtsp")>=0) {
//采用tcp协议 udp会丢包
grabber.setOption("rtsp_transport","tcp");
// 设置采集器构造超时时间(单位微秒,1秒=1000000微秒)
grabber.setOption("stimeout", "2000000");
}
// 开始之后ffmpeg会采集视频信息,之后就可以获取音视频信息
grabber.start();
width = grabber.getImageWidth();
height = grabber.getImageHeight();
if(width == 0 && height == 0){
System.err.println("拉流超时");
return null;
}
// 视频参数
audiocodecid = grabber.getAudioCodec();
codecid = grabber.getVideoCodec();
framerate = grabber.getVideoFrameRate();
bitrate = grabber.getVideoBitrate();
return this;
} catch (FrameGrabber.Exception e) {
e.printStackTrace();
log.error("拉流失败");
}
return this;
}
/**
* 选择输出.
*/
public CameraPush to(String out) throws IOException {
try {
// 推流器
record = new FFmpegFrameRecorder(out, width, height);
// 画面质量参数,0~51;18~28是一个合理范围
record.setVideoOption("crf", "28");
record.setGopSize(2);
record.setFrameRate(framerate);
record.setVideoBitrate(bitrate);
AVFormatContext fc = null;
if (out.indexOf("rtmp") >= 0 || out.indexOf("flv") > 0) {
// 封装格式flv
record.setFormat("flv");
record.setAudioCodecName("aac");
record.setVideoCodec(codecid);
fc = grabber.getFormatContext();
}
record.start(fc);
} catch (FrameRecorder.Exception e) {
e.printStackTrace();
log.error("取流转封装失败");
}
return this;
}
/**
* 转封装.
*/
public CameraPush go() throws IOException {
//采集或推流导致的错误次数
long err_index = 0;
//将探测时留下的数据帧释放掉,以免因为dts,pts的问题对推流造成影响
grabber.flush();
//连续五次没有采集到帧则认为视频采集结束,程序错误次数超过5次即中断程序
for(int no_frame_index=0; no_frame_index<5 || err_index < 5;) {
AVPacket pkt=null;
if (exitcode == 1) {
break;
}
try {
//没有解码的音视频帧
pkt=grabber.grabPacket();
if(pkt==null||pkt.size()<=0||pkt.data()==null) {
//空包记录次数跳过
no_frame_index++;
err_index++;
continue;
}
//不需要编码直接把音视频帧推出去, 如果失败err_index自增1
err_index += (record.recordPacket(pkt) ? 0 : 1);
av_packet_unref(pkt);
//System.out.println("推流成功");
}catch (Exception e) {
err_index++;
System.out.println("推流失败");
}
}
grabber.close();
record.close();
System.out.println("推流完毕");
return this;
}
}
- 这边需要启动之前下载好的流媒体服务器进行推流.
- 把sdk获取到的rtsp路径传进去,再把推送地址写 rtmp://localhost:1935/live/10000100$0 端口就是流媒体服务器中app.js的配置 .
mp4test可以自行定义(建议为设备通道号); - 推流成功后流媒体服务器会生成一个rtmp流和http访问路径列:
2.4 前端使用flv.js播放
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script src="https://cdn.bootcss.com/flv.js/1.4.0/flv.min.js"></script>
<div>
<video id="videoElement" style="width: 45%;height: 600px;" controls="controls"></video>
</div>
<script>
if (flvjs.isSupported()) {
var videoElement = document.getElementById('videoElement');
var flvPlayer = flvjs.createPlayer({
type: 'flv',
url:'http://127.0.0.1:8000/live/1000010$1$0$0.flv'
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
}
</script>
</body>
</html>
3. 实时视频保活机制和关流机制
每个监控设备按照通道号开启流的时候都会记录开启的时间和唯一标识存放到缓存当中,当用户关闭播放的时候对此通道当前使用人数减1,当定时任务检测到此通道已经没有人观看的时候自动关闭流(调用sdk的CloseReal()方法)
设置保活时间,当定时任务检测到当前时间-开始播放时间大于保活时间的活会自动关闭实时视频流
前端每次调用保活接口修改播放时间为当前时间.
定时任务示例:
public ReturnT<String> monitorChannelJob(String param) {
log.info("-----------------------------XXL-JOB, MonitorChannelJob开始执行-------------------------------");
log.info("定时任务 当前有" + MonitorSdkFacadeImpl.JOBMAP.size() + "个推流任务正在进行推流");
// 管理缓存
//如果已经有开始推的流
if (null != CacheUtil.STREATMAP && 0 != CacheUtil.STREATMAP.size()) {
Set<String> keys = CacheUtil.STREATMAP.keySet();
for (String key : keys) {
try {
// 最后打开时间
long openTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
.parse(CacheUtil.STREATMAP.get(key).getOpentime()).getTime();
// 当前系统时间
long newTime = new Date().getTime();
CameraDTO cameraDTO =CacheUtil.STREATMAP.get(key);
// 如果通道使用人数为0,则关闭推流
if (CacheUtil.STREATMAP.get(key).getCount() == 0) {
//使用sdk关闭流
dpsdkUtil.CloseReal(cameraDTO.getNRealSeq());
// 结束线程
MonitorSdkFacadeImpl.JOBMAP.get(key).setInterrupted(key);
log.info("定时任务 当前设备使用人数为0结束推流 设备信息:"+cameraDTO.toString());
}
else if (null == CacheUtil.STREATMAP.get(key).getStarttime()
&& (newTime - openTime) / 1000 / 60 >= Integer.valueOf(dejiCommonConfig.getKeepalive())) {
dpsdkUtil.CloseReal(cameraDTO.getNRealSeq());
MonitorSdkFacadeImpl.JOBMAP.get(key).setInterrupted(key);
log.info("定时任务 当前设备使用时间超时结束推流 设备信息:"+cameraDTO.toString());
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
-参考地址:https://blog.csdn.net/weixin_40777510/article/details/103764198
4.录像回放功能
4.1 按照设备通道号和开始时间和结束时间调用sdk获取录像文件(dav类型)
4.2 把dav文件转换成MP4文件上传到fast客户端并且记录路径做后期清除功能
4.3 如果按时间段查询到有多个录像文件,会拼接成1个录像文件 (文件大小存储服务器管理平台上可以进行设置,也可以设置摄像头录制视频的时间)
/**
* 下载录像回放文件
*/
public int startDownLoadRecordByTime(RecordDTO recordDTO) {
Long beginTime = DateUtil.timeToLong(recordDTO.getBeginTime()) / 1000;
log.info("录像开始时间转为秒:{}", beginTime);
Long endTime = DateUtil.timeToLong(recordDTO.getEndTime()) / 1000;
log.info("录像结束时间转为秒:{}", endTime);
Query_Record_Info_t queryInfo = new Query_Record_Info_t();
Return_Value_Info_t nRecordCount = new Return_Value_Info_t();
queryInfo.szCameraId = recordDTO.getChannelId().getBytes();
//下载模式
queryInfo.nRecordType = dpsdk_record_type_e.DPSDK_CORE_PB_RECORD_UNKONWN;
//不检查权限,请求视频流,无需加载组织结构
queryInfo.nRight = dpsdk_check_right_e.DPSDK_CORE_NOT_CHECK_RIGHT;
//设备录像
queryInfo.nSource = 2;
//转换成秒;
queryInfo.uBeginTime = beginTime;
queryInfo.uEndTime = endTime;
int nRet = IDpsdkCore.DPSDK_QueryRecord(m_nDLLHandle, queryInfo, nRecordCount, 60 * 1000);
if (nRet != 0) {
log.error("录像查询失败,nRet={}", nRet);
return -1;
}
if (nRecordCount.nReturnValue == 0) {
log.error("没有录像!!!!!");
return -1;
}
Return_Value_Info_t nDownLoadSeq = new Return_Value_Info_t();
Get_RecordStream_Time_Info_t getInfo = new Get_RecordStream_Time_Info_t();
getInfo.szCameraId = recordDTO.getChannelId().getBytes();
//下载模式
getInfo.nMode = 2;
//不检查权限,请求视频流,无需加载组织结构
getInfo.nRight = dpsdk_check_right_e.DPSDK_CORE_NOT_CHECK_RIGHT;
//设备录像
getInfo.nSource = 2;
log.info("开始录像下载 begintime = {}, endtime = {}", beginTime, endTime);
//转换成秒
getInfo.uBeginTime = beginTime;
getInfo.uEndTime = endTime;
nRet = IDpsdkCore.DPSDK_GetRecordStreamByTime(
m_nDLLHandle,
nDownLoadSeq,
getInfo,
new FMediaDataCallback() {
FileOutputStream writer = null;
String davPath = dejiCommonConfig.getStorageFileName() + "\\" + IdUtil.nextStringId() + ".dav";
File file = new File(davPath);
@Override
public void invoke(int nPDLLHandle, int nSeq, int nMediaType, byte[] szNodeId, int nParamVal, byte[] szData, int nDataLen) {
log.info("录像流回调");
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
//nMediaType==2 音频
//录像下载结束,开线程调用停止录像,否则接口会超时
if (nMediaType == 2 && nDataLen == 0) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
if (writer != null) {
writer.flush();
writer.close();
writer = null;
}
int nRet = IDpsdkCore.DPSDK_CloseRecordStreamBySeq(m_nDLLHandle, nSeq, 10000);
String mp4Path = davPath;
mp4Path = mp4Path.replace(".dav", ".mp4");
log.info("dav路径{}", davPath);
log.info("mp4路径{}", mp4Path);
log.info("dav视频转成mp4");
Boolean flag = VideoUtil.convertToMp4(davPath, mp4Path);
if (flag) {
log.info("dav视频转为MP4成功,开始上传到fastDFS");
String data = fastDfsClient.uploadFileWithFilepath(mp4Path);
//如果录像文件上传到了服务器,删除本地文件
File davFile = new File(davPath);
File mp4File = new File(mp4Path);
if (davFile.exists()) {
log.info("删除本地dav文件");
davFile.delete();
}
if (mp4File.exists()) {
log.info("删除本地mp4文件");
mp4File.delete();
}
String videoUrl = dejiCommonConfig.getFastdfsurl() + data;
RBucket<String> bucket = redissonClient.getBucket(recordDTO.getRecordId());
String json = bucket.get();
RecordDTO recordDTO = JSON.parseObject(json, RecordDTO.class);
recordDTO.setVideoUrl(videoUrl);
bucket.set(JSON.toJSONString(recordDTO));
log.info("录像前端播放地址:{}", videoUrl);
//把远程服务器文件路径记录下来
log.info("把远程服务器文件路径保存到数据库");
monitorVideoRecordService.insert(data,recordDTO.getRecordId());
}
if (nRet == 0) {
System.out.println("nRet=0");
}
log.info("下载结束,停止下载nRet = {}", nRet);
} catch (Exception e) {
e.printStackTrace();
}
}
});
t.start();
}
try {
if (writer == null) {
writer = new FileOutputStream(davPath, true);
}
if (nDataLen > 0) {
writer.write(szData);
}
} catch (Exception e) {
e.printStackTrace();
}
}
},
10000);
if (nRet == dpsdk_retval_e.DPSDK_RET_SUCCESS) {
log.info("开始录像下载成功,nRet = {}, nSeq = {}", nRet, nDownLoadSeq.nReturnValue);
return nDownLoadSeq.nReturnValue;
} else {
log.info("开始录像下载失败,nRet = {}", nRet);
return -1;
}
}
4.4 dav文件转MP4文件
/**
* 转换视频文件为mp4
*
* @param srDavPath dav 文件路径
* @param destMp4Path 转换的MP4 文件路径
* @return
*/
public static boolean convertToMp4(String srDavPath, String destMp4Path) throws Exception {
System.err.println(srDavPath + " " + destMp4Path);
List<String> commend = new java.util.ArrayList<String>();
//ffmpeg -i "30.dav" -vcodec copy -acodec copy "30.mp4"
commend.add("ffmpeg");
commend.add("-i");
commend.add("\"" + srDavPath + "\"");
commend.add("-vcodec");
commend.add("copy");
commend.add("-acodec");
commend.add("copy");
commend.add("\"" + destMp4Path + "\"");
ProcessBuilder builder = new ProcessBuilder();
InputStream in = null;
try {
builder.command(commend);
builder.redirectErrorStream(true);
Process p = builder.start();
byte[] b = new byte[2048];
int readbytes = -1;
StringBuffer output = new StringBuffer();
// 读取进程输出值
in = p.getInputStream();
while ((readbytes = in.read(b)) != -1) {
output.append(new String(b, 0, readbytes));
}
return true;
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
in.close();
}
}