个人博客:banmajio’s blog

海康sdk二次开发系列文章
海康sdk捕获码流数据通过JavaCV推成rtmp流的实现思路(PS流转封装RTMP)
海康sdk进行历史回放时,码流数据回调过快问题的解决方法
海康sdk项目(java)部署Linux环境相关问题总结
海康sdk部署Linux环境下无法播放子码流的问题
海康sdk项目部署Linux系统时出现java.lang.UnstisfiedLinkError:jnidispatch(xxx)not found in resource path错误
通过海康sdk实现指定时间段内的录像文件下载
海康sdk查询指定时间段内NVR的录像文件列表

问题描述

在对监控直播或回放进行抓图操作时,大概有三种方式。

  1. 直接使用播放器抓图,例如video.js、easyplayer.js等播放器大多提供了抓图按钮,可以直接截取播放器画面到客户端。但是缺点就是如果服务需要定时截图就无法满足了。
  2. 使用海康sdk二次开发,调用海康自己的接口如NET_DVR_PlayBackCaptureFile等接口实现抓图功能。但是这些接口无一例外,需要搭配海康的播放库才能实现,也就是需要在调用预览接口或者回放接口是传入窗口句柄,否则调用该接口无效,或返回错误码23(调用次序出错)
  3. 依旧是海康sdk二次开发,可以在预览和回放接口中传入回调函数,捕获ps封装的h264数据,通过opencv时间抓图操作

实现方式

本篇文章以直播的抓图为例作为教程,回放的抓图与之同理。
首先需要明白海康sdk接口调用的流程:
在这里插入图片描述
如图所示,根据流程参考海康sdk文档调用接口,我们的抓图操作是要在启动预览之后,注册了回调函数,并且回调函数中有码流数据回调时进行的。

将回调函数中的码流数据写入到管道流中

首先确保直播预览接口调用无误,并且回调函数可以正常执行,当调用了抓图接口,回调函数中得到了抓图开始的标志 开始将回调的码流数据copy一份到管道流中。管道流的使用请参考海康sdk捕获码流数据通过JavaCV推成rtmp流的实现思路(PS流转封装RTMP)

package com.banmajio.callback;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

import com.junction.sdk.HCNetSDK.FRealDataCallBack_V30;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.ByteByReference;

/**
 * @Title RealDataCallBack.java
 * @description 实时预览回调函数
 * @time 2020年3月17日 下午2:45:08
 * @author banmajio
 **/
public class RealDataCallBack implements FRealDataCallBack_V30 {

	private PipedOutputStream outputStream;// 管道输出流
	private PipedOutputStream picOutputStream;// 抓图管道流

	public static boolean playbackcapture = false;// 开始抓图标志 true:开始抓图 false:结束抓图

	public RealDataCallBack(PipedOutputStream outputStream) {
		this.outputStream = outputStream;
	}

	public void setPicOutputStream(PipedOutputStream picOutputStream) {
		this.picOutputStream = picOutputStream;
	}

	@Override
	public void invoke(NativeLong lRealHandle, int dwDataType, ByteByReference pBuffer, int dwBufSize, Pointer pUser) {
		try {
			if (playbackcapture) {
				// 将数据同时写入抓图管道流中
				picOutputStream.write(pBuffer.getPointer().getByteArray(0, dwBufSize));
			}
			outputStream.write(pBuffer.getPointer().getByteArray(0, dwBufSize));
		} catch (IOException e) {
//			logger.error(e.getMessage());
		}
	}
}

读取管道流获取AVFrame帧,转为图片保存到本地

从管道流中取出数据,喂到javacv的帧抓取器FFmpegFrameGrabber中,获取码流数据的AVFrame帧,通过opencv的函数,将帧转为图片输出。代码如下:

package com.banmajio.play;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.ArrayList;
import java.util.Date;

import javax.imageio.ImageIO;

import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameGrabber.Exception;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.bytedeco.javacv.OpenCVFrameConverter;
import org.bytedeco.opencv.global.opencv_core;
import org.bytedeco.opencv.opencv_core.IplImage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @ClassName: PlayBackCapture
 * @Description:抓图
 * @author: banmajio
 * @date: 2020-11-13
 */
public class PlayBackCapture {

	private final static Logger logger = LoggerFactory.getLogger(PlayBackCapture.class);

	private PipedInputStream picInputStream;// 抓图输入流
	private PipedOutputStream picOutputStream;// 抓图输出流
	private FFmpegFrameGrabber grabber;// 抓流器
	private ArrayList<String> picturePaths = new ArrayList<>();

	public PlayBackCapture(PipedInputStream picInputStream, PipedOutputStream picOutputStream) {
		this.picInputStream = picInputStream;
		this.picOutputStream = picOutputStream;
	}

	public void setPicturePath(String picturepath) {
		picturePaths.add(picturepath);
	}

	public void playBackCapture(String token) throws IOException, InterruptedException {
		try {
			picInputStream.connect(picOutputStream);
			grabber = new FFmpegFrameGrabber(picInputStream, 0);
			//检测管道流中是否存在数据,如果2s后依然没有写入1024的数据,则认为管道流中无数据,避免grabber.start();发生阻塞
			long stime = new Date().getTime();
			while (true) {
				Thread.sleep(100);
				if (new Date().getTime() - stime > 2000) {
					return;
				}
				if (picInputStream.available() == 1024) {
					break;
				}
			}
			grabber.start();
			String rotate = grabber.getVideoMetadata("rotate");// 视频的旋转角度
			Frame frame = null;
			int frameIndex = 0;
			int pictureIndex = 0;
			while (frameIndex < 10) {
			    // 获取image帧
				frame = grabber.grabImage();
				if (null != frame && null != frame.image) {
					IplImage src = null;
					if (null != rotate && rotate.length() > 1) {
						OpenCVFrameConverter.ToIplImage converter = new OpenCVFrameConverter.ToIplImage();
						src = converter.convert(frame);
						frame = converter.convert(rotate(src, Integer.valueOf(rotate)));
					}
					doExecuteFrame(frame, picturePaths.get(pictureIndex), frameIndex);
					logger.info("hcsdk " + " 抓图完成 保存路径为:" + picturePaths.get(pictureIndex));
					pictureIndex++;
					if (pictureIndex < picturePaths.size()) {
						continue;
					} else {
						break;
					}
				}
				frameIndex++;
			}
		} catch (Exception e) {
			logger.info("hcsdk " + " 抓图失败");
			grabber.stop();
			grabber.close();
			picInputStream.close();
			picOutputStream.close();
			e.printStackTrace();
		}
		grabber.stop();
		grabber.close();
		picInputStream.close();
		picOutputStream.close();
	}

	private void doExecuteFrame(Frame frame, String picturepath, int frameIndex) throws IOException {
		if (null == frame || null == frame.image) {
			return;
		}
		Java2DFrameConverter converter = new Java2DFrameConverter();
		BufferedImage bi = converter.getBufferedImage(frame);

		File output = new File(picturepath);
		try {
			ImageIO.write(bi, "jpg", output);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private IplImage rotate(IplImage src, Integer angle) {
		IplImage img = IplImage.create(src.height(), src.width(), src.depth(), src.nChannels());
		opencv_core.cvTranspose(src, img);
		opencv_core.cvFlip(img, img, angle);
		return img;
	}
}

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐