海康NVR录像回放SDK原始流转FLV视频流:基于Java的流媒体转码(无需安装第三方插件ffmpeg)

news2025/6/9 20:01:34

wlinker-video-monitor
代码地址:https://gitee.com/wlinker/wlinker-video-monitor

背景与需求

在安防监控、智能楼宇等场景中,海康威视设备作为行业主流硬件,常需要将录像回放功能集成到Web系统中。然而,海康设备的原始视频流格式(如私有协议或MPEG)通常无法直接在浏览器中播放。本文通过一个真实案例,介绍如何利用 JavaCV海康SDK 和 Spring Boot,实现设备录像回放流的实时转码与FLV格式输出,最终在浏览器中无缝播放。


技术架构概览

核心组件

  1. 海康SDK(HCNetSDK):设备控制、录像回放流获取。

  2. JavaCV(FFmpeg):视频流抓取、转码(MPEG→FLV)。

  3. Spring Boot:HTTP接口封装、异步任务处理。

  4. 管道流(PipedStream):跨线程数据传输。

流程示意图

[海康设备]
   │
   ↓ (SDK回调流数据)
[HCNetTools] → PipedOutputStream
   │
   ↓ (跨线程传输)
[PipedInputStream] → [FFmpegGrabber]
   │
   ↓ (转码)
[FFmpegRecorder] → HTTP响应流(FLV)
   │
   ↓
[浏览器/VLC播放器]

关键技术实现

1. 海康SDK流获取与管道传输(HCNetTools)

海康SDK通过回调函数返回原始视频流数据,需将其写入管道流供后续处理。核心代码如下:

public class  HCNetTools {

    
    /**
     * 按时间回放录像
     * @param lChannel 通道号
     */
    public void playBackByTime(LocalDateTime startTime, LocalDateTime endTime, int lChannel,Thread thread,PipedInputStream inputStream)
    {
        playBackCallBack = null;
        try {
            pipeOutput = new PipedOutputStream(inputStream);
        } catch (Exception e) {
            StaticLog.error(e);
        }
        HCNetSDK.NET_DVR_VOD_PARA net_dvr_vod_para = new HCNetSDK.NET_DVR_VOD_PARA();
        net_dvr_vod_para.dwSize = net_dvr_vod_para.size();
        net_dvr_vod_para.struIDInfo.dwChannel = lChannel; //通道号
        //开始时间
        net_dvr_vod_para.struBeginTime = getHkTime(startTime);
        //停止时间
        net_dvr_vod_para.struEndTime = getHkTime(endTime);
        net_dvr_vod_para.hWnd = null; // 回放的窗口句柄,若置为空,SDK仍能收到码流数据,但不解码显示
        net_dvr_vod_para.write();
        iPlayBack = hCNetSDK.NET_DVR_PlayBackByTime_V40(lUserID, net_dvr_vod_para);
        if (iPlayBack <= -1) {
            System.out.println("按时间回放失败,错误码为" + hCNetSDK.NET_DVR_GetLastError());
            palybackFlay = true;
            return;
        }
        //开启取流
        IntByReference intP = new IntByReference(54962 * 1024);
        IntByReference intInlen1 = new IntByReference(0);
        boolean bCrtl = hCNetSDK.NET_DVR_PlayBackControl_V40(iPlayBack, HCNetSDK.NET_DVR_PLAYSTART, Pointer.NULL, 0, Pointer.NULL, intInlen1);
        if (bCrtl == false) {
            System.out.println("NET_DVR_PLAYSTART失败,错误码为" + hCNetSDK.NET_DVR_GetLastError());
            return;
        }
        playBackCallBack = new MyPlayDataCallBack(thread,pipeOutput);
        boolean bRet = hCNetSDK.NET_DVR_SetPlayDataCallBack(iPlayBack, playBackCallBack, Pointer.NULL);
        return;
    }

  class MyPlayDataCallBack implements HCNetSDK.FPlayDataCallBack{

        private final Thread thread;

        private boolean flag;

        private final PipedOutputStream pipeOutput;

        private AtomicInteger atomicInteger = new AtomicInteger(0);

        public MyPlayDataCallBack(Thread thread,PipedOutputStream pipeOutput)
        {
            this.thread = thread;
            this.flag = false;
            this.pipeOutput = pipeOutput;
        }

        public void invoke(int lPlayHandle, int dwDataType, Pointer pBuffer, int dwBufSize, int dwUser) {
            //将设备发送过来的回放码流数据写入文件
            //StaticLog.info("回放码流回调次数:{}",atomicInteger.addAndGet(1));
            long offset = 0;
            ByteBuffer buffers = pBuffer.getByteBuffer(offset, dwBufSize);
            byte[] bytes = new byte[dwBufSize];
            buffers.rewind();
            buffers.get(bytes);
            try {
                this.pipeOutput.write(bytes);
                if(atomicInteger.addAndGet(1) % 50 == 0){
                    int i = atomicInteger.get() / 50;
					//StaticLog.info("{},刷出通道:{}",atomicInteger.get(), i);
                    this.pipeOutput.flush();
                    if(i == 1){
                        LockSupport.unpark(thread);
                    }
                }
            } catch (IOException e) {
                stopPlayback();
                getPlaybacktimer().cancel();
                StaticLog.error(e,e.getLocalizedMessage());
            }
        }

    }

    
}

关键设计

  • 多线程协作:通过 LockSupport 实现主线程阻塞与回调线程唤醒,确保FFmpeg在数据到达后开始处理。

  • 缓冲控制:每50次写入刷新管道,平衡实时性与I/O开销。


2. FFmpeg实时转码与HTTP流输出(NetPlayBackController)

控制器从管道流读取数据并转码为FLV格式,通过HTTP响应流式输出:

@Slf4j
@Api(tags = "海康网络设备-录像回放原始流转flv流输出")
@RestController
@RequestMapping("/api/hikNet")
public class NetPlayBackController {
    
    @Resource
    HikCameraAspect aspect;

    @GetMapping(value = "/playBackVideoOne")
    @ApiOperation(value = "海康网络设备-录像回放(基于原始流)-通道不复用",  notes = "响应通道不复用,但较稳定")
    @Async
    public void playBackVideo2(@RequestParam String id, @RequestParam String startTime, @RequestParam String endTime, HttpServletResponse httpServletResponse) throws InterruptedException, IOException {
        //设置响应头
        httpServletResponse.setContentType("video/x-flv");
        httpServletResponse.setHeader("Connection", "keep-alive");
        httpServletResponse.setStatus(HttpServletResponse.SC_OK);
        httpServletResponse.flushBuffer();
        Thread mainThread = Thread.currentThread();
        MonitorDeviceDTO monitorDeviceDTO = VideoKeyUtils.getMonitorDeviceDtoById(id);
        String deviceCode = monitorDeviceDTO.getDeviceCode();
        String accessPlatform = monitorDeviceDTO.getAccessPlatform();
        if (!HikVisionAccessPlatformEnum.net.getAccessPlatform().equals(accessPlatform)) {
            throw new RuntimeException("不支持该平台");
        }
        LocalDateTime startLocalTime = DateUtil.parseLocalDateTime(startTime);
        LocalDateTime endLocalTime = DateUtil.parseLocalDateTime(endTime);
        //单次录像下载时长范围不能超过一小时
        if (startLocalTime.isAfter(endLocalTime)) {
            throw new RuntimeException("开始时间不能大于结束时间");
        }
        if (startLocalTime.plusHours(1).isBefore(endLocalTime)) {
            throw new RuntimeException("时间区间不能超过一小时");
        }
        Integer channelNo = monitorDeviceDTO.getChannelNo();
        aspect.logout(deviceCode);
        aspect.login(deviceCode);
        FFmpegFrameGrabber grabber = null;
        FFmpegFrameRecorder recorder = null;
        HCNetTools hcNetTools = NetDeviceCacheUtils.getHcNetTools(deviceCode);
        try (PipedInputStream inputStream = new PipedInputStream(1024 * 1024);) {
            Thread thread = Thread.currentThread();
            // 启动下载线程:将数据写入管道输出流
            hcNetTools.playBackByTime(startLocalTime, endLocalTime, channelNo, thread, inputStream);
            // 最多等待5秒(配置响应超时时间)
            long delayNanos = TimeUnit.SECONDS.toNanos(5);
            LockSupport.parkNanos(delayNanos);
            // 配置日志级别
            avutil.av_log_set_level(avutil.AV_LOG_FATAL);
            // 配置FFmpegFrameGrabber从管道输入流读取
            grabber = new FFmpegFrameGrabber(inputStream);
            // 明确指定输入格式(部分流需要)
            grabber.setFormat("mpeg");
            grabber.setOption("stimeout", "500000");
            //设置缓存大小,提高画质、减少卡顿花屏
            grabber.setOption("buffer_size", "1024000");
            grabber.start();
            // 配置FFmpegFrameRecorder直接输出FLV到HTTP响应流
            //创建转码器
            recorder = new FFmpegFrameRecorder(
                    httpServletResponse.getOutputStream(), grabber.getImageWidth(),
                    grabber.getImageHeight(),
                    grabber.getAudioChannels());
            //配置转码器
            recorder.setFrameRate(grabber.getFrameRate());
            recorder.setSampleRate(grabber.getSampleRate());
            if (grabber.getAudioChannels() > 0) {
                recorder.setAudioChannels(grabber.getAudioChannels());
                recorder.setAudioBitrate(grabber.getAudioBitrate());
                recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
                //设置视频比例
                //recorder.setAspectRatio(grabber.getAspectRatio());
            }
            recorder.setFormat("flv");
            recorder.setVideoBitrate(grabber.getVideoBitrate());
            recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
            recorder.setVideoOption("color_range", "tv");
            recorder.start();
            Frame frame;
            while ((frame = grabber.grab()) != null) {
                recorder.record(frame);
            }
        } catch (Exception e) {
            log.error("播放视频时发生异常:{}", e.getMessage());
        } finally {
            // 释放资源
            if (recorder != null) {
                try {
                    recorder.close();
                } catch (Exception e) {
                    log.error("关闭recorder发生异常:{}", e.getMessage());
                }
            }
            if (grabber != null) {
                try {
                    grabber.close();
                } catch (Exception e) {
                    log.error("关闭grabber发生异常:{}", e.getMessage());
                }
            }
            hcNetTools.stopPlayback();
            hcNetTools.deviceLogout();
        }
    }

}

关键优化

  • 低延迟:直接传递帧对象,避免内存复制。

  • 自适应参数:从原始流继承分辨率、帧率等属性,确保兼容性。

总结与展望

本文方案通过 海康SDK + JavaCV + 管道流 的组合,实现了设备录像回放流的实时转码与Web输出。其优势在于:

  • 高实时性:从设备到浏览器的端到端延迟可控。

  • 轻量级:无需中间服务器转存视频文件。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

运维Linux之Ansible详解学习(更新中)

什么是Ansible Ansible 是一款新出现的自动化运维工具&#xff0c;基于 Python 开发。以下是对它的详细介绍&#xff1a; 功能特点&#xff1a;集合了众多运维工具的优点&#xff0c;能实现批量系统配置、批量程序部署、批量运行命令等功能。它是基于模块工作的&#xff0c;本…

深入浅出IIC协议 - 从总线原理到FPGA实战开发 -- 第三篇:Verilog实现I2C Master核

第三篇&#xff1a;Verilog实现I2C Master核 副标题 &#xff1a;从零构建工业级I2C控制器——代码逐行解析与仿真实战 1. 架构设计 1.1 模块分层设计 三层架构 &#xff1a; 层级功能描述关键信号PHY层物理信号驱动与采样sda_oe, scl_oe控制层协议状态机与数据流控制state…

ARM笔记-嵌入式系统基础

第一章 嵌入式系统基础 1.1嵌入式系统简介 1.1.1嵌入式系统定义 嵌入式系统定义&#xff1a; 嵌入式系统是以应用为中心&#xff0c;以计算机技术为基础&#xff0c;软硬件可剪裁&#xff0c;对功能、可靠性、成本、体积、功耗等有严格要求的专用计算机系统 ------Any devic…

upload-labs通关笔记-第19关文件上传之条件竞争

系列目录 upload-labs通关笔记-第1关 文件上传之前端绕过&#xff08;3种渗透方法&#xff09; upload-labs通关笔记-第2关 文件上传之MIME绕过-CSDN博客 upload-labs通关笔记-第3关 文件上传之黑名单绕过-CSDN博客 upload-labs通关笔记-第4关 文件上传之.htacess绕过-CSDN…

第5章:任务间通信机制(IPC)全解析

💬 在多线程开发中,线程之间如何协作?如何让一个线程产生数据,另一个线程消费数据?本章聚焦 Zephyr 提供的多种任务间通信机制(IPC)及实战使用技巧。 📚 本章导读 你将学到: Zephyr 提供的常用 IPC 接口:FIFO、消息队列、邮箱、信号量 每种机制适用场景和用法对比…

CAPL自动化-诊断Demo工程

文章目录 前言一、诊断控制面板二、诊断定义三、发送诊断通过类.方法的方式req.SetParameterdiagSetParameter四、SendRequestAndWaitForResponse前言 本文将介绍CANoe的诊断自动化测试,工程可以从CANoe的 Sample Configruration 界面打开,也可以参考下面的路径中打开(以实…

SVN被锁定解决svn is already locked

今天遇到一个问题&#xff0c;svn 在提交代码的时候出现了svn is already locked&#xff0c;解决方案

【深度学习】1. 感知器,MLP, 梯度下降,激活函数,反向传播,链式法则

一、感知机 对于分类问题&#xff0c;我们设定一个映射&#xff0c;将x通过函数f(x)映射到y 1. 感知机的基本结构 感知机&#xff08;Perceptron&#xff09;是最早期的神经网络模型&#xff0c;由 Rosenblatt 在 1958 年提出&#xff0c;是现代神经网络和深度学习模型的雏形…

云原生安全:网络协议TCP详解

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 &#xff08;注&#xff1a;文末附可视化流程图与专有名词说明表&#xff09; 1. 基础概念 TCP&#xff08;Transmission Control Protocol&#xff09;是…

使用CentOS部署本地DeekSeek

一、查看服务器的操作系统版本 cat /etc/centos-release二、下载并安装ollama 1、ollama下载地址&#xff1a; Releases ollama/ollama GitHubGet up and running with Llama 3.3, DeepSeek-R1, Phi-4, Gemma 3, Mistral Small 3.1 and other large language models. - Re…

LLMs之Qwen:《Qwen3 Technical Report》翻译与解读

LLMs之Qwen&#xff1a;《Qwen3 Technical Report》翻译与解读 导读&#xff1a;Qwen3是Qwen系列最新的大型语言模型&#xff0c;它通过集成思考和非思考模式、引入思考调度机制、扩展多语言支持以及采用强到弱的知识等创新技术&#xff0c;在性能、效率和多语言能力方面都取得…

从工程实践角度分析H.264与H.265的技术差异

作为音视频从业者&#xff0c;我们时刻关注着视频编解码技术的最新发展。RTMP推流、轻量级RTSP服务、RTMP播放、RTSP播放等模块是大牛直播SDK的核心功能&#xff0c;在这些模块的实现过程中&#xff0c;H.264和H.265两种视频编码格式的应用实践差异是我们技术团队不断深入思考的…

如何设计一个高性能的短链设计

1.什么是短链 短链接&#xff08;Short URL&#xff09; 是通过算法将长 URL 压缩成简短字符串的技术方案。例如将 https://flowus.cn/veal/share/3306b991-e1e3-4c92-9105-95abf086ae4e 缩短为 https://sourl.cn/aY95qu&#xff0c;用户点击短链时会自动重定向到原始长链接。其…

提升工作效率的可视化笔记应用程序

StickyNotes桌面便签软件介绍 StickyNotes是一款极为简洁的桌面便签应用程序&#xff0c;让您能够快速记录想法、待办事项或其他重要信息。这款工具操作极其直观&#xff0c;只需输入文字内容&#xff0c;选择合适的字体大小和颜色&#xff0c;然后点击添加按钮即可创建个性化…

11|省下钱买显卡,如何利用开源模型节约成本?

不知道课程上到这里&#xff0c;你账户里免费的5美元的额度还剩下多少了&#xff1f;如果你尝试着完成我给的几个数据集里的思考题&#xff0c;相信这个额度应该是不太够用的。而ChatCompletion的接口&#xff0c;又需要传入大量的上下文信息&#xff0c;实际消耗的Token数量其…

机器学习圣经PRML作者Bishop20年后新作中文版出版!

机器学习圣经PRML作者Bishop20年后新书《深度学习&#xff1a;基础与概念》出版。作者克里斯托弗M. 毕晓普&#xff08;Christopher M. Bishop&#xff09;微软公司技术研究员、微软研究 院 科学智 能 中 心&#xff08;Microsoft Research AI4Science&#xff09;负责人。剑桥…

吴恩达机器学习笔记:逻辑回归3

3.判定边界 现在说下决策边界(decision boundary)的概念。这个概念能更好地帮助我们理解逻辑回归的假设函数在计算什么。 在逻辑回归中&#xff0c;我们预测&#xff1a; 当ℎθ (x) > 0.5时&#xff0c;预测 y 1。 当ℎθ (x) < 0.5时&#xff0c;预测 y 0 。 根据…

docker中使用openresty

1.为什么要使用openresty 我这边是因为要使用1Panel&#xff0c;第一个最大的原因&#xff0c;就是图方便&#xff0c;比较可以一键安装。但以前一直都是直接安装nginx。所以需要一个过度。 2.如何查看openResty使用了nginx哪个版本 /usr/local/openresty/nginx/sbin/nginx …

Java 中的 super 关键字

个人总结&#xff1a; 1.子类构造方法中没有显式使用super&#xff0c;Java 也会默认调用父类的无参构造方法 2.当父类中没有无参构造方法&#xff0c;只有有参构造方法时&#xff0c;子类构造方法就必须显式地使用super来调用父类的有参构造方法。 3.如果父类没有定义任何构造…

PCM音频数据的编解码

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据 总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 例如&#xff1a…