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

wlinker-video-monitor

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

背景与需求

在安防监控、智能楼宇等场景中,海康威视设备作为行业主流硬件,常需要将录像回放功能集成到Web系统中。然而,海康设备的原始视频流格式(如私有协议或MPEG)通常无法直接在浏览器中播放。本文通过一个真实案例,介绍如何利用 JavaCV海康SDKSpring 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通过回调函数返回原始视频流数据,需将其写入管道流供后续处理。核心代码如下:

java 复制代码
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响应流式输出:

java 复制代码
@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输出。其优势在于:

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

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

相关推荐
无敌秋7 分钟前
python/c++/java上云
java·c++·python
To_OC7 分钟前
对接大模型流式接口,我被一个 ReadableStream 卡了半小时
前端·node.js·llm
都叫我大帅哥16 分钟前
BCrypt 还是 Argon2?Spring Security 密码加密方案深度解析与实战对比
java·spring
IKUN家族8 小时前
Spring MVC(一)
java·spring·mvc
石小石Orz10 小时前
我发现了开发者AI产品营收的新方向
前端·虚拟现实
老马识途2.010 小时前
关于跨域问题的总结
java·前端
魔力女仆10 小时前
分享一个 JS 鼠标跟随贪吃蛇背景库
开发语言·javascript·计算机外设
都叫我大帅哥11 小时前
Java日期时间三十年战争:从Date考古到LocalDateTime革命,以及数据库与前端的那点事儿
java
Muscleheng11 小时前
SpringBoot 集成 DeepSeek 实现 RAG 文档问答
java·spring boot·ai·springai
2401_8949155311 小时前
GEO 搜索优化完整源码从零部署:环境配置、集群搭建全流程
开发语言·python·tcp/ip·算法·unity