SpringBoot 使用海康 SDK 和 flv.js 显示监控画面

由于工作需要将海康监控的画面在网页上显示,经过查找资料最终实现了。过程中发现网上的资料都不怎么完整,没办法直接用,所以记录一下,也帮后人避避坑。我把核心代码放到下面,完整工程放到码云上。完整工程带有前端页面,简单调整后即可运行。需要的下载参考:hikDemo

海康

有以下几个关键点:

  1. flv.js 需要 flv 格式的数据,并且最先接收到的必须是 flv 头,否则无法继续
  2. VideoDemo.getESRealStreamData 方法中回调返回的是 H264 格式数据
  3. 回调数据只需要处理 I 帧和 P 帧, I 帧大概接 49 帧 P 帧,需要将 I 帧和下一帧 I 帧前的 P 帧一块打包给 FFmpegFrameRecorder 解析

下方代码是在官方 Demo 的基础上删减修改而来。

java 复制代码
import com.NetSDKDemo.ClientDemo;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.springframework.stereotype.Controller;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;


@ServerEndpoint("/live")
@Controller
public class Websocket {
    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    public static Session session;

    private static FFmpegFrameRecorder recorder;
    private static ByteArrayOutputStream outputStream;
    private static boolean initialized = false;

    /**
     * 连接成功
     *
     * @param session
     */
    @OnOpen
    public void onOpen(Session session) throws IOException {
        Websocket.session = session; // 保存客户端连接的Session对象

        outputStream = new ByteArrayOutputStream();
        recorder = new FFmpegFrameRecorder(outputStream, 0);

        ClientDemo.start();
    }

    /**
     * 连接关闭
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session) {


    }

    /**
     * 接收到消息
     *
     * @param text
     */
    @OnMessage
    public String onMsg(String text) throws IOException {
        System.out.println("连接成功");
        return null;
    }

    public static void sendBuffer(byte[] bytes) {
        try {
            // 使用ByteArrayInputStream作为输入流
            ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);

            // 创建FFmpegFrameGrabber
            FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputStream);
            grabber.setFormat("h264");
            grabber.start();

            if (!initialized) {
                initialized = true;
                recorder = new FFmpegFrameRecorder(outputStream, 0);
                recorder.setVideoCodec(grabber.getVideoCodec());
                recorder.setFormat("flv");
                recorder.setFrameRate(grabber.getFrameRate());
                recorder.setGopSize((int) (grabber.getFrameRate() * 2));
                recorder.setVideoBitrate(grabber.getVideoBitrate());
                recorder.setImageWidth(grabber.getImageWidth());
                recorder.setImageHeight(grabber.getImageHeight());
                recorder.start();
            }

            Frame frame;
            while ((frame = grabber.grab()) != null) {
                recorder.record(frame);
            }

            grabber.stop();
            grabber.release();

            byte[] flvData = outputStream.toByteArray();
            System.out.println("flvData size: " + flvData.length);
            outputStream.reset();

            synchronized (session) {
                session.getBasicRemote().sendBinary(ByteBuffer.wrap(flvData));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
java 复制代码
import Common.osSelect;
import com.sun.jna.Native;
import com.sun.jna.Pointer;

public class ClientDemo {
    static HCNetSDK hCNetSDK = null;
    static int lUserID = -1;// 用户句柄
    static int lPlayHandle = -1;  // 预览句柄
    static FExceptionCallBack_Imp fExceptionCallBack;

    static class FExceptionCallBack_Imp implements HCNetSDK.FExceptionCallBack {
        public void invoke(int dwType, int lUserID, int lHandle, Pointer pUser) {
            System.out.println("异常事件类型:" + dwType);
        }
    }

    /**
     * 动态库加载
     *
     * @return
     */
    private static boolean createSDKInstance() {
        if (hCNetSDK == null) {
            synchronized (HCNetSDK.class) {
                String strDllPath = "";
                try {
                    if (osSelect.isWindows())
                        // win系统加载库路径
                        strDllPath = System.getProperty("user.dir") + "\\lib\\HCNetSDK.dll";

                    else if (osSelect.isLinux())
                        // Linux系统加载库路径
                        strDllPath = System.getProperty("user.dir") + "/lib/libhcnetsdk.so";
                    hCNetSDK = (HCNetSDK) Native.loadLibrary(strDllPath, HCNetSDK.class);
                } catch (Exception ex) {
                    System.out.println("loadLibrary: " + strDllPath + " Error: " + ex.getMessage());
                    return false;
                }
            }
        }
        return true;
    }

    public static void start() {
        if (hCNetSDK == null) {
            if (!createSDKInstance()) {
                System.out.println("Load SDK fail");
                return;
            }
        }
        // linux系统建议调用以下接口加载组件库
        if (osSelect.isLinux()) {
            HCNetSDK.BYTE_ARRAY ptrByteArray1 = new HCNetSDK.BYTE_ARRAY(256);
            HCNetSDK.BYTE_ARRAY ptrByteArray2 = new HCNetSDK.BYTE_ARRAY(256);
            // 这里是库的绝对路径,请根据实际情况修改,注意改路径必须有访问权限
            String strPath1 = System.getProperty("user.dir") + "/lib/libcrypto.so.1.1";
            String strPath2 = System.getProperty("user.dir") + "/lib/libssl.so.1.1";
            System.arraycopy(strPath1.getBytes(), 0, ptrByteArray1.byValue, 0, strPath1.length());
            ptrByteArray1.write();
            hCNetSDK.NET_DVR_SetSDKInitCfg(HCNetSDK.NET_SDK_INIT_CFG_LIBEAY_PATH, ptrByteArray1.getPointer());
            System.arraycopy(strPath2.getBytes(), 0, ptrByteArray2.byValue, 0, strPath2.length());
            ptrByteArray2.write();
            hCNetSDK.NET_DVR_SetSDKInitCfg(HCNetSDK.NET_SDK_INIT_CFG_SSLEAY_PATH, ptrByteArray2.getPointer());
            String strPathCom = System.getProperty("user.dir") + "/lib/";
            HCNetSDK.NET_DVR_LOCAL_SDK_PATH struComPath = new HCNetSDK.NET_DVR_LOCAL_SDK_PATH();
            System.arraycopy(strPathCom.getBytes(), 0, struComPath.sPath, 0, strPathCom.length());
            struComPath.write();
            hCNetSDK.NET_DVR_SetSDKInitCfg(HCNetSDK.NET_SDK_INIT_CFG_SDK_PATH, struComPath.getPointer());
        }
        // SDK初始化,一个程序只需要调用一次
        boolean initSuc = hCNetSDK.NET_DVR_Init();
        // 异常消息回调
        if (fExceptionCallBack == null) {
            fExceptionCallBack = new FExceptionCallBack_Imp();
        }
        Pointer pUser = null;
        if (!hCNetSDK.NET_DVR_SetExceptionCallBack_V30(0, 0, fExceptionCallBack, pUser)) {
            return;
        }
        System.out.println("设置异常消息回调成功");
        // 启动SDK写日志
        hCNetSDK.NET_DVR_SetLogToFile(3, "./sdkLog", false);

        // 设备登录
        lUserID = loginDevice("192.168.89.19", (short) 8000, "admin", "admin123");

        System.out.println("实时获取裸码流示例代码");
        lPlayHandle = VideoDemo.getESRealStreamData(lUserID, 35);
    }

    /**
     * 登录设备,支持 V40 和 V30 版本,功能一致。
     *
     * @param ip   设备IP地址
     * @param port SDK端口,默认为设备的8000端口
     * @param user 设备用户名
     * @param psw  设备密码
     * @return 登录成功返回用户ID,失败返回-1
     */
    public static int loginDevice(String ip, short port, String user, String psw) {
        // 创建设备登录信息和设备信息对象
        HCNetSDK.NET_DVR_USER_LOGIN_INFO loginInfo = new HCNetSDK.NET_DVR_USER_LOGIN_INFO();
        HCNetSDK.NET_DVR_DEVICEINFO_V40 deviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40();

        // 设置设备IP地址
        byte[] deviceAddress = new byte[HCNetSDK.NET_DVR_DEV_ADDRESS_MAX_LEN];
        byte[] ipBytes = ip.getBytes();
        System.arraycopy(ipBytes, 0, deviceAddress, 0, Math.min(ipBytes.length, deviceAddress.length));
        loginInfo.sDeviceAddress = deviceAddress;

        // 设置用户名和密码
        byte[] userName = new byte[HCNetSDK.NET_DVR_LOGIN_USERNAME_MAX_LEN];
        byte[] password = psw.getBytes();
        System.arraycopy(user.getBytes(), 0, userName, 0, Math.min(user.length(), userName.length));
        System.arraycopy(password, 0, loginInfo.sPassword, 0, Math.min(password.length, loginInfo.sPassword.length));
        loginInfo.sUserName = userName;

        // 设置端口和登录模式
        loginInfo.wPort = port;
        loginInfo.bUseAsynLogin = false; // 同步登录
        loginInfo.byLoginMode = 0; // 使用SDK私有协议

        // 执行登录操作
        int userID = hCNetSDK.NET_DVR_Login_V40(loginInfo, deviceInfo);
        if (userID == -1) {
            System.err.println("登录失败,错误码为: " + hCNetSDK.NET_DVR_GetLastError());
        } else {
            System.out.println(ip + " 设备登录成功!");
            // 处理通道号逻辑
            int startDChan = deviceInfo.struDeviceV30.byStartDChan;
            System.out.println("预览起始通道号: " + startDChan);
        }
        return userID; // 返回登录结果
    }
}
java 复制代码
import com.demo.impl.Websocket;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;

import static com.NetSDKDemo.ClientDemo.hCNetSDK;

/**
 * 视频取流预览,下载,抓图mok
 *
 * @create 2022-03-30-9:48
 */
public class VideoDemo {
    static fPlayEScallback fPlayescallback; // 裸码流回调函数
    static FileOutputStream outputStream;
    static IntByReference m_lPort = new IntByReference(-1);

    /**
     * 获取实时裸码流回调数据
     *
     * @param userID     登录句柄
     * @param iChannelNo 通道号参数
     */
    public static int getESRealStreamData(int userID, int iChannelNo) {
        if (userID == -1) {
            System.out.println("请先注册");
            return -1;
        }
        HCNetSDK.NET_DVR_PREVIEWINFO previewInfo = new HCNetSDK.NET_DVR_PREVIEWINFO();
        previewInfo.read();
        previewInfo.hPlayWnd = null;  // 窗口句柄,从回调取流不显示一般设置为空
        previewInfo.lChannel = iChannelNo;  // 通道号
        previewInfo.dwStreamType = 0; // 0-主码流,1-子码流,2-三码流,3-虚拟码流,以此类推
        previewInfo.dwLinkMode = 1; // 连接方式:0- TCP方式,1- UDP方式,2- 多播方式,3- RTP方式,4- RTP/RTSP,5- RTP/HTTP,6- HRUDP(可靠传输) ,7- RTSP/HTTPS,8- NPQ
        previewInfo.bBlocked = 1;  // 0- 非阻塞取流,1- 阻塞取流
        previewInfo.byProtoType = 0; // 应用层取流协议:0- 私有协议,1- RTSP协议
        previewInfo.write();
        // 开启预览
        int Handle = hCNetSDK.NET_DVR_RealPlay_V40(userID, previewInfo, null, null);
        if (Handle == -1) {
            int iErr = hCNetSDK.NET_DVR_GetLastError();
            System.err.println("取流失败" + iErr);
            return -1;
        }
        System.out.println("取流成功");

        // 设置裸码流回调函数
        if (fPlayescallback == null) {
            fPlayescallback = new fPlayEScallback();
        }
        if (!hCNetSDK.NET_DVR_SetESRealPlayCallBack(Handle, fPlayescallback, null)) {
            System.err.println("设置裸码流回调失败,错误码:" + hCNetSDK.NET_DVR_GetLastError());
        }

        /*

        Boolean bStopSaveVideo = hCNetSDK.NET_DVR_StopSaveRealData(lPlay);
        if (bStopSaveVideo == false) {
            int iErr = hCNetSDK.NET_DVR_GetLastError();
            System.out.println("NET_DVR_StopSaveRealData failed" + iErr);
            return;
        }
            System.out.println("NET_DVR_StopSaveRealData suss");


        if (lPlay>=0) {
            if (hCNetSDK.NET_DVR_StopRealPlay(lPlay))
            {
                System.out.println("停止预览成功");
                return;
            }
        }*/
        return Handle;
    }

    static class fPlayEScallback implements HCNetSDK.FPlayESCallBack {
        private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        private boolean start = false;

        public void invoke(int lPreviewHandle, HCNetSDK.NET_DVR_PACKET_INFO_EX pstruPackInfo, Pointer pUser) {
            pstruPackInfo.read();
            // 保存I帧和P帧数据
            // 从第一帧 I 帧开始解析
            if (pstruPackInfo.dwPacketType == 1) {
                start = true;
            }
            if (!start) {
                return;
            }
            if (pstruPackInfo.dwPacketType == 1 || pstruPackInfo.dwPacketType == 3) {
                // 如果是 I 帧,则将上一帧 I 帧到当前 I 帧的数据发送给 Websocket 解析
                if (pstruPackInfo.dwPacketType == 1) {
                    byte[] byteArray = outputStream.toByteArray();
                    outputStream.reset();
                    if (byteArray.length > 0) {
                        // 通过websocket发送
                        long start = System.currentTimeMillis();
                        Websocket.sendBuffer(byteArray);
                        System.out.println("cost: "+(System.currentTimeMillis() - start));
                    }
                }

                // System.out.println("dwPacketType:" + pstruPackInfo.dwPacketType
                //         + ":wWidth:" + pstruPackInfo.wWidth
                //         + ":wHeight:" + pstruPackInfo.wHeight
                //         + ":包长度:" + pstruPackInfo.dwPacketSize
                //         + ":帧号:" + pstruPackInfo.dwFrameNum);
                ByteBuffer buffers = pstruPackInfo.pPacketBuffer.getByteBuffer(0, pstruPackInfo.dwPacketSize);
                byte[] bytes = new byte[pstruPackInfo.dwPacketSize];
                buffers.rewind();
                buffers.get(bytes);
                try {
                    outputStream.write(bytes);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Websocket 建立连接后执行 onOpen 方法,保存 session ,初始化 FFmpegFrameRecorder,然后启动 ClientDemo。

ClientDemo 的代码基本上都是官方 Demo 的,修改的地方在 start 方法。 start 方法是在原 main 方法的基础上删除输入控制部分,直接调用 VideoDemo 的 getESRealStreamData 方法。

VideoDemo 原代码中有两个和实时预览相关的方法,上方代码使用的是 getESRealStreamData 方法,此方法返回的是 H264 编码的帧数据。帧的类型有多种,需要解析的是 I 帧和 P 帧。I 帧和 I 帧之间有多个 P 帧,将打印帧信息的代码注释后可以看到一般是 1 帧 I 帧紧跟 49 帧 P 帧。解析帧数据时必须从 I 帧开始,等到下一个 I 帧到来后将累计的数据交给 FFmpegFrameRecorder 解析,然后将封装成的 flv 格式数据发给前端的 flv.js 解析然后显示。

注意: I 帧和 P 帧 1:49 的比例不是固定的,必须等待下一帧 I 帧到来。

大华

大华的更简单,调用 Demo 中的 CommonWithCallBack.RealPlayByDataType 方法,确保

stIn.emDataType = EM_REAL_DATA_TYPE.EM_REAL_DATA_TYPE_FLV_STREAM,然后在 RealDataCallBack 的 invoke 方法的

if (dwDataType == (NetSDKLib.NET_DATA_CALL_BACK_VALUE + EM_REAL_DATA_TYPE.EM_REAL_DATA_TYPE_FLV_STREAM)) 块中将数据直接通过 Websocket 传给 flv.js 即可。

相关推荐
XMYX-02 小时前
Spring Boot + Prometheus 实现应用监控(基于 Actuator 和 Micrometer)
spring boot·后端·prometheus
@yanyu6664 小时前
springboot实现查询学生
java·spring boot·后端
酷爱码4 小时前
Spring Boot项目中JSON解析库的深度解析与应用实践
spring boot·后端·json
java干货5 小时前
虚拟线程与消息队列:Spring Boot 3.5 中异步架构的演进与选择
spring boot·后端·架构
武昌库里写JAVA8 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
小白杨树树8 小时前
【WebSocket】SpringBoot项目中使用WebSocket
spring boot·websocket·网络协议
clk660714 小时前
Spring Boot
java·spring boot·后端
爱敲代码的TOM15 小时前
基于JWT+SpringSecurity整合一个单点认证授权机制
spring boot