引言
近期接到一个需求,需要实现下面2点。
1.实时预览大华网络摄像机(IPC);2.网络硬盘录像机(NVR)的回放。
笔者通过大华官网支持(support)的页面,下载了WEB相关的SDK(WEB无插件开发包),却发现无法实现公网访问。退而求其次寻求替代方案,最后确定使用Java后端转发视频流的方式来实现需求。
本文参考了下面2位作者的文章,在此表示非常感谢。
大华SDK+JAVA+4g网络摄像头进行二次开发(Cljxy~)
vue+flv.js+SpringBoot+websocket实现视频监控与回放(香草味糖葫芦)
开发前准备
1.大华IPC摄像头;2.大华NetSDK_JAVA;3.IntelliJ IDEA。
技术分析
【实时预览】
根据大华《NetSDK_JAVA编程指导手册》的流程图
目标:在【回调fRealDataCallBackEx 解析码流】这个步骤,得到码流推送至前端解码即可。
此流程图中没有外网访问的功能,该功能需要另一份开发文档【NetSDK_JAVA 主动注册】来实现。
【主动注册】
主动注册需要启用监听服务器监听9500端口,然后在网络摄像头的设置中按图设置好服务器的ip地址、端口、子设备ID(需要保证系统唯一性)。
设备会隔间30秒左右向服务器注册自己的信息(【ip地址】,【端口号】,【设备ID】)。然后在设备的信息基础上增加【设备账户】与【设备密码】,即可得到登录句柄。
什么是句柄?
JAVA 开发人员很少接触、使用句柄。句柄是一个唯一的整数,作为对象的身份id,用于区分不同的对象和同类中的不同实例。程序可以通过句柄访问对象的部分信息。JAVA 中任何东西都可以视为对象。我们可以认为操纵的标识符实际是指向一个对象的“句柄"(Handle)。
【最终方案】
1.Java后端通过NetSDK得到IPC回调的FLV流;
2.后端与前端通过websocket进行数据的传输;
3.前端通过后端转发的FLV流,使用flv.js进行解析并播放。
代码实现
1.新建springboot项目
2.导入相关的依赖和资源
lib包与common包可以从大华的demo中直接复制过来。linnx64与win64按需要复制即可。要注意NetSDKLib.java文件在windows与liunx中有差异,请按实际需要选择。
3.SDK启动
按照大华示例,进行相应模块的编写,如LoginModule、AutoRegisterModule、ServiceCB、DisConnect、HaveReConnect的编写,可以复制相关示例或者自己按需编写。
public class LoginModule {
public static NetSDKLib netSdk = NetSDKLib.NETSDK_INSTANCE;
// 设备信息
public static NetSDKLib.NET_DEVICEINFO_Ex m_stDeviceInfo = new NetSDKLib.NET_DEVICEINFO_Ex();
// 登陆句柄
public static NetSDKLib.LLong m_hLoginHandle = new NetSDKLib.LLong(0);
private static boolean bInit = false;
private static boolean bLogopen = false;
/**
* 初始化
*/
public static boolean init(NetSDKLib.fDisConnect disConnect, NetSDKLib.fHaveReConnect haveReConnect) {
bInit = netSdk.CLIENT_Init(disConnect, null);
if(!bInit) {
System.out.println("Initialize SDK failed");
return false;
}
//打开日志,可选
NetSDKLib.LOG_SET_PRINT_INFO setLog = new NetSDKLib.LOG_SET_PRINT_INFO();
File path = new File("./sdklog/");
if (!path.exists()) {
path.mkdir();
}
String logPath = path.getAbsoluteFile().getParent() + "\\sdklog\\" + ToolKits.getDate() + ".log";
setLog.nPrintStrategy = 0;
setLog.bSetFilePath = 1;
System.arraycopy(logPath.getBytes(), 0, setLog.szLogFilePath, 0, logPath.getBytes().length);
System.out.println(logPath);
setLog.bSetPrintStrategy = 1;
bLogopen = netSdk.CLIENT_LogOpen(setLog);
if(!bLogopen ) {
System.err.println("Failed to open NetSDK log");
}
// 设置断线重连回调接口,设置过断线重连成功回调函数后,当设备出现断线情况,SDK内部会自动进行重连操作
// 此操作为可选操作,但建议用户进行设置
netSdk.CLIENT_SetAutoReconnect(haveReConnect, null);
//设置登录超时时间和尝试次数,可选
int waitTime = 5000; //登录请求响应超时时间设置为5S
int tryTimes = 1; //登录时尝试建立链接1次
netSdk.CLIENT_SetConnectTime(waitTime, tryTimes);
// 设置更多网络参数,NET_PARAM的nWaittime,nConnectTryNum成员与CLIENT_SetConnectTime
// 接口设置的登录设备超时时间和尝试次数意义相同,可选
NetSDKLib.NET_PARAM netParam = new NetSDKLib.NET_PARAM();
netParam.nConnectTime = 10000; // 登录时尝试建立链接的超时时间
netParam.nGetConnInfoTime = 3000; // 设置子连接的超时时间
netParam.nGetDevInfoTime = 3000;//获取设备信息超时时间,为0默认1000ms
netSdk.CLIENT_SetNetworkParam(netParam);
return true;
}
/**
* 清除环境
*/
public static void cleanup() {
if(bLogopen) {
netSdk.CLIENT_LogClose();
}
if(bInit) {
netSdk.CLIENT_Cleanup();
}
}
/**
* 登录设备
*/
public static boolean login(String m_strIp, int m_nPort, String m_strUser, String m_strPassword) {
//IntByReference nError = new IntByReference(0);
//入参
NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY pstInParam=new NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY();
pstInParam.nPort=m_nPort;
pstInParam.szIP=m_strIp.getBytes();
pstInParam.szPassword=m_strPassword.getBytes();
pstInParam.szUserName=m_strUser.getBytes();
//出参
NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY pstOutParam=new NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY();
pstOutParam.stuDeviceInfo=m_stDeviceInfo;
m_hLoginHandle = netSdk.CLIENT_LoginWithHighLevelSecurity(pstInParam, pstOutParam);
if(m_hLoginHandle.longValue() == 0) {
System.err.printf("Login Device[%s] Port[%d]Failed. %s\n", m_strIp, m_nPort, ToolKits.getErrorCodePrint());
} else {
System.out.println("Login Success [ " + m_strIp + " ]");
}
return m_hLoginHandle.longValue() == 0? false:true;
}
/**
* 登出设备
*/
public static boolean logout() {
if(m_hLoginHandle.longValue() == 0) {
return false;
}
boolean bRet = netSdk.CLIENT_Logout(m_hLoginHandle);
if(bRet) {
m_hLoginHandle.setValue(0);
}
return bRet;
}
}
public class AutoRegisterModule {
public static final NetSDKLib DHNetSdkLib = NetSDKLib.NETSDK_INSTANCE;
// 监听服务句柄
public static NetSDKLib.LLong mServerHandler = new NetSDKLib.LLong(0);
// 主动注册监听回调
private static ServiceCB serviceCallback = new ServiceCB();
// 设备断线回调
private static DisConnect disConnectCallback = new DisConnect();
// 设备重连通知回调
private static HaveReConnect haveReConnect = new HaveReConnect();
// 设备列表
/**
* 开启服务
* @param address 本地IP地址
* @param port 本地端口, 建议9500
*/
public static boolean startServer(String address, int port) {
//SDK初始化,并设置回调
boolean flag = LoginModule.init(disConnectCallback, haveReConnect);
if (flag){
System.out.println("注册成功");
}
mServerHandler = DHNetSdkLib.CLIENT_ListenServer(address, port, 1000, serviceCallback, null);
if (0 == mServerHandler.longValue()) {
System.err.println("Failed to start server." + ToolKits.getErrorCodePrint());
} else {
System.out.printf("Start server, [Server address %s][Server port %d]\n", address, port);
}
return mServerHandler.longValue() != 0;
}
/**
* 结束服务
*/
public static boolean stopServer() {
boolean flag = false;
if(mServerHandler.longValue() != 0) {
flag = DHNetSdkLib.CLIENT_StopListenServer(mServerHandler);
mServerHandler.setValue(0);
System.out.println("Stop server!");
}
return flag;
}
public static boolean getServerState() {
return mServerHandler.longValue() != 0;
}
/**
* 设备断线回调: 通过 CLIENT_Init 设置该回调函数,当设备出现断线时,SDK会调用该函数
*/
private static class DisConnect implements NetSDKLib.fDisConnect {
public void invoke(NetSDKLib.LLong m_hLoginHandle, String pchDVRIP, int nDVRPort, Pointer dwUser) {
System.out.printf("Device[%s] Port[%d] DisConnect!\n", pchDVRIP, nDVRPort);
}
}
/**
* 设备重连回调: 通过 CLIENT_Init 设置该回调函数,当设备出现断线时,SDK会调用该函数
*/
private static class HaveReConnect implements NetSDKLib.fHaveReConnect {
public void invoke(NetSDKLib.LLong m_hLoginHandle, String pchDVRIP, int nDVRPort, Pointer dwUser) {
System.out.printf("ReConnect Device[%s] Port[%d]\n", pchDVRIP, nDVRPort);
}
}
}
public class ServiceCB implements NetSDKLib.fServiceCallBack {
@Override
public int invoke(NetSDKLib.LLong lHandle, final String pIp, final int wPort,
int lCommand, Pointer pParam, int dwParamLen,
Pointer dwUserData) {
// 将 pParam 转化为序列号
byte[] buffer = new byte[dwParamLen];
pParam.read(0, buffer, 0, dwParamLen);
String deviceId = "";
try {
deviceId = new String(buffer, "GBK").trim();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
System.out.printf("Register Device Info [Device address %s][port %s][DeviceID %s] \n", pIp, wPort, deviceId);
switch(lCommand) {
case NetSDKLib.EM_LISTEN_TYPE.NET_DVR_DISCONNECT: { // 验证期间设备断线回调
break;
}
case NetSDKLib.EM_LISTEN_TYPE.NET_DVR_SERIAL_RETURN: {// 设备注册携带序列号
List<DevicesModule> deviceList = DeviceListUtil.getDeviceList();
for (DevicesModule deviceModule : deviceList) {
if (deviceModule.getDeviceInfo().getDid().equals(deviceId)) {
//设置为虚拟pIp,wPort
deviceModule.getDeviceInfo().setDIp(pIp);
deviceModule.getDeviceInfo().setDPort(wPort);
Executors.newSingleThreadExecutor().submit(new Runnable() {
@Override
public void run() {
//设备登陆,标记状态
deviceModule.login();
}
});
}
}
break;
}
default:
break;
}
return 0;
}
}
这里主要是通过AutoRegisterModule.startServer(serverIP, serverPort)方法来启动自动注册监听,设备通过设置自动注册后,将自己的信息通过回调进ServiceCB中,然后根据设备信息登录来获取【设备登录句柄】。
4.websocket
websocket的示例很多,第一步是pom中引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置WebSocketConfig
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
编写
@ServerEndpoint("/device/monitor/{device}/{channel}")
@Component
@Slf4j
public class WebSocketServer {
public static VideoMonitorService service;
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全
*/
private final AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 存放每个客户端对应的WebSocket对象,根据设备realPlayHandler建立session
*/
public static ConcurrentHashMap<String, Session> sessions = new ConcurrentHashMap<>();
/**
* 保存覆盖播放标识
*/
public static CopyOnWriteArrayList<String> sessionList = new CopyOnWriteArrayList<>();
/**
* 有websocket client连接
* @param device 设备ID
* @param channel 预览句柄
* @param session
*/
@OnOpen
public void OnOpen(@PathParam("device") String device, @PathParam("channel") String channel, Session session) throws InterruptedException {
log.info("连接进入");
//设备ID+预览句柄组成唯一性标识
String uuid = device + channel;
if (sessions.containsKey(uuid)) {
sessions.put(uuid, session);
} else {
sessions.put(uuid, session);
addOnlineCount();
}
log.info("websocket connect.session: " + session);
}
/**
* 连接关闭调用的方法
* @param device 设备ID
* @param channel 预览句柄
* @param session websocket连接对象
*/
@OnClose
public void onClose(@PathParam("device") String device, @PathParam("channel") String channel, Session session){
String uuid = device + channel;
if (sessions.containsKey(uuid)) {
sessions.remove(uuid);
subOnlineCount();
try{
if(service != null){
long handle = AutoRegisterEventModule.findRealPlayInfoByDeviceIdAndChannelNum(uuid);
RealPlayInfo realPlayInfo = AutoRegisterEventModule.findRealPlayInfo(handle);
if(realPlayInfo != null){
System.out.println(uuid + ":websocket 断开连接!");
boolean b = false;
if("1".equals(realPlayInfo.getType())){
b = service.stopPlay(new NetSDKLib.LLong(handle),realPlayInfo.getDeviceId());
}else if("2".equals(realPlayInfo.getType())){
b = service.stopReport(new NetSDKLib.LLong(handle),realPlayInfo.getDeviceId());
}
if(b){
AutoRegisterEventModule.removeRealPlayInfo(handle);
}
}else{
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* 发生错误
* @param throwable e
*/
@OnError
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
/**
* 收到客户端发来消息
* @param message 消息对象
*/
@OnMessage
public void onMessage(ByteBuffer message) {
log.info("服务端收到客户端发来的消息: {}", message);
}
/**
* 收到客户端发来消息
* @param message 字符串类型消息
*/
@OnMessage
public void onMessage(String message) {
log.info("服务端收到客户端发来的消息: {}", message);
}
/**
* 发送消息
* @param message 字符串类型的消息
*/
public void sendAll(String message) {
for (Map.Entry<String, Session> session : sessions.entrySet()) {
session.getValue().getAsyncRemote().sendText(message);
}
}
/**
* 发送binary消息
* @param buffer
*/
public void sendMessage(ByteBuffer buffer) {
for (Map.Entry<String, Session> session : sessions.entrySet()) {
session.getValue().getAsyncRemote().sendBinary(buffer);
}
}
/**
* 转发数据流
* @param realPlayHandler 预览句柄
* @param buffer 码流数据
*/
public void forwardDataFlow(long realPlayHandler, ByteBuffer buffer) {
//登录句柄无效
if (realPlayHandler == 0) {
log.error("loginHandler is invalid.please check.", this);
return;
}
RealPlayInfo realPlayInfo = AutoRegisterEventModule.findRealPlayInfo(realPlayHandler);
if(realPlayInfo == null){
//连接已断开
}
String key = realPlayInfo.getDeviceId()+realPlayInfo.getChannel();
Session session = sessions.get(key);
if (session != null) {
synchronized (session) {
try {
session.getBasicRemote().sendBinary(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
log.error("session is null.please check.", this);
}
}
/**
* 主动关闭websocket连接
* @param realPlayHandler 预览句柄
*/
public void closeSession(long realPlayHandler) {
try {
Session session = sessions.get(realPlayHandler);
if (session != null) {
session.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获取当前连接数
* @return
*/
public int getOnlineCount() {
return onlineCount.get();
}
/**
* 增加当前连接数
* @return
*/
public int addOnlineCount() {
return onlineCount.getAndIncrement();
}
/**
* 减少当前连接数
* @return
*/
public int subOnlineCount() {
return onlineCount.getAndDecrement();
}
public static boolean crrentSession(String key){
if(sessions.containsKey(key)){
return true;
}
return false;
}
}
主要方法是forwardDataFlow方法,在摄像头的流数据回调至NetSDKLib.fRealDataCallBackEx时,用此方法,可以转给对应的前端用于视频流的展示。
5.预览
将预览服务封装进一个service服务中,方便调用
public class VideoMonitorService {
@Autowired
private WebSocketRealDataCallback webSocketRealDataCallback;
/**
* 视频预览
*/
public long startRealPlay(String deviceId,int channelNum,int rType){
DevicesModule devicesModule = DeviceListUtil.getDeviceModuleByDeviceId(deviceId);
if(devicesModule != null)
return devicesModule.startRealPlay(channelNum,webSocketRealDataCallback,deviceId,rType);
return 0;
}
public boolean stopPlay(NetSDKLib.LLong playHandle, String deviceId){
DevicesModule devicesModule = DeviceListUtil.getDeviceModuleByDeviceId(deviceId);
if(devicesModule != null)
return devicesModule.stopPlay(playHandle);
return false;
}
}
通过调用VideoMonitorService中的startRealPlay方法,通过设备ID获取到已在自动注册成功后存储的设备登录句柄,然后再调用封装的startRealPlay方法来实现预览的回调
public long startRealPlay(NetSDKLib.LLong m_hLoginHandle, int channelNum, WebSocketRealDataCallback webSocketRealDataCallback, String deviceId, int rType) {
//入参对象
int emDataType = NetSDKLib.EM_REAL_DATA_TYPE.EM_REAL_DATA_TYPE_FLV_STREAM;
NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE inParam = new NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE();
inParam.emDataType = emDataType;
inParam.nChannelID = channelNum;
inParam.emAudioType = EM_AUDIO_DATA_TYPE.EM_AUDIO_DATA_TYPE_AAC.ordinal();
inParam.rType = rType;
inParam.cbRealData = webSocketRealDataCallback;
inParam.dwUser = null;
//返回对象
NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE stOut = new NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE();
//获取预览句柄
NetSDKLib.LLong lRealHandle = DHNetSdkLib.CLIENT_RealPlayByDataType(m_hLoginHandle, inParam, stOut, 5000);
//开启实时监控
if(lRealHandle.longValue() != 0) {
RealPlayInfo info = new RealPlayInfo(m_hLoginHandle.longValue(), emDataType, channelNum, NetSDKLib.NET_RealPlayType.NET_RType_Realplay,deviceId,"1",lRealHandle.longValue(),null);
realPlayHandlers.put(lRealHandle.longValue(), info);
} else {
System.err.printf("RealPlayByDataType Failed!Last Error[0x%x]\n", DHNetSdkLib.CLIENT_GetLastError());
}
return lRealHandle.longValue();
}
再通过WebSocketRealDataCallback implements NetSDKLib.fRealDataCallBackEx来获取设备流数据的回调。
public class WebSocketRealDataCallback implements NetSDKLib.fRealDataCallBackEx {
@Autowired
private WebSocketServer server;
@Override
public void invoke(NetSDKLib.LLong lRealHandle, int dwDataType, Pointer pBuffer, int dwBufSize, int param, Pointer dwUser) {
RealPlayInfo info = AutoRegisterEventModule.findRealPlayInfo(lRealHandle.longValue());
if (info != null && info.getLoginHandler() != 0) {
//过滤码流
byte[] buffer = pBuffer.getByteArray(0, dwBufSize);
if (info.getEmDataType() == 0 || info.getEmDataType() == 3) {
//选择私有码流或mp4码流,拉流出的码流都是私有码流
if (dwDataType == 0) {
//sendBuffer(buffer, lRealHandle.longValue());
}
} else if ((dwDataType - 1000) == info.getEmDataType()) {
sendBuffer(pBuffer.getByteArray(0, dwBufSize), lRealHandle.longValue());
}
}
}
private void sendBuffer(byte[] bytes, long realPlayHandler) {
/**
* 发送流数据
* 使用pBuffer.getByteBuffer(0,dwBufSize)得到的是一个指向native pointer的ByteBuffer对象,其数据存储在native,
* 而webSocket发送的数据需要存储在ByteBuffer的成员变量hb,使用pBuffer的getByteBuffer得到的ByteBuffer其hb为null
* 所以,需要先得到pBuffer的字节数组,手动创建一个ByteBuffer
*/
ByteBuffer buffer = ByteBuffer.wrap(bytes);
server.forwardDataFlow(realPlayHandler, buffer);
}
}
设备流数据再通过websocket推送给前端,完成闭环。
注意事项
1.需要告诉摄像头回调的流形式需要是FLV格式 NetSDKLib.EM_REAL_DATA_TYPE.EM_REAL_DATA_TYPE_FLV_STREAM;
2.前端如果报错
[TransmuxingController] > DemuxException: type = CodecUnsupported, info = Flv: Unsupported codec in video frame: 12
是因为FLV流的格式不对,因为FLV流要求视频编码为H264格式,声音为AAC格式。你需要检查网络摄像机视频设置视频码流设置是否正确。
3.请务必阅读大华《NetSDK_JAVA开发_FAQ》开发文档。特别是开发环境windows,服务器环境liunx的情况。
最后前端效果图

下载资源