
wlinker-video-monitor
代码地址:https://gitee.com/wlinker/wlinker-video-monitor
背景与需求
在安防监控、智能楼宇等场景中,海康威视设备作为行业主流硬件,常需要将录像回放功能集成到Web系统中。然而,海康设备的原始视频流格式(如私有协议或MPEG)通常无法直接在浏览器中播放。本文通过一个真实案例,介绍如何利用 JavaCV 、海康SDK 和 Spring Boot,实现设备录像回放流的实时转码与FLV格式输出,最终在浏览器中无缝播放。
技术架构概览
核心组件
-
海康SDK(HCNetSDK):设备控制、录像回放流获取。
-
JavaCV(FFmpeg):视频流抓取、转码(MPEG→FLV)。
-
Spring Boot:HTTP接口封装、异步任务处理。
-
管道流(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输出。其优势在于:
-
高实时性:从设备到浏览器的端到端延迟可控。
-
轻量级:无需中间服务器转存视频文件。