内容
前面一篇文章:Java对接海康SDK并推往ZLM4J实现超低延迟播放 我们介绍了如何实时取流的功能。这篇文章介绍如何取回放流并实现倍速播放功能。
需要资源
- ZLM4J相关知识:ZLM4J使用文档
- 海康SDK相关开发知识
实操
登录海康SDK并配置ZLM4J相关
/**
* 录像回放demo
*
* @author lidaofu
* @since 2023/12/1
**/
public class RecordPlayDemo {
public static ZLMApi ZLM_API = Native.load("mk_api", ZLMApi.class);
public static HCNetSDK hCNetSDK = Native.load("HCNetSDK", HCNetSDK.class);
static int lUserID = 0;
public static void main(String[] args) throws InterruptedException {
//初始化zmk服务器
ZLM_API.mk_env_init1(1, 1, 1, null, 0, 0, null, 0, null, null);
//创建http服务器 0:失败,非0:端口号
short http_server_port = ZLM_API.mk_http_server_start((short) 7788, 0);
//创建rtsp服务器 0:失败,非0:端口号
short rtsp_server_port = ZLM_API.mk_rtsp_server_start((short) 7554, 0);
//创建rtmp服务器 0:失败,非0:端口号
short rtmp_server_port = ZLM_API.mk_rtmp_server_start((short) 7935, 0);
//初始化海康SDK
boolean initSuc = hCNetSDK.NET_DVR_Init();
if (!initSuc) {
System.out.println("海康SDK初始化失败");
return;
}
//登录海康设备
Login_V40("172.16.6.236", (short) 8000, "admin", "telit123");
MK_INI mkIni = ZLM_API.mk_ini_create();
ZLM_API.mk_ini_set_option(mkIni, "enable_rtsp", "1");
ZLM_API.mk_ini_set_option(mkIni, "enable_rtmp", "1");
//创建媒体
MK_MEDIA mkMedia = ZLM_API.mk_media_create2("__defaultVhost__", "live", "record", 0, mkIni);
//释放资源
ZLM_API.mk_ini_release(mkIni);
//这里分辨率、帧率、码率都可随便写 0是H264 1是h265 可以事先定义好 也可以放到回调里面判断编码类型让后再初始化这个
ZLM_API.mk_media_init_video(mkMedia, 0, 1280, 720, 25.0f, 2500);
ZLM_API.mk_media_init_audio(mkMedia, 2, 8000, 1, 16);
ZLM_API.mk_media_init_complete(mkMedia);
FPlayDataCallback fPlayDataCallBack = new FPlayDataCallback(mkMedia, 25.0, 2.0);
HCNetSDK.NET_DVR_TIME startDvrTime = new HCNetSDK.NET_DVR_TIME();
HCNetSDK.NET_DVR_TIME endDvrTime = new HCNetSDK.NET_DVR_TIME();
startDvrTime.dwYear = 2024;
startDvrTime.dwMonth = 4;
startDvrTime.dwDay = 30;
startDvrTime.dwHour = 11;
startDvrTime.dwMinute = 0;
startDvrTime.dwSecond = 0;
startDvrTime.write();
endDvrTime.dwYear = 2024;
endDvrTime.dwMonth = 4;
endDvrTime.dwDay = 30;
endDvrTime.dwHour = 11;
endDvrTime.dwMinute = 20;
endDvrTime.dwSecond = 0;
endDvrTime.write();
long ret = hCNetSDK.NET_DVR_PlayBackByTime(lUserID, 33, startDvrTime, endDvrTime, null);
if (ret == -1) {
System.out.println("【海康SDK】使用sdk播放回放失败! 错误码:{}" + hCNetSDK.NET_DVR_GetLastError());
return;
}
boolean flag = hCNetSDK.NET_DVR_PlayBackControl(ret, HCNetSDK.NET_DVR_PLAYSTART, 0, null);
if (flag) {
flag = hCNetSDK.NET_DVR_SetPlayDataCallBack_V40(ret, fPlayDataCallBack, Pointer.NULL);
}else {
System.out.println("【海康SDK】使用sdk播放回放失败! 错误码:{}" + hCNetSDK.NET_DVR_GetLastError());
}
//休眠
Thread.sleep(120000);
flag = hCNetSDK.NET_DVR_PlayBackControl(ret, HCNetSDK.NET_DVR_PLAYSTOP, 0, null);
//释放资源
fPlayDataCallBack.release();
//fRealDataCallBack.release();
hCNetSDK.NET_DVR_StopRealPlay(ret);
Logout();
}
/**
* 登录
*
* @param m_sDeviceIP 设备ip地址
* @param wPort 端口号,设备网络SDK登录默认端口8000
* @param m_sUsername 用户名
* @param m_sPassword 密码
*/
public static void Login_V40(String m_sDeviceIP, short wPort, String m_sUsername, String m_sPassword) {
/* 注册 */
// 设备登录信息
HCNetSDK.NET_DVR_USER_LOGIN_INFO m_strLoginInfo = new HCNetSDK.NET_DVR_USER_LOGIN_INFO();
// 设备信息
HCNetSDK.NET_DVR_DEVICEINFO_V40 m_strDeviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40();
m_strLoginInfo.sDeviceAddress = new byte[HCNetSDK.NET_DVR_DEV_ADDRESS_MAX_LEN];
System.arraycopy(m_sDeviceIP.getBytes(), 0, m_strLoginInfo.sDeviceAddress, 0, m_sDeviceIP.length());
m_strLoginInfo.wPort = wPort;
m_strLoginInfo.sUserName = new byte[HCNetSDK.NET_DVR_LOGIN_USERNAME_MAX_LEN];
System.arraycopy(m_sUsername.getBytes(), 0, m_strLoginInfo.sUserName, 0, m_sUsername.length());
m_strLoginInfo.sPassword = new byte[HCNetSDK.NET_DVR_LOGIN_PASSWD_MAX_LEN];
System.arraycopy(m_sPassword.getBytes(), 0, m_strLoginInfo.sPassword, 0, m_sPassword.length());
// 是否异步登录:false- 否,true- 是
m_strLoginInfo.bUseAsynLogin = false;
// write()调用后数据才写入到内存中
m_strLoginInfo.write();
lUserID = hCNetSDK.NET_DVR_Login_V40(m_strLoginInfo, m_strDeviceInfo);
if (lUserID == -1) {
System.out.println("登录失败,错误码为" + hCNetSDK.NET_DVR_GetLastError());
return;
} else {
System.out.println("登录成功!");
// read()后,结构体中才有对应的数据
m_strDeviceInfo.read();
return;
}
}
//设备注销 SDK释放
public static void Logout() {
if (lUserID >= 0) {
if (!hCNetSDK.NET_DVR_Logout(lUserID)) {
System.out.println("注销失败,错误码为" + hCNetSDK.NET_DVR_GetLastError());
}
System.out.println("注销成功");
hCNetSDK.NET_DVR_Cleanup();
return;
} else {
System.out.println("设备未登录");
hCNetSDK.NET_DVR_Cleanup();
return;
}
}
}
海康SDK取回放流回调
public class FPlayDataCallback implements HCNetSDK.FPlayDataCallBack {
private final MK_MEDIA mkMedia;
private final Memory buffer = new Memory(1024 * 1024 * 5);
private int bufferSize = 0;
private int dataSize = 0;
private long pts;
private double fps;
private double multiplier;
private long time_base;
private long last_send_time;
private int videoType=0;
private int audioType=0;
public FPlayDataCallback(MK_MEDIA mkMedia, double fps, double multiplier) {
this.mkMedia = mkMedia;
this.fps = fps;
this.multiplier = multiplier;
//ZLM以1000为时间基准
time_base = (long) (1000 / (fps * multiplier));
//回调使用同一个线程
Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "HikPlayBackStream"));
}
@Override
public void invoke(long lPlayHandle, int dwDataType, Pointer pointer, int dwBufSize, int dwUser) {
if (dwDataType == HCNetSDK.NET_DVR_STREAMDATA) {
int offset = 0;
readData(pointer, offset, dwBufSize);
}
}
/**
* 读取数据
*
* @param pointer
* @param offset
* @param dwSize
*/
private void readData(Pointer pointer, int offset, int dwSize) {
//解析psh头 psm头 psm标题
offset = readPSHAndPSMAndPSMT(pointer, offset, dwSize);
//读取pes数据
readPES(pointer, offset, dwSize);
}
/**
* 读取pes及数据
*
* @param pointer
* @param offset
*/
private void readPES(Pointer pointer, int offset, int dwSize) {
//pes header
byte[] pesHeaderStartCode = new byte[3];
pointer.read(offset, pesHeaderStartCode, 0, pesHeaderStartCode.length);
if ((pesHeaderStartCode[0] & 0xFF) == 0x00 && (pesHeaderStartCode[1] & 0xFF) == 0x00 && (pesHeaderStartCode[2] & 0xFF) == 0x01) {
offset = offset + pesHeaderStartCode.length;
byte[] streamTypeByte = new byte[1];
pointer.read(offset, streamTypeByte, 0, streamTypeByte.length);
offset = offset + streamTypeByte.length;
int streamType = streamTypeByte[0] & 0xFF;
//视频流
if (streamType >= 0xE0 && streamType <= 0xEF) {
//视频数据
readVideoES(pointer, offset, dwSize);
} else if (streamType >= 0xC0 & streamType <= 0xDF) {
//音频数据
readAudioES(pointer, offset);
}
}
}
/**
* 读取视频数据
*
* @param pointer
* @param offset
*/
private void readVideoES(Pointer pointer, int offset, int dwSize) {
byte[] pesLengthByte = new byte[2];
pointer.read(offset, pesLengthByte, 0, pesLengthByte.length);
offset = offset + pesLengthByte.length;
int pesLength = (pesLengthByte[0] & 0xFF) << 8 | (pesLengthByte[1] & 0xFF);
//pes数据
if (pesLength > 0) {
byte[] pts_dts_length_info = new byte[3];
pointer.read(offset, pts_dts_length_info, 0, pts_dts_length_info.length);
offset = offset + pts_dts_length_info.length;
int pesHeaderLength = (pts_dts_length_info[2] & 0xFF);
//判断是否是有pts 忽略dts
int i = (pts_dts_length_info[1] & 0xFF) >> 6;
if (i == 0x02 || i == 0x03) {
//byte[] pts_dts = new byte[5];
//pointer.read(offset, pts_dts, 0, pts_dts.length);
//这里获取的是以90000为时间基的 需要转为 1/1000为基准的 但是pts还是不够平滑导致画面卡顿 所以不采用读取的pts
//long pts_90000 = ((pts_dts[0] & 0x0e) << 29) | (((pts_dts[1] << 8 | pts_dts[2]) & 0xfffe) << 14) | (((pts_dts[3] << 8 | pts_dts[4]) & 0xfffe) >> 1);
pts = time_base + pts;
}
offset = offset + pesHeaderLength;
byte[] naluStart = new byte[5];
pointer.read(offset, naluStart, 0, naluStart.length);
//nalu起始标志
if ((naluStart[0] & 0xFF) == 0x00 && (naluStart[1] & 0xFF) == 0x00 && (naluStart[2] & 0xFF) == 0x00 && (naluStart[3] & 0xFF) == 0x01) {
if (bufferSize != 0) {
//nalu类型 如果是sps pps 则跳过时间等待
int naluType = (naluStart[4] & 0x1F);
if (naluType != 7 && naluType != 8) {
long now = System.currentTimeMillis();
if (last_send_time != 0) {
long diff = now - last_send_time;
if (diff < time_base) {
try {
Thread.sleep(time_base - diff);
} catch (InterruptedException ignored) {
}
}
}
last_send_time = now;
}else {
pts=pts-time_base;
}
if (videoType==0x1B) {
//推送h264帧数据
ZLM_API.mk_media_input_h264(mkMedia, buffer.share(0), bufferSize, pts, pts);
}else if (videoType==0x24) {
//推送h265帧数据
ZLM_API.mk_media_input_h265(mkMedia, buffer.share(0), bufferSize, pts, pts);
}
bufferSize = 0;
}
}
int naluLength = pesLength - pts_dts_length_info.length - pesHeaderLength;
int surplus = dwSize - offset;
//说明有剩余数据
if (surplus > naluLength) {
byte[] temp = new byte[naluLength];
pointer.read(offset, temp, 0, naluLength);
buffer.write(bufferSize, temp, 0, naluLength);
bufferSize = naluLength + bufferSize;
offset = offset + naluLength;
//继续解析数据
readData(pointer, offset, dwSize);
} else {
byte[] temp = new byte[surplus];
pointer.read(offset, temp, 0, surplus);
buffer.write(bufferSize, temp, 0, surplus);
bufferSize = surplus + bufferSize;
//还需要读取多少数据
dataSize = naluLength - surplus;
}
}
}
/**
* 读取音频数据
*
* @param pointer
* @param offset
*/
private void readAudioES(Pointer pointer, int offset) {
byte[] pesLengthByte = new byte[2];
pointer.read(offset, pesLengthByte, 0, pesLengthByte.length);
offset = offset + pesLengthByte.length;
int pesLength = (pesLengthByte[0] & 0xFF) << 8 | (pesLengthByte[1] & 0xFF);
//pes数据
if (pesLength > 0) {
byte[] pts_dts_length_info = new byte[3];
pointer.read(offset, pts_dts_length_info, 0, pts_dts_length_info.length);
offset = offset + pts_dts_length_info.length;
int pesHeaderLength = (pts_dts_length_info[2] & 0xFF);
//判断是否是有pts 忽略dts
int i = (pts_dts_length_info[1] & 0xFF) >> 6;
long pts_90000 =0;
if (i == 0x02 || i == 0x03) {
byte[] pts_dts = new byte[5];
pointer.read(offset, pts_dts, 0, pts_dts.length);
//这里获取的是以90000为时间基的 需要转为 1/1000为基准的 但是pts还是不够平滑导致画面卡顿 所以不采用读取的pts
pts_90000 = ((pts_dts[0] & 0x0e) << 29) | (((pts_dts[1] << 8 | pts_dts[2]) & 0xfffe) << 14) | (((pts_dts[3] << 8 | pts_dts[4]) & 0xfffe) >> 1);
//pts = time_base + pts;
}
offset = offset + pesHeaderLength;
int audioLength = pesLength - pts_dts_length_info.length - pesHeaderLength;
byte[] bytes = G711ACodec._toPCM(pointer.getByteArray(offset, audioLength));
Memory temp = new Memory(bytes.length);
temp.write(0, bytes, 0, bytes.length);
ZLM_API.mk_media_input_pcm(mkMedia,temp.share(0),bytes.length, pts_90000);
temp.close();
}
}
/**
* 读取psh头 psm头 psm标题 及数据
*
* @param pointer
* @param offset
* @return
*/
private int readPSHAndPSMAndPSMT(Pointer pointer, int offset, int dwSize) {
//ps头起始标志
byte[] psHeaderStartCode = new byte[4];
pointer.read(offset, psHeaderStartCode, 0, psHeaderStartCode.length);
//判断是否是ps头
if ((psHeaderStartCode[0] & 0xFF) == 0x00 && (psHeaderStartCode[1] & 0xFF) == 0x00 && (psHeaderStartCode[2] & 0xFF) == 0x01 && (psHeaderStartCode[3] & 0xFF) == 0xBA) {
byte[] stuffingLengthByte = new byte[1];
offset = offset + 13;
pointer.read(offset, stuffingLengthByte, 0, stuffingLengthByte.length);
int stuffingLength = stuffingLengthByte[0] & 0x07;
offset = offset + stuffingLength + 1;
//ps头起始标志
byte[] psSystemHeaderStartCode = new byte[4];
pointer.read(offset, psSystemHeaderStartCode, 0, psSystemHeaderStartCode.length);
//PS system header 系统标题
if ((psSystemHeaderStartCode[0] & 0xFF) == 0x00 && (psSystemHeaderStartCode[1] & 0xFF) == 0x00 && (psSystemHeaderStartCode[2] & 0xFF) == 0x01 && (psSystemHeaderStartCode[3] & 0xFF) == 0xBB) {
offset = offset + psSystemHeaderStartCode.length;
byte[] psSystemLengthByte = new byte[1];
//ps系统头长度
pointer.read(offset, psSystemLengthByte, 0, psSystemLengthByte.length);
int psSystemLength = psSystemLengthByte[0] & 0xFF;
//跳过ps系统头
offset = offset + psSystemLength;
pointer.read(offset, psSystemHeaderStartCode, 0, psSystemHeaderStartCode.length);
}
//判断是否是psm系统头 则为IDR帧
if ((psSystemHeaderStartCode[0] & 0xFF) == 0x00 && (psSystemHeaderStartCode[1] & 0xFF) == 0x00 && (psSystemHeaderStartCode[2] & 0xFF) == 0x01 && (psSystemHeaderStartCode[3] & 0xFF) == 0xBC) {
offset = offset + psSystemHeaderStartCode.length;
//psm头长度可以
byte[] psmLengthByte = new byte[2];
pointer.read(offset, psmLengthByte, 0, psmLengthByte.length);
int psmLength = (psmLengthByte[0] & 0xFF) << 8 | (psmLengthByte[1] & 0xFF);
//获取音视频类型
if (videoType==0||audioType==0) {
//自定义复合流描述
byte[] detailStreamLengthByte = new byte[2];
int tempOffset = offset + psmLengthByte.length + 2;
pointer.read(tempOffset, detailStreamLengthByte, 0, detailStreamLengthByte.length);
int detailStreamLength = (detailStreamLengthByte[0] & 0xFF) << 8 | (detailStreamLengthByte[1] & 0xFF);
tempOffset = detailStreamLength + detailStreamLengthByte.length + tempOffset + 2;
byte[] videoStreamTypeByte = new byte[1];
pointer.read(tempOffset, videoStreamTypeByte, 0, videoStreamTypeByte.length);
videoType = videoStreamTypeByte[0] & 0xFF;
tempOffset = tempOffset + videoStreamTypeByte.length + 1;
byte[] videoStreamDetailLengthByte = new byte[2];
pointer.read(tempOffset, videoStreamDetailLengthByte, 0, videoStreamDetailLengthByte.length);
int videoStreamDetailLength = (videoStreamDetailLengthByte[0] & 0xFF) << 8 | (videoStreamDetailLengthByte[1] & 0xFF);
tempOffset = tempOffset + videoStreamDetailLengthByte.length + videoStreamDetailLength;
byte[] audioStreamTypeByte = new byte[1];
pointer.read(tempOffset, audioStreamTypeByte, 0, audioStreamTypeByte.length);
audioType = audioStreamTypeByte[0] & 0xFF;
}
offset = offset + psmLengthByte.length + psmLength;
}
} else {
if (dataSize > 0) {
int readSize = dwSize > dataSize ? dataSize : dwSize;
dataSize = dataSize - readSize;
byte[] temp = new byte[readSize];
pointer.read(offset, temp, 0, readSize);
buffer.write(bufferSize, temp, 0, readSize);
bufferSize = readSize + bufferSize;
offset = offset + readSize;
if (dwSize > offset) {
readData(pointer, offset, dwSize);
}
}
}
return offset;
}
/**
* 释放资源
*
* @return
*/
public void release() {
ZLM_API.mk_media_release(mkMedia);
buffer.close();
}
}
总结
实时取流参见:Java对接海康SDK并推往ZLM4J实现超低延迟播放
回放流解析ps相同,但是数据混在一起,需要进行流控,目前回放会出现轻微跳帧正在排查中,目前还有音频数据未接入。
项目全部代码以放到Gitee上 项目代码仓库