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 即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/960229.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android Studio:视图绑定的岁月变迁(2/100)

一、博文导读 本文是基于Android Studio真实项目,通过解析源码了解真实应用场景,写文的视角和读者是同步的,想到看到写到,没有上帝视角。 前期回顾,本文是第二期。 private Unbinder mUnbinder; 只是声明了一个 接口…

LeetCode | 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。 问总共有多少条不同的路径? 示例 1…

低代码系统-产品架构案例介绍、得帆云(八)

产品名称 得帆云DeCode低代码平台-私有化 得帆云DeMDM主数据管理平台 得帆云DeCode低代码平台-公有云 得帆云DePortal企业门户 得帆云DeFusion融合集成平台 得帆云DeHoop数据中台 名词 概念 云原生 指自己搭建的运维平台,区别于阿里云、腾讯云 Dehoop 指…

使用ensp进行ppp协议综合实验

实验拓扑 实验划分 AR1的Serial3/0/0接口:192.168.1.1/24; AR2的Serial3/0/0接口:192.168.1.2/24; AR2的Serial3/0/1和4/0/0的聚合接口:192.168.2.2/24; AR3的Serial3/0/0和3/0/1的聚合接口:192…

【Python・机器学习】多元回归模型(原理及代码)

前言 自学笔记,分享给语言学/语言教育学方向的,但对语言数据处理感兴趣但是尚未入门,却需要在论文中用到的小伙伴,欢迎大佬们补充或绕道。ps:本文最少限度涉及公式讲解(文科生小白友好体质)&am…

unity免费资源2025-1-26

https://assetstore.unity.com/packages/tools/animation/motion-warping-climb-interact-270046 兑换码KINEMATION2025

Kitchen Racks 2

Kitchen Racks 2 吸盘置物架 Kitchen Racks-CSDN博客

ESMC-600M蛋白质语言模型本地部署攻略

前言 之前介绍了ESMC-6B模型的网络接口调用方法,但申请token比较慢,有网友问能不能出一个本地部署ESMC小模型的攻略,遂有本文。 其实本地部署并不复杂,官方github上面也比较清楚了。 操作过程 环境配置:CUDA 12.1、…

JAVA设计模式:依赖倒转原则(DIP)在Spring框架中的实践体现

文章目录 一、DIP原则深度解析1.1 核心定义1.2 现实比喻 二、Spring中的DIP实现机制2.1 传统实现 vs Spring实现对比 三、Spring中DIP的完整示例3.1 领域模型定义3.2 具体实现3.3 高层业务类3.4 配置类 四、Spring实现DIP的关键技术4.1 依赖注入方式对比4.2 自动装配注解 五、D…

JVM栈溢出线上环境排查

#查看当前Linux系统进程ID、线程ID、CPU占用率(-eo后面跟想要展示的列) ps H -eo pid,tid,%cpups H -eo pid,tid,%cpu |grep tid #使用java jstack 查看进程id下所有线程id的情况 jstack pid 案例2 通过jstack 排查死锁问题 #启动java代码 jstack 进…

Langchain+讯飞星火大模型Spark Max调用

1、安装langchain #安装langchain环境 pip install langchain0.3.3 openai -i https://mirrors.aliyun.com/pypi/simple #灵积模型服务 pip install dashscope -i https://mirrors.aliyun.com/pypi/simple #安装第三方集成,就是各种大语言模型 pip install langchain-comm…

Gradle配置指南:深入解析settings.gradle.kts(Kotlin DSL版)

文章目录 Gradle配置指南:深入解析settings.gradle.kts(Kotlin DSL版)settings.gradle.kts 基础配置选项单项目配置多项目配置 高级配置选项插件管理(Plugin Management)基础配置模板案例:Android项目标准配…

php twig模板引擎详细使用教程

php twig模板引擎 1. 什么是Twig模板引擎 Twig是一个强大且灵活的PHP模板引擎,它提供了一种更简洁和可扩展的方法来创建PHP应用程序的视图层。Twig模板引擎旨在将设计与业务逻辑分离,并为开发人员提供一种更加清晰和易于维护的方式来构建网页。Twig由S…

Java后端之AOP

AOP&#xff1a;面向切面编程&#xff0c;本质是面向特定方法编程 引入依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>示例&#xff1a;记录…

vim的多文件操作

[rootxxx ~]# vim aa.txt bb.txt cc.txt #多文件操作 next #下一个文件 prev #上一个文件 first #第一个文件 last #最后一个文件 快捷键: ctrlshift^ #当前和上个之间切换 说明&#xff1a;快捷键ctrlshift^&#xff0c…

DataSecOps的要点

2020年首次提出&#xff0c;DataSecOps是一种敏捷、全面、内置安全的 方法&#xff0c;用于协调不断变化的数据及其用户&#xff0c;旨在快速提供数据价值&#xff0c; 同时确保数据的私密性、安全性和良好的管理。 强调数据全生命周 期流转运营过程中的内嵌安全属性&#x…

实用工具推荐----wsl安装

一&#xff1a;Win设置修改 Win 搜索 ”启用或关闭windows 功能“ 将如下内容选中 点击升级 重启电脑 二&#xff1a;安装步骤 参考官方文档 适用于 Linux 的 Windows 子系统文档 | Microsoft Learn 下载wsl ubantu发行包 旧版 WSL 的手动安装步骤 | Microsoft Learn 将u…

如何建设一个企业级的数据湖

建设一个企业级的数据湖是一项复杂且系统化的工程&#xff0c;需要从需求分析、技术选型、架构设计到实施运维等多个方面进行综合规划和实施。以下是基于我搜索到的资料&#xff0c;详细阐述如何建设企业级数据湖的步骤和关键要点&#xff1a; 一、需求分析与规划 明确业务需…

如何在 macOS 上安装 PIP ?

PIP 是任何 Python 开发人员必备的工具&#xff0c;因为它简化了安装和管理 Python 包的过程。本教程是为 macOS 用户量身定制的&#xff0c;并假设对使用终端有基本的了解。 必备条件 在安装 PIP 之前&#xff0c;必须确保您的系统上已经安装了 Python。Python 3.4 及更高版…

Kotlin开发(六):Kotlin 数据类,密封类与枚举类

引言 想象一下&#xff0c;你是个 Kotlin 开发者&#xff0c;敲着代码忽然发现业务代码中需要一堆冗长的 POJO 类来传递数据。烦得很&#xff1f;别急&#xff0c;Kotlin 贴心的 数据类 能帮你自动生成 equals、hashCode&#xff0c;直接省时省力&#xff01;再想想需要多种状…