赞
踩
使用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(), //输入缓冲区大小 //这个参数会影响抓图后的图片大小,1024x1024=1M, //太小会抓图失败报内存空间太小,1024X100=100kb 实测可以抓图成功,低于70kb就会报异常 image.getMemSize(), intByReference); if (b) { log.info("抓图到内存成功,返回长度:{}", intByReference.getValue()); } else { super.error("抓图失败,image:" + image, true); return null; } byte[] byteArray = image.getPointer().getByteArray(0, image.getMemSize()); //这里使用的是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))。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。