一、前言
如果本文章对你有帮助帮忙点个关注或小赞赞哦。
技术栈:SpringBoot、Websocket、Flv.js框架、大华SDK代码(设备网络SDK下载地址:)
兼容性:PC端,H5(uniapp的app可用页面嵌套方式),不兼容IOS(IOS目前版本不支持FLV流)
二、配置流程
1.主动注册(可连接到公网服务器)
先说一下整体的工作流程,首先通过使用大华摄像头的主动注册功能填写我们的服务器IP(公网IP或局域网IP)和端口,点击设置后摄像头就能够主动连接服务器锁所听的端口,比如我这里服务器监听的端口就为9200,(以下配置面板需要下载大华SDK源码,并运行源码代码或源码打包的jar包)
2. 编码模式修改
需要将编码模式从h265改为h264否则无法实时查看,ConfigTool需要再大华官网中进行下载,野生网站也有,或者问大华客服也会给链接,我这里就懒得贴上了。
三、 源码部分(自己按需引入大华源码SDK)
1.所需依赖
(1)websocket依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.后端代码部分
(1)创建监听StartListener类
这个类主要是用来监听一个端口,让大华摄像头通过主动注册连接上来,这个也是大华提供给我们的SDK,必须使用这个,否则协议不一样会有乱码。我这里开放的9100端口,部署到公网startServer一定要是0.0.0.0否则无法访问,StartListener类这部分基本都是大华SDK里面源码需要的一些代码,觉得麻烦把整个大华SDK源码放入项目让它自动导入就行。
@Component
public class StartListener implements ApplicationListener<ContextRefreshedEvent> {
public static final NetSDKLib netSdk = NetSDKLib.NETSDK_INSTANCE;
// 主动注册监听回调
private SgymCameraServiceImpl.ServiceCB servicCallback = new SgymCameraServiceImpl.ServiceCB();
/**
* 注入启动器
*/
@Autowired
private WebSocketNettyServer webSocketNettyServer;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
InitTest();
AutoRegisterModule.startServer("0.0.0.0",
Integer.parseInt("9100"),
servicCallback);
}
public void InitTest(){
// 初始化SDK库
netSdk.CLIENT_Init(HaveReConnectCallBack.DisConnectCallBack.getInstance(), null);
// 设置断线重连成功回调函数
netSdk.CLIENT_SetAutoReconnect(HaveReConnectCallBack.getInstance(), null);
//打开日志,可选
NetSDKLib.LOG_SET_PRINT_INFO setLog = new NetSDKLib.LOG_SET_PRINT_INFO();
String logPath = new File(".").getAbsoluteFile().getParent() + File.separator + "sdk_log" + File.separator + "sdk.log";
setLog.bSetFilePath = 1;
System.arraycopy(logPath.getBytes(), 0, setLog.szLogFilePath, 0, logPath.getBytes().length);
setLog.bSetPrintStrategy = 1;
setLog.nPrintStrategy = 0;
if (!netSdk.CLIENT_LogOpen(setLog)){
System.err.println("Open SDK Log Failed!!!");
}
}
/**
* 设备重连回调
*/
private static class HaveReConnectCallBack implements NetSDKLib.fHaveReConnect {
private HaveReConnectCallBack() {
}
private static class CallBackHolder {
private static HaveReConnectCallBack instance = new HaveReConnectCallBack();
}
public static HaveReConnectCallBack getInstance() {
return HaveReConnectCallBack.CallBackHolder.instance;
}
public void invoke(NetSDKLib.LLong m_hLoginHandle, String pchDVRIP, int nDVRPort, Pointer dwUser) {
System.out.printf("ReConnect Device[%s] Port[%d]\n", pchDVRIP, nDVRPort);
}
private static class DisConnectCallBack implements NetSDKLib.fDisConnect {
private DisConnectCallBack() {
}
private static class CallBackHolder {
private static HaveReConnectCallBack.DisConnectCallBack instance = new HaveReConnectCallBack.DisConnectCallBack();
}
public static HaveReConnectCallBack.DisConnectCallBack getInstance() {
return HaveReConnectCallBack.DisConnectCallBack.CallBackHolder.instance;
}
public void invoke(NetSDKLib.LLong lLoginID, String pchDVRIP, int nDVRPort, Pointer dwUser) {
System.out.printf("Device[%s] Port[%d] DisConnect!\n", pchDVRIP, nDVRPort);
}
}
}
}
(2)新建业务处理类CameraServiceImpl
具体推送到websocket我就不做示范了,具体根据自己业务来写,摄像头注册上来需要一个map去存储摄像头的一些信息,像登录句柄,预览句柄都要做好关联关系。
@Slf4j
@Service
public class SgymCameraServiceImpl {
/**
* 开始预览
* @param loginHandle
* @param deviceId
*/
public void realplay(NetSDKLib.LLong loginHandle,String deviceId) {
NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE inParam = new NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE();
NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE outParam = new NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE();
inParam.nChannelID = 0;
inParam.rType = 0;
inParam.emDataType = 5;
// inParam.szSaveFileName = "d:/123.mp4";
NetSDKLib.LLong lRealHandle = netsdk.CLIENT_RealPlayByDataType(loginHandle,
inParam, outParam, 20000);
log.info("设备:{},进入开始预览",deviceId);
log.info("lRealHandle---->{},----------->{},-------------->{}",lRealHandle,loginHandle,lRealHandle);
if(lRealHandle.longValue()!=0){
System.out.println("realplay success");
netSdk.CLIENT_SetRealDataCallBackEx(lRealHandle, CbfRealDataCallBackEx.getInstance(),null, 31);
}
}
/**
* \if ENGLISH_LANG
* Start RealPlay
* \else
* 停止预览
* \endif
*/
public synchronized static void stopRealPlay(NetSDKLib.LLong rawLRealHandle,String deviceId) {
if(Objects.isNull(rawLRealHandle)) {
return;
}
boolean bRet = LoginModule.netsdk.CLIENT_StopRealPlay(rawLRealHandle);
if(bRet) {
log.info("设备:{}已停止预览",deviceId);
}
}
/**
* 实时预览数据回调函数--扩展(pBuffer内存由SDK内部申请释放)
*/
private static class CbfRealDataCallBackEx implements NetSDKLib.fRealDataCallBackEx {
private CbfRealDataCallBackEx() {
}
private static class CallBackHolder {
private static CbfRealDataCallBackEx instance = new CbfRealDataCallBackEx();
}
public static CbfRealDataCallBackEx getInstance() {
return CbfRealDataCallBackEx.CallBackHolder.instance;
}
@SneakyThrows
@Override
public void invoke(NetSDKLib.LLong lRealHandle, int dwDataType, Pointer pBuffer,
int dwBufSize, int param, Pointer dwUser) {
byte[] bytes = pBuffer.getByteArray(0, dwBufSize);
if ((dwDataType-1000) == 5) {//回调格式为flv的流
ByteBuffer buffer = ByteBuffer.wrap(bytes);
log.info("flv流-----> {}",buffer);
//这里将流通过websocket推送到前端
}
/* if(0 != lRealHandle.longValue())
{
switch(dwDataType) {
case 0:
*//* ByteBuffer byteBufferFromStream = ByteBuffer.wrap(bytes);
staticWebSocket.sendMessageToOne(0,bytes);*//*
break;
case 1:
//标准视频数据
break;
case 2:
//yuv 数据
break;
case 3:
//pcm 音频数据
break;
case 4:
//原始音频数据
break;
default:
break;
}
}*/
}
}
/**
* 侦听服务器回调函数
*/
public static 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: { // 验证期间设备断线回调
log.info("进入验证期间设备断线回调");
break;
}
case NetSDKLib.EM_LISTEN_TYPE.NET_DVR_SERIAL_RETURN: { // 设备注册携带序列号
log.info("进入设备注册携带序列号--->{}",deviceId);
NetSDKLib.LLong loginLLongId = login(pIp, wPort, "admin", "123456", deviceId);
break;
}
default:
break;
}
return 0;
}
}
}
(3) WebSocket类
具体怎么写根据你们业务来,但是要做好session与设备id或需要查看监控的用户id之间的关联关系
@Slf4j
@Component
@ServerEndpoint("/websocket/{userId}/{deviceId}")
public class WebSocket {
/* @Autowired
private CameraPreviewSession cameraPreviewSession;*/
private static CameraPreviewSession cameraPreviewSession;
@Autowired
public void setDeviceListenerService(CameraPreviewSession cameraPreviewSession) {
WebSocket.cameraPreviewSession = cameraPreviewSession;
}
/**
* 线程安全的无序的集合
*/
private static final CopyOnWriteArraySet<Session> SESSIONS = new CopyOnWriteArraySet<>();
/**
* 存储在线连接数
*/
public static final Map<String, Session> SESSION_POOL = new HashMap<>();
public static final Map<Session, String> USER_SESSION_POOL = new HashMap<>();
public static final Map<Session, String> DEVICE_SESSION_POOL = new HashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId,@PathParam(value = "deviceId") String deviceId) {
try {
SESSIONS.add(session);
SESSION_POOL.put(userId +"-"+ deviceId, session);
USER_SESSION_POOL.put(session,userId +"-"+ deviceId);
DEVICE_SESSION_POOL.put(session,deviceId);
log.info("【WebSocket消息】有新的连接,总数为:" + SESSIONS.size());
cameraPreviewSession.setCameraPreviewSession(deviceId, userId +"-"+ deviceId);
} catch (Exception e) {
e.printStackTrace();
}
}
@OnClose
public void onClose(Session session) {
try {
SESSIONS.remove(session);
String userId = USER_SESSION_POOL.get(session);
SESSION_POOL.remove(userId);
String deviceId = DEVICE_SESSION_POOL.get(session);
int size = cameraPreviewSession.removeDeviceCameraPreviewUserId(userId, deviceId);
USER_SESSION_POOL.remove(session);
DEVICE_SESSION_POOL.remove(session);
Set<NetSDKLib.LLong> longList = cameraPreviewSession.byUserGetLRealHandle(userId);
if (CollectionUtil.isNotEmpty(longList)){
longList.forEach(item->{
if (Objects.nonNull(item)){
SgymCameraServiceImpl.stopRealPlay(item);
}
});
}
cameraPreviewSession.byUserDeleteRealHandle(userId);
if (size<=0){
cameraPreviewSession.removeDeviceCameraPreview(deviceId);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@OnMessage
public void onMessage(String message) {
log.info("【WebSocket消息】收到客户端消息:" + message);
}
/**
* 此为广播消息
*
* @param message 消息
*/
public void sendAllMessage(String message) {
log.info("【WebSocket消息】广播消息:" + message);
for (Session session : SESSIONS) {
try {
if (session.isOpen()) {
session.getAsyncRemote().sendText(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 此为单点消息
*
* @param userId 用户编号
* @param message 消息
*/
public void sendOneMessage(String userId, String message) {
Session session = SESSION_POOL.get(userId);
if (session != null && session.isOpen()) {
try {
synchronized (session) {
log.info("【WebSocket消息】单点消息:" + message);
session.getAsyncRemote().sendText(JSON.toJSONString(message));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 此为单点消息(多人)
*
* @param userIds 用户编号列表
* @param message 消息
*/
public void sendMoreMessage(String[] userIds, String message) {
for (String userId : userIds) {
Session session = SESSION_POOL.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("【WebSocket消息】单点消息:" + message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 实现服务器主动推送
* @param userId 播放拉流的回调句柄
* @param buffer 发送的数据
*/
public void sendMessageToOne(Long userId, ByteBuffer buffer) throws IOException {
// 使用Base64编码器将byte数组转换为Base64字符串
Session session = SESSION_POOL.get(userId.toString());
if (Objects.nonNull(session)){
session.getBasicRemote().sendBinary(buffer);
}
}
public void sendMessageToMulti(String userId, ByteBuffer buffer) throws IOException {
System.out.println("userId----->"+userId);
Session session = SESSION_POOL.get(userId);
if (Objects.nonNull(session)){
try {
synchronized(SESSIONS) {
session.getBasicRemote().sendBinary(buffer);
}
}catch (Exception e){
e.printStackTrace();
session.close();
}
}
}
}
(4)CameraPreviewSession类
public interface CameraPreviewSession {
void setCameraPreviewSession(String deviceId,String userId);
void setCameraPreviewSession(String deviceId, List<String> userId);
void setLRealHandleDeviceSession(String deviceId, NetSDKLib.LLong lRealHandle);
String byLRealHandleGetDeviceId(NetSDKLib.LLong lRealHandle);
NetSDKLib.LLong byDeviceIdGetLRealHandle(String deviceId);
Set<String> byDeviceIdGetUserIds(String deviceId);
void removeDeviceCameraPreview(String deviceId);
void removeDeviceRealHandle(String deviceId);
void removeLRealHandleDevice(NetSDKLib.LLong lRealHandle);
int removeDeviceCameraPreviewUserId(String userId, String deviceId);
void setLRealHandleUserMap(NetSDKLib.LLong lRealHandle,String userId);
String byLRealHandleGetUser(NetSDKLib.LLong lRealHandle);
void byUserDeleteRealHandle(String userId);
Set<NetSDKLib.LLong> byUserGetLRealHandle(String userId);
}
(4)CameraPreviewSessionImpl类
@Service
public class CameraPreviewSessionImpl implements CameraPreviewSession {
private static Map<String, Set<String>> previewSessionKeyMap = new HashMap<>();
private static Map<NetSDKLib.LLong, String> lRealHandleUserMap = new HashMap<>();
private static Map<String,Set<NetSDKLib.LLong>> userLRealHandleMap = new HashMap<>();
private static Map<NetSDKLib.LLong, String> lRealHandleDeviceMap = new HashMap<>();
private static Map<String,NetSDKLib.LLong> deviceRealHandleMap = new HashMap<>();
@Override
public void setCameraPreviewSession(String deviceId, String userId) {
Set<String> rawUserIds = previewSessionKeyMap.get(deviceId);
if (CollectionUtil.isNotEmpty(rawUserIds)){
rawUserIds.add(userId);
previewSessionKeyMap.put(deviceId,rawUserIds);
}else{
Set<String> userIds = new HashSet<>();
userIds.add(userId);
previewSessionKeyMap.put(deviceId,userIds);
}
}
@Override
public void setCameraPreviewSession(String deviceId, List<String> userId) {
Set<String> rawUserIds = previewSessionKeyMap.get(deviceId);
if (CollectionUtil.isNotEmpty(rawUserIds)){
rawUserIds.addAll(userId);
previewSessionKeyMap.put(deviceId,rawUserIds);
}else{
Set<String> userIds = new HashSet<>(userId);
previewSessionKeyMap.put(deviceId,userIds);
}
}
@Override
public void setLRealHandleDeviceSession(String deviceId, NetSDKLib.LLong lRealHandle) {
lRealHandleDeviceMap.put(lRealHandle,deviceId);
deviceRealHandleMap.put(deviceId,lRealHandle);
}
@Override
public String byLRealHandleGetDeviceId(NetSDKLib.LLong lRealHandle) {
return lRealHandleDeviceMap.get(lRealHandle);
}
@Override
public NetSDKLib.LLong byDeviceIdGetLRealHandle(String deviceId) {
return deviceRealHandleMap.get(deviceId);
}
@Override
public Set<String> byDeviceIdGetUserIds(String deviceId) {
Set<String> userIds = previewSessionKeyMap.get(deviceId);
if (CollectionUtil.isEmpty(userIds)){
return new HashSet<>();
}
return userIds;
}
@Override
public void removeDeviceCameraPreview(String deviceId) {
previewSessionKeyMap.remove(deviceId);
}
@Override
public void removeDeviceRealHandle(String deviceId) {
deviceRealHandleMap.remove(deviceId);
}
@Override
public void removeLRealHandleDevice(NetSDKLib.LLong lRealHandle) {
lRealHandleDeviceMap.remove(lRealHandle);
}
@Override
public int removeDeviceCameraPreviewUserId(String userId, String deviceId) {
Set<String> userIds = previewSessionKeyMap.get(deviceId);
if (CollectionUtils.isNotEmpty(userIds)){
userIds.remove(userId);
if (CollectionUtils.isNotEmpty(userIds)){
previewSessionKeyMap.put(deviceId,userIds);
}
return Math.max((userIds.size() - 1), 0);
}
return 0;
}
@Override
public void setLRealHandleUserMap(NetSDKLib.LLong lRealHandle, String userId) {
lRealHandleUserMap.put(lRealHandle,userId);
Set<NetSDKLib.LLong> rawLRealHandles = userLRealHandleMap.get(userId);
Set<NetSDKLib.LLong> newList = new HashSet<>();
newList.add(lRealHandle);
if (rawLRealHandles!=null){
newList.addAll(rawLRealHandles);
}
userLRealHandleMap.put(userId,newList);
}
@Override
public String byLRealHandleGetUser(NetSDKLib.LLong lRealHandle) {
return lRealHandleUserMap.get(lRealHandle);
}
@Override
public void byUserDeleteRealHandle(String userId) {
Set<NetSDKLib.LLong> lLongList = userLRealHandleMap.get(userId);
lLongList.forEach(item->{
lRealHandleUserMap.remove(item);
});
userLRealHandleMap.remove(userId);
}
@Override
public Set<NetSDKLib.LLong> byUserGetLRealHandle(String userId) {
return userLRealHandleMap.get(userId);
}
}
3.前端代码部分
导入flv.js
npm install flv.js
(1)代码部分
<template>
<div>
<video
controls
type="rtmp/flv"
style="width: 100%; min-height: 160.31px"
@click="handleClick"
></video>
<button>开始预览</button>
</div>
</template>
<script>
import flvjs from 'flv.js'
export default {
data() {
return {
flvPlayerList:null,
}
},
computed: {
},
methods: {
// 创建播放实例(我这里用户id和设备id都是1)
handleClick() {
if (flvjs.isSupported()) {
this.flvPlayerList = flvjs.createPlayer(
{
type: 'flv',
//192.168.31.48:10003填写自己的ip和端口
url: `ws://192.168.31.48:10003/websocket/1/1`,
isLive: true, //数据源是否为直播流
hasAudio: false, //数据源是否包含有音频
hasVideo: true, //数据源是否包含有视频
enableStashBuffer: false //是否启用缓存区
},
{
enableWorker: false, //不启用分离线程
enableStashBuffer: false, //关闭IO隐藏缓冲区
autoCleanupSourceBuffer: true //自动清除缓存
}
)
}
},
}
}
</script>
<style scoped lang="scss">
</style>