当前位置: 首页 > article >正文

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 的基础上删减修改而来。

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);
        }
    }
}
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; // 返回登录结果
    }
}
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 即可。


http://www.kler.cn/a/523194.html

相关文章:

  • 认识小程序的基本组成结构
  • Flutter_学习记录_基本组件的使用记录
  • Java基础知识总结(二十七)--Jdk5.0新特性:
  • java小白日记32(注解)
  • 【Python】 python实现我的世界(Minecraft)计算器(重制版)
  • 01学习预热篇(D6_正式踏入JVM深入学习前的铺垫)
  • objection的简单使用
  • 一图展示汽车和航空电子领域的安全和互操作性解决方案的概览
  • https数字签名手动验签
  • PythonFlask框架
  • Effective Objective-C 2.0 读书笔记—— objc_msgSend
  • 跨平台物联网漏洞挖掘算法评估框架设计与实现文献综述:物联网设备漏洞挖掘的挑战和机遇
  • iPhone SE(第三代) 设备详情图
  • 约瑟夫问题(信息学奥赛一本通-2037)
  • 具身智能体俯视全局的导航策略!TopV-Nav: 解锁多模态语言模型在零样本目标导航中的顶视空间推理潜力
  • 从源码深入理解One-API框架:适配器模式实现LLM接口对接
  • python Flask-Redis 连接远程redis
  • GWO优化决策树分类预测matlab
  • 硬脂酸单甘油酯(GMS)行业分析
  • LeetCode:509.斐波那契数
  • Linux - 进程间通信(2)
  • python flask 使用 redis写一个例子
  • STranslate 中文绿色版即时翻译/ OCR 工具 v1.3.1.120
  • C语言数据类型及取值范围
  • 构建一个有智能体参与的去中心化RWA零售生态系统商业模型
  • go理论知识记录(入门2)