由于工作需要将海康监控的画面在网页上显示,经过查找资料最终实现了。过程中发现网上的资料都不怎么完整,没办法直接用,所以记录一下,也帮后人避避坑。我把核心代码放到下面,完整工程放到码云上。完整工程带有前端页面,简单调整后即可运行。需要的下载参考:hikDemo。
海康
有以下几个关键点:
- flv.js 需要 flv 格式的数据,并且最先接收到的必须是 flv 头,否则无法继续
- VideoDemo.getESRealStreamData 方法中回调返回的是 H264 格式数据
- 回调数据只需要处理 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 即可。