首页 > 基础资料 博客日记
java 对接海康 抓图,报警布防,视频拉流
2024-09-18 09:00:07基础资料围观180次
java 海康抓图,布防,拉流
介绍:
使用java对接海康的一些操作,包括对海康摄像头(以下均表示为设备)进行抓图,报警布防,视频拉流的一些操作,主要列出一些调用时的核心代码,以及遇到的一些问题。
需要用到的:
海康SDK:这个在官网(海康开放平台 (hikvision.com))下载即可,注意区分系统,本次使用到的是windowsSDK
FFMPEG:这个作为视频推流使用(本次直接引入了FFMPEG的maven,不需要再另外安装单独的软件),亦可使用其他推流软件如OBS(需要知道相应的操作命令)
SRS:这个是媒体服务器,将视频流转为其他格式,使用nginx也可以(参考nginx +rtmp+nginx-http-flv-module 环境搭建_phpstudy 安装nginx-http-flv-module-CSDN博客),但实际环境中你可能已经安装了nginx,但安装时并没有加入视频模块,就涉及到下载模块并重新编译nginx了,这里就不赘述了,SRS安装教程可参考(liunx下使用SRS搭建直播流媒体服务器_srs rtc直播间问题-CSDN博客)
VLC:视频播放器,也可以使用EasyPlayer 用来测试视频地址
这里只列出关键的一些依赖
<dependencies>
<!--ffmpeg-->
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.1-1.4.4</version>
</dependency>
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-all-deps</artifactId>
<version>3.3.1</version>
<exclusions>
<!-- 排除windows 32位系统 -->
<exclusion>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-win32</artifactId>
</exclusion>
<!-- 排除linux 32位系统 -->
<exclusion>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux32</artifactId>
</exclusion>
<!-- 排除Mac系统-->
<exclusion>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-osx64</artifactId>
</exclusion>
<!-- 排除osxm-->
<exclusion>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-osxm1</artifactId>
</exclusion>
<!-- 排除arm-->
<exclusion>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux-arm32</artifactId>
</exclusion>
<exclusion>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux-arm64</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- jna 这个使用海康提供的jna最好,这里是自己引入的-->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.13.0</version>
</dependency>
<!-- hc API 这个在海康官网下载的包里会有,必须要引入-->
<dependency>
<groupId>examples</groupId>
<artifactId>hc-examples</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/examples.jar</systemPath>
</dependency>
</dependencies>
这个HCNetSDK.java 和上述example.jar 必须引入,是操作设备的关键(就是SDK), 文件在下载的压缩包里面的开放示例里面会有,直接放到项目下
整个用到的库文件,官网下载的包里会有,这里HCNetSDK.dll 的路径需要拿到,在加载hcSDK时需要用到
抓图:
流程:sdk加载 -> 初始化 -> 登录注册 -> 获取设备工作状态 -> 抓图
其中SDK 加载和初始化只需要 执行一次,所以可以注册到spring中,后续需要使用时直接注入
@Bean
public HCNetSDK hcNetSDK() {
//sdkPath:上述HCNetSDK.dll的路径
String sdkPath = HcProperties.getSdkPath();
HCNetSDK hcNetSDK = Native.load(sdkPath, HCNetSDK.class);
if (hcNetSDK.NET_DVR_Init()) {
log.info("HCNetSDK 初始化成功");
} else {
//hcNetSDK.NET_DVR_GetLastError()获取最后一次异常码,这些异常码在HCNetSDK.java中有相应解释,大概在200行,也可以在网上搜海康异常码
log.error("HcNetSDK 初始化失败,异常码:{},信息:{}",
hcNetSDK.NET_DVR_GetLastError(),
HCErrorEnum.getByCode(hcNetSDK.NET_DVR_GetLastError()).getMessage());
}
//设置连接超时时间,连接尝试次数
hcNetSDK.NET_DVR_SetConnectTime(2000,1);
//设置重连功能,重连间隔ms,是否重连,
hcNetSDK.NET_DVR_SetReconnect(60000,true);
//解释:1
if (HcProperties.isSetCallBack()) {
//这里回调函数必须是全局唯一,否则会导致无法调用
HCNetSDK.FMSGCallBack_V31 fmsgCallBack = FMSGCallBack.CALL_BACK_V_31;
boolean b = hcNetSDK.NET_DVR_SetDVRMessageCallBack_V31(fmsgCallBack, null);
if (b) log.info("设置报警回调函数成功.");
else log.error("设置报警回调函数失败:异常码:{},信息:{}",
hcNetSDK.NET_DVR_GetLastError(), HCErrorEnum.getByCode(hcNetSDK.NET_DVR_GetLastError()).getMessage());
/*
设备上传的报警信息是COMM_VCA_ALARM(0x4993)类型,
在SDK初始化之后增加调用NET_DVR_SetSDKLocalCfg(enumType为NET_DVR_LOCAL_CFG_TYPE_GENERAL)设置通用参数NET_DVR_LOCAL_GENERAL_CFG的byAlarmJsonPictureSeparate为1,
将Json数据和图片数据分离上传,这样设置之后,报警布防回调函数里面接收到的报警信息类型为COMM_ISAPI_ALARM(0x6009),
报警信息结构体为NET_DVR_ALARM_ISAPI_INFO(与设备无关,SDK封装的数据结构),更便于解析。
*/
HCNetSDK.NET_DVR_LOCAL_GENERAL_CFG struNET_DVR_LOCAL_GENERAL_CFG = new HCNetSDK.NET_DVR_LOCAL_GENERAL_CFG();
struNET_DVR_LOCAL_GENERAL_CFG.byAlarmJsonPictureSeparate = 1; //设置JSON透传报警数据和图片分离
struNET_DVR_LOCAL_GENERAL_CFG.write();
Pointer pStrNET_DVR_LOCAL_GENERAL_CFG = struNET_DVR_LOCAL_GENERAL_CFG.getPointer();
hcNetSDK.NET_DVR_SetSDKLocalCfg(17, pStrNET_DVR_LOCAL_GENERAL_CFG);
}
return hcNetSDK;
}
解释1:上述设置回调函数只需设置一次,设置多次会覆盖掉,这个回调函数是在设备报警时的接口,所有设备报警都会进入到此接口,根据形参中第二个参数区分具体设备,这个回调函数必须全局唯一,否则有可能会被回收,收不到报警,实际测试中我直接new一个函数实现类是无法收到报警;
HCNetSDK.FMSGCallBack_V31 fmsgCallBack = FMSGCallBack.CALL_BACK_V_31;
hcNetSDK.NET_DVR_SetDVRMessageCallBack_V31(fmsgCallBack, null);
也可以写成
hcNetSDK.NET_DVR_SetDVRMessageCallBack_V31(this::invoke, null)
public boolean invoke(int lCommand, HCNetSDK.NET_DVR_ALARMER pAlarmer, Pointer pAlarmInfo, int dwBufLen, Pointer pUser) {
//....
return true;
}
回调函数
public class FMSGCallBack implements HCNetSDK.FMSGCallBack_V31 {
public static final HCNetSDK.FMSGCallBack_V31 CALL_BACK_V_31 = new FMSGCallBack();
@Override
public boolean invoke(int lCommand, HCNetSDK.NET_DVR_ALARMER pAlarmer, Pointer pAlarmInfo, int dwBufLen, Pointer pUser) {
//AlarmDataParse 这个可以参考海康提供的开发示例来写,里面详细解释了参数的一些使用方式
//可以根据你需要的报警类型来写相关的业务,下面我只贴部分代码
AlarmDataParse.alarmDataHandler(lCommand, pAlarmer, pAlarmInfo, dwBufLen, pUser);
return true;
}
}
处理相关报警,
lCommand :代表事件类型,可以查看HCNetSDK.java 中的对应事件判断报警类型,大概在886行左右;
pAlarmer: 表示设备信息,可以区分是哪个设备的报警;
pAlarmInfo: 与lCommand 关联,不同的报警会触发不同的行为,比如温度报警,会在pAlarmInfo里面存放热成像图片和可见光图片信息,这时就可以对这个pAlarmInfo进行读取,从而抓取图片,但是有些事件又不会触发抓图行为,所以需要根据事件类型来判断pAlarmInfo 的具体实现;
详见:NET_DVR_SetDVRMessageCallBack_V31 (hikvision.com)
@Slf4j
public class AlarmDataParse {
public static void alarmDataHandler(int lCommand, HCNetSDK.NET_DVR_ALARMER pAlarmer, Pointer pAlarmInfo, int dwBufLen, Pointer pUser) {
log.error("侦测到事件类型: {},事件信息:{},deviceIp:{},serialNumber:{},deviceName:{};",
Integer.toHexString(lCommand),
HCEventEnum.getByCode(lCommand).getMessage(),
new String(pAlarmer.sDeviceIP),
new String(pAlarmer.sSerialNumber),
new String(pAlarmer.sDeviceName, StandardCharsets.US_ASCII));
switch (lCommand) {
case HCNetSDK.COMM_THERMOMETRY_ALARM: //温度报警信息
HCNetSDK.NET_DVR_THERMOMETRY_ALARM struTemInfo = new HCNetSDK.NET_DVR_THERMOMETRY_ALARM();
struTemInfo.write();
Pointer pTemInfo = struTemInfo.getPointer();
pTemInfo.write(0, pAlarmInfo.getByteArray(0, struTemInfo.size()), 0, struTemInfo.size());
struTemInfo.read();
String sThermAlarmInfo = "规则ID:" + struTemInfo.byRuleID + "预置点号:" + struTemInfo.wPresetNo + "报警等级:" + struTemInfo.byAlarmLevel + "报警类型:" +
struTemInfo.byAlarmType + "当前温度:" + struTemInfo.fCurrTemperature;
log.error(sThermAlarmInfo);
break;
case HCNetSDK.COMM_ALARM_RULE: //行为分析信息
break;
case HCNetSDK.COMM_ALARM_V30://移动侦测、视频丢失、遮挡、IO信号量等报警信息(V3.0以上版本支持的设备)
HCNetSDK.NET_DVR_ALARMINFO_V30 struAlarmInfo = new HCNetSDK.NET_DVR_ALARMINFO_V30();
struAlarmInfo.write();
Pointer pAlarmInfo_V30 = struAlarmInfo.getPointer();
pAlarmInfo_V30.write(0, pAlarmInfo.getByteArray(0, struAlarmInfo.size()), 0, struAlarmInfo.size());
struAlarmInfo.read();
switch (struAlarmInfo.dwAlarmType) {
case 0:
log.error("信号量报警");
break;
case 1:
log.error("硬盘满");
break;
case 2:
log.error("信号丢失");
break;
case 3:
log.error("移动侦测");
break;
case 4:
log.error("硬盘未格式化");
break;
case 5:
log.error("读写硬盘出错");
break;
case 6:
log.error("遮挡报警");
break;
case 7:
log.error("制式不匹配");
break;
case 8:
log.error("非法访问");
break;
}
break;
default:
log.info("其他告警,类型:{}", Integer.toHexString(lCommand));
break;
}
}
}
目前为止,一切正常的话,sdk已经成功加载,初始化,(根据需求决定是否布置回调函数,这个后续布防时详细说明),并注册到Spring中。
现在执行后续操作,登录注册与抓图
设备信息结构
public interface Devices {
//设备ip
String getIp();
//设备端口,一般是8000
Short getPort();
//设备登录名
String getUsername();
//设备密码
String getPassword();
//设备通道,(用于区分可见光还是热成像)
Integer getChannel();
}
hc通用操作抽象类
public abstract class AbstractHcNetHandler<T> {
//sdk
private HCNetSDK hcNetSDK;
//需要对接的设备
private final Devices devices;
//登录后返回的id,用于注销设备,登出
protected int id = -1;
//是否已获取到设备工作状态
private boolean workState = false;
//监听句柄
private int lAlarmHandle = -1;
public AbstractHcNetHandler(HCNetSDK sdk, Devices device) {
this.hcNetSDK = sdk;
this.devices = device;
}
/**
* 注册,登录;
*/
public void registerV30() {
if (!registered()) {
NET_DVR_DEVICEINFO_V30 v30 = new NET_DVR_DEVICEINFO_V30();
this.id = this.hcNetSDK.NET_DVR_Login_V30(
devices.getIp(),
devices.getPort(),
devices.getUsername(),
devices.getPassword(),
v30);
} else {
return;
}
if (!registered()) {
error("设备注册失败", true);
} else {
log.info("设备注册成功");
}
}
/**
* 设备工作状态;
*/
@Override
public boolean workState() {
if (workState) return true;
NET_DVR_WORKSTATE_V30 devwork = new NET_DVR_WORKSTATE_V30();
this.workState = this.hcNetSDK.NET_DVR_GetDVRWorkState_V30(this.id, devwork);
return workState;
}
/**
* 撤防,登出,注销;
*/
@Override
public void logout() {
if (registered()) {
//撤防
close();
log.info("登出设备:{}", this.devices);
this.hcNetSDK.NET_DVR_Logout(this.id);
//这个写在项目关闭前清除SDK。
//this.hcNetSDK.NET_DVR_Cleanup();
}
}
protected void error(String message, boolean throwE) {
String s = message +
",异常码:" + this.hcNetSDK.NET_DVR_GetLastError() +
",异常信息:" + HCErrorEnum.getByCode(this.hcNetSDK.NET_DVR_GetLastError()).getMessage() +
",设备信息:" + this.devices.toString();
log.error(s);
if (throwE) throw new RuntimeException(s);
}
@Override
public T invoke() {
try {
//注册,登录
registerV30();
//获取设备工作状态
if (workState()) {
return handler(this.hcNetSDK, this.devices);
} else {
error("获取设备状态异常", true);
}
} catch (Exception e) {
error("执行任务异常", true);
}
// finally {
// logout();
// }
return null;
}
@Override
public int getId() {
return id;
}
@Override
public String getObjectId() {
return this.devices.getIp();
}
@Override
public boolean hasSetAlarm() {
return this.lAlarmHandle != -1;
}
@Override
public void setAlarm() {
if (hasSetAlarm()) return;
registerV30();
//布防需要注册登录,监听则不需要
if (registered()) {
//报警布防参数设置
HCNetSDK.NET_DVR_SETUPALARM_PARAM m_strAlarmInfo = new HCNetSDK.NET_DVR_SETUPALARM_PARAM();
m_strAlarmInfo.dwSize = m_strAlarmInfo.size();
m_strAlarmInfo.byLevel = 0; //布防等级
m_strAlarmInfo.byAlarmInfoType = 1; // 智能交通报警信息上传类型:0- 老报警信息(NET_DVR_PLATE_RESULT),1- 新报警信息(NET_ITS_PLATE_RESULT)
m_strAlarmInfo.byDeployType = 0; //布防类型:0-客户端布防,1-实时布防
m_strAlarmInfo.write();
this.lAlarmHandle = this.hcNetSDK.NET_DVR_SetupAlarmChan_V41(this.id, m_strAlarmInfo);
if (hasSetAlarm()) {
log.info("布防成功:device:{}", devices);
} else {
error("布防失败", false);
}
} else {
error("未注册,无法布防", false);
}
}
@Override
public void close() {
if (hasSetAlarm()) {
if (this.hcNetSDK.NET_DVR_CloseAlarmChan(this.lAlarmHandle)) {
this.lAlarmHandle = -1;
log.info("撤防成功:device:{}", devices);
} else {
error("停止监听失败", false);
}
}
}
protected abstract T handler(HCNetSDK sdk, Devices cameraInfo) throws Exception;
截图实现
public class HCNetImageManager extends AbstractHcNetHandler<Photo> {
private final Image image;
public HCNetImageManager(HCNetSDK sdk, Devices devices, Image image) {
super(sdk, devices);
this.image = image;
}
@Override
protected Photo handler(HCNetSDK sdk, Devices cameraInfo) throws Exception {
//返回图片数据的大小
IntByReference intByReference = new IntByReference();
//NET_DVR_CaptureJPEGPicture_NEW 这个方法是将图片信息抓到内存中去,
//再读取内存写入到文件中去,方便对图片信息进行其他操作
//NET_DVR_CaptureJPEGPicture 可以直接将图片抓取到目标路径
// sdk.NET_DVR_CaptureJPEGPicture(super.id,
// cameraInfo.getChannel(),
// image.getQuality(),
// image.getPhotoPath().getBytes(StandardCharsets.UTF_8));
boolean b = sdk.NET_DVR_CaptureJPEGPicture_NEW(
// 这个是注册登录后的返回值,返回值不等于-1 即成功,详见上上述注册登录方法registerV30()
super.id,
//通道号
cameraInfo.getChannel(),
//JPEG图像参数
image.getQuality(),
//保存JPEG数据的缓冲区
image.getPointer(),
/*输入缓冲区大小
*太小会抓图失败报内存空间太小,1024X100=100kb 实测可以抓图成功,低于70kb就会报异常
*同时这个缓冲区大小是不固定的,取决于你摄像头拍摄的照片质量,有可能当时测试时,设置100kb就可以了,但环境变化了,截取的照片大小超过了阈值,也会报内存空间太小,尽量设置大一些,兼容多个场景
*/
image.getMemSize(),
intByReference);
if (b) {
log.info("抓图到内存成功,返回长度:{}", intByReference.getValue());
} else {
super.error("抓图失败,image:" + image, true);
return null;
}
//实际抓取多长就读多长,使用image.getMemSize() 图片大小会和MemSize一样
byte[] byteArray = image.getPointer().getByteArray(0, intByReference.getValue());
//这里使用的是hutool的工具类写入文件
FileUtil.writeBytes(byteArray, image.getPhotoPath());
image.getPointer().clear(byteArray.length);
return Photo.builder()
.name(FileNameUtil.getName(image.getPhotoPath()))
.path(image.getPhotoPath())
.ip(cameraInfo.getIp())
.build();
}
抓图的参数
public class Image {
/**
* 图片质量
*/
private final HCNetSDK.NET_DVR_JPEGPARA quality;
/**
* 图片内存容器
*/
private Pointer pointer;
/**
* 内存容器大小
*/
private int memSize;
/**
* 图片存放路径
*/
private String photoPath;
private Image(){
this.quality=new HCNetSDK.NET_DVR_JPEGPARA();
}
public static Image builder(){
return new Image();
}
/**
*
* @param wPicSize
* 注意:当图像压缩分辨率为VGA时,支持0=CIF, 1=QCIF, 2=D1抓图,
* 当分辨率为3=UXGA(1600x1200), 4=SVGA(800x600), 5=HD720p(1280x720),6=VGA,7=XVGA, 8=HD900p
* 仅支持当前分辨率的抓图.
*
*
* 0=CIF, 1=QCIF, 2=D1 3=UXGA(1600x1200), 4=SVGA(800x600), 5=HD720p(1280x720),6=VGA
*
* @return
*/
public Image picSize(short wPicSize){
this.quality.wPicSize=wPicSize;
this.quality.write();
return this;
}
/**
* @param wPicQuality <p>片质量系数 0-最好 1-较好 2-一般</p>
*/
public Image picQuality(short wPicQuality){
this.quality.wPicQuality=wPicQuality;
this.quality.write();
return this;
}
/**
* @param memSize <p> 图片内存容器大小 </p>
*/
public Image memSize(int memSize ){
this.memSize=memSize;
this.pointer=new Memory(memSize);
return this;
}
public Image photoDir(String photoPath){
this.photoPath=photoPath;
return this;
}
}
测试
@SpringBootTest
public class HcTest {
@Resource
HCNetSDK sdk;
@Test
public void t1() {
Image image = Image.builder().photoDir("H:\\test\\192.168.1.2.jpg")
.picSize((short) 2)
.picQuality((short) 2)
.memSize(1024 * 100);
Devices devices = devices();
AbstractHcNetHandler<Photo> photoHandler = new HCNetImageManager(sdk, devices, image);
Photo invoke = photoHandler.invoke();
}
public Devices devices() {
return new Devices() {
@Override
public String getIp() {
return "192.168.1.2";
}
@Override
public Short getPort() {
return 8000;
}
@Override
public String getUsername() {
return "admin";
}
@Override
public String getPassword() {
return "admin123";
}
@Override
public Integer getChannel() {
return 1;
}
};
}
}
报警布防
希望设备在遇到某些事件时,我们要捕捉到这一告警并作出相应的动作,执行某些业务,比如设备被遮挡时,环境温度突升,有人员在设备前移动,我要截取当前的设备图片,保存这一记录。
上述将HcNetSDK注册到Spring中时,设置了一个报警回调函数,这个就是设备报警时将信息回调的入口,它是全局唯一的,意味着所有设备报警时都会来这个接口中,设置回调函数时不需要登录设备也侧面印证了这一说法,要接收到报警有两种方式:1:报警布防;2:报警监听
报警监听:不需要逐个登录设备,在sdk加载,初始化,设置回调函数后即可开启监听(我理解的话是在本地开启特定端口,监听报警,设备报警主动上报,这一块儿暂时就没去弄了,后面再研究)
报警布防:报警布防需要登录设备,根据设备的登录返回句柄,根据句柄然后设置布防参数就行了,在上述AbstractHcNetHandler中setAlarm() 就是 报警布防。
public void setAlarm() {
if (hasSetAlarm()) return;
registerV30();
//布防需要注册登录,监听则不需要
if (registered()) {
//报警布防参数设置
HCNetSDK.NET_DVR_SETUPALARM_PARAM m_strAlarmInfo = new HCNetSDK.NET_DVR_SETUPALARM_PARAM();
m_strAlarmInfo.dwSize = m_strAlarmInfo.size();
m_strAlarmInfo.byLevel = 0; //布防等级
m_strAlarmInfo.byAlarmInfoType = 1; // 智能交通报警信息上传类型:0- 老报警信息(NET_DVR_PLATE_RESULT),1- 新报警信息(NET_ITS_PLATE_RESULT)
m_strAlarmInfo.byDeployType = 0; //布防类型:0-客户端布防,1-实时布防
m_strAlarmInfo.write();
this.lAlarmHandle = this.hcNetSDK.NET_DVR_SetupAlarmChan_V41(this.id, m_strAlarmInfo);
if (hasSetAlarm()) {
log.info("布防成功:device:{}", devices);
} else {
error("布防失败", false);
}
} else {
error("未注册,无法布防", false);
}
}
进入报警回调函数后就可以解析相关类型了,就会用到上述AlarmDataParse 那个类,这个参照海康提供的示例来弄的。
在海康管理界面里面配置温度突升报警,左侧事件里面可以配置移动侦测,遮挡报警
测试
@SpringBootTest
public class HcTest {
@Resource
HCNetSDK sdk;
@Test
public void t1() {
Image image = Image.builder().photoDir("H:\\test\\192.168.1.2.jpg")
.picSize((short) 2)
.picQuality((short) 2)
.memSize(1024 * 100);
Devices devices = devices();
//再写个其他实现类也成,我这里偷懒直接用的抓图实现
AbstractHcNetHandler<Photo> photoHandler = new HCNetImageManager(sdk, devices, image);
//告警布防
photoHandler.setAlarm();
//todo 在设备前打个打火机,就会触发温度报警
try {
Thread.sleep(1000 * 60 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public Devices devices() {
return new Devices() {
@Override
public String getIp() {
return "192.168.1.2";
}
@Override
public Short getPort() {
return 8000;
}
@Override
public String getUsername() {
return "admin";
}
@Override
public String getPassword() {
return "admin123";
}
@Override
public Integer getChannel() {
return 1;
}
};
}
}
拉流
设备主要功能还是监控,我们要获取设备的视频流进行统一管理,对单一设备进行视频流的控制,实测拉流再推流到媒体服务器,视频延迟在4-5s。
视频推流会用到FFMPEG,接流用到SRS媒体服务器,可以先在本地安装一个ffmpeg ,试一下将视频流推到服务器,再用播放器播放。
ffmpeg 命令参数有很多,可以在官网查看,本次用到的参数windows和linux可以共用,(之前测试时有些命令windows可以用,linux不能用,就很蛋疼,比如那个降低延迟的参数zerolatency),实际上不考虑性能的话可以直接用ffmpeg 对视频流进行截图,但测试时截图一次要话3s时间,但用sdk的话一台设备截图只需要3-400ms,前提是你已经登录注册且获取设备性能,所以在设计上可以在登录设备后将每台设备与sdk的包装类缓存起来,待需要时直接拿取进行相应操作。
public static final LinkedList<String> DEFAULT_CMD = new LinkedList<String>() {{
add("-rtsp_transport");//设置RTSP传输协议
add("tcp");//tcp/udp
add("-i");//设置输入流
add("input-address");//input流地址占位
add("-threads");//设置线程个数
add("8");//
add("-r");//帧率设定
add("30");//默认25帧
add("-b:v");//输出视频比特率
add("1000k");//
add("-bufsize");//
add("1000k");//
add("-maxrate");//
add("1000k");//
add("-acodec");//设置音频编码格式
add("copy");//直接用输入流音频格式
add("-vcodec");//设置视频编码格式
add("copy");//直接用输入流视频格式
add("-an");//禁用音频
add("-f");//设置输出文件格式
add("flv");//
add("-y");//覆盖输出文件而不询问
add("output-address");//
}};
拼起来就是
ffmpeg -rtsp_transport tcp -i "rtsp拉流地址" -threads 8 -r 30 -b:v 1000k -bufsize 1000k -maxrate 1000k -acodec copy -vcodec copy -an -f flv -y "rtmp推流地址"
海康rtsp拉流地址:rtsp://用户名:密码@设备地址:554/h264/ch1/main/av_stream
比如 rtsp://admin:admin123@192.168.1.2:554/h264/ch1/main/av_stream
rtmp推流地址: rtmp://媒体服务器地址:1935/live/文件名
比如 rtmp://192.168.1.200:1935/live/192.168.1.2
执行完成后,打开vlc或者srs自带的播放器 输入播放地址 http://192.168.1.200:8080/live/192.168.1.2.flv 即可播放
然后用java调用其实就是 用Process调用外部命令,可以在内部侦测程序是否结束,执行是否成功
public static ProcessWrapper exec(LinkedList<String> cmd, boolean startNow, boolean destroyOnRuntimeShutdown, boolean openIOStreams) throws IOException {
//这个是上述maven引入的,点进去可以看到它帮我们复制了一个ffmpeg到本地路径,所以如果把ffmpeg打包进去就会很大,直接输入命令即可执行ffmpeg命令,已经默认在参数前添加了 ffmpeg的全路径
ProcessWrapper process = new DefaultFFMPEGLocator().createExecutor();
cmd.forEach(process::addArgument);
//destroyOnRuntimeShutdown 主程序结束后是否结束
//openIOStreams 是否开启输入,输出流
if (startNow) process.execute(destroyOnRuntimeShutdown, openIOStreams);
return process;
}
这个是ProcessWrapper部分代码,可以看到有三个标准输入输出流,可以读取inputStream和errorStream内容来查看外部标注输出和异常输出,可以通过判断输出内容中关键字来判断进程是否退出或其他异常,虽然Process有接口可以查看进程是否退出,但它也仅仅只能检测进程是否退出(而且命令一开始就执行失败会一直卡住在那里,无法获取到程序返回码,不知道有无大佬知道如何解决),拉流是一直在拉的,中途流媒体服务器异常或设备异常,程序退出就无法判断是哪里故障了。
public class ProcessWrapper implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(ProcessWrapper.class);
private final String ffmpegExecutablePath;
private final ArrayList<String> args = new ArrayList();
private Process ffmpeg = null;
private ProcessKiller ffmpegKiller = null;
private InputStream inputStream = null;
private OutputStream outputStream = null;
private InputStream errorStream = null;
}
测试
public class FFMPEGTest {
public static void main(String[] args) throws IOException {
LinkedList<String> DEFAULT_CMD = new LinkedList<String>() {{
add("-rtsp_transport");//设置RTSP传输协议
add("tcp");//tcp/udp
add("-i");//设置输入流
add("rtsp://admin:admin123@192.168.1.2:554/h264/ch1/main/av_stream");//input流地址占位
add("-threads");//设置线程个数
add("8");//
add("-r");//帧率设定
add("30");//默认25帧
add("-b:v");//输出视频比特率
add("1000k");//
add("-bufsize");//
add("1000k");//
add("-maxrate");//
add("1000k");//
add("-acodec");//设置音频编码格式
add("copy");//直接用输入流音频格式
add("-vcodec");//设置视频编码格式
add("copy");//直接用输入流视频格式
add("-an");//禁用音频
add("-f");//设置输出文件格式
add("flv");//
add("-y");//覆盖输出文件而不询问
add("rtmp://192.168.1.200:1935/live/192.168.1.2");//
}};
ProcessWrapper exec = exec(DEFAULT_CMD, true, false, true);
BufferedReader br = new BufferedReader(new InputStreamReader(exec.getInputStream()));
String msg;
while ((msg = br.readLine()) != null) {
if (msg.contains("error") || msg.contains("fail") || msg.contains("miss") || msg.contains("No such")) {
System.out.println("程序发生异常:" + msg);
} else {
System.out.println("程序执行回显:" + msg);
}
}
exec.destroy();
}
public static ProcessWrapper exec(LinkedList<String> cmd, boolean startNow, boolean destroyOnRuntimeShutdown, boolean openIOStreams) throws IOException {
ProcessWrapper process = new DefaultFFMPEGLocator().createExecutor();
cmd.forEach(process::addArgument);
if (startNow) process.execute(destroyOnRuntimeShutdown, openIOStreams);
return process;
}
}
然后访问媒体服务器上flv格式播放地址即可,测试中使用的是srs默认的推流地址,播放地址对应的是:http://192.168.1.200:8080/live/192.168.1.2.flv (后面这个ip随便取名,你推的时候是什么,拉流的时候就用什么) ;
srs支持转换为多种不同格式的视频流地址,按需使用即可,http-flv使用上对比hls延迟更低,但兼容性上会比hls低。详见(Docker | SRS (ossrs.net))。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签: