流媒体服务框架ZLM4J发布1.0.8版本
🔥🔥🔥ZLM4J 打造属于Java的流媒体生态框架,打通直播协议栈、视频监控协议栈、实时音视频协议栈,是您二开流媒体不二的选择。
🌟发布 1.0.8(已上传到中央仓库无需自己编译!)
- 开源地址:https://gitee.com/aizuda/zlm4j
- 使用文档:https://ux5phie02ut.feishu.cn/wiki/NA2ywJRY2ivALSkPfUycZFM4nUB
<dependency> <groupId>com.aizuda</groupId> <artifactId>zlm4j</artifactId> <version>1.0.8</version> </dependency>
版本 1.0.8 更新日志:
- 拉取基于2024-05-29-master分支开发
- 发布jar到中央仓库
- 增加mk_proxy_player_create3,mk_proxy_player_create4函数配置拉流代理重试次数
- 废弃
改为mk_env_init2mk_env_init1 - 更多记录请查看: 版本更新记录
实战打通海康SDK与ZLM4J实现超低延迟实时预览监控
1. 预备知识与工具
海康SDK、海康SDK对接知识、海康摄像头or海康NVR、ZLM4J、VLC播放器/flv.js播放器
2. 使用到的ZLM4J的功能
- 创建流、推送流功能
- 音频编码功能
- 拉流播放功能
- 按需转协议功能
3. 对接流程
- 初始化海康SDK及ZLM4J
- 海康SDK登录摄像头
- 开启摄像头实时预览及配置取流回调
- 创建ZLM4J对应流、并初始化音视频轨道
- 在回调的ps流中取到H264/H265裸码流及音频数据,并将音频数据解码为PCM
- 推送音视频流到ZLM4J中
- 使用VLC播放器/flv.js播放器播放并观察延迟
4. 相关代码
1-4步相关代码
public class RealPlayDemo { 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_init2(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("192.168.1.64", (short) 8000, "admin", "hk123456"); 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"); ZLM_API.mk_ini_set_option_int(mkIni, "auto_close", 1); //创建媒体 MK_MEDIA mkMedia = ZLM_API.mk_media_create2( "defaultVhost", "live", "test", 0, mkIni ); ZLM_API.mk_ini_release(mkIni); //这里分辨率、帧率、码率都可随便写 0是H264 1是h265 可以事先定义好 也可以放到回调里面判断编码类型让后再初始化这个 ZLM_API.mk_media_init_video(mkMedia, 0, 1, 1, 25.0f, 2500); ZLM_API.mk_media_init_audio(mkMedia, 2, 8000, 1, 16); ZLM_API.mk_media_init_complete(mkMedia); FRealDataCallback fRealDataCallBack = new FRealDataCallback(mkMedia, 25.0); HCNetSDK.NET_DVR_PREVIEWINFO netDvrPreviewinfo = new HCNetSDK.NET_DVR_PREVIEWINFO(); netDvrPreviewinfo.lChannel = 1; netDvrPreviewinfo.dwStreamType = 0; netDvrPreviewinfo.bBlocked = 0; netDvrPreviewinfo.dwLinkMode = 0; netDvrPreviewinfo.byProtoType = 0; //播放视频 long ret = hCNetSDK.NET_DVR_RealPlay_V40( lUserID, netDvrPreviewinfo, fRealDataCallBack, Pointer.NULL ); if (ret == -1) { System.out.println( "【海康SDK】开始sdk播放视频失败! 错误码:" + hCNetSDK.NET_DVR_GetLastError() ); return; } ZLM_API.mk_media_set_on_close( mkMedia, pointer -> { fRealDataCallBack.release(); hCNetSDK.NET_DVR_StopRealPlay(ret); System.out.println("流关闭自动释放资源"); }, Pointer.NULL ); //休眠 Thread.sleep(120000); // 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; } } }
5-6步相关代码
public class FRealDataCallback implements HCNetSDK.FRealDataCallBack_V30 { private final MK_MEDIA mkMedia; private final Memory buffer = new Memory(1024 * 1024 * 5); private int bufferSize = 0; private long pts; private double fps; private long time_base; private int videoType = 0; private int audioType = 0; public FRealDataCallback(MK_MEDIA mkMedia, double fps) { this.mkMedia = mkMedia; this.fps = fps; //ZLM以1000为时间基准 time_base = (long) (1000 / fps); //回调使用同一个线程 Native.setCallbackThreadInitializer( this, new CallbackThreadInitializer(true, false, "HikRealStream") ); } @Override public void invoke( long lRealHandle, int dwDataType, ByteByReference pBuffer, int dwBufSize, Pointer pUser ) throws IOException { //ps封装 if (dwDataType == HCNetSDK.NET_DVR_STREAMDATA) { Pointer pointer = pBuffer.getPointer(); int offset = 0; //解析psh头 psm头 psm标题 offset = readPSHAndPSMAndPSMT(pointer, offset); //读取pes数据 readPES(pointer, offset); } } /** * 读取pes及数据 * * @param pointer * @param offset */ private void readPES(Pointer pointer, int offset) { //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); } else if ((streamType >= 0xC0) & (streamType <= 0xDF)) { //音频数据 readAudioES(pointer, offset); } } } /** * 读取视频数据 * * @param pointer * @param offset */ private void readVideoES(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; 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类型 int naluType = (naluStart[4] & 0x1F); //如果是sps pps不需要变化pts if (naluType == 7 || naluType == 8) { pts = pts - time_base; } if (videoType == 0x1B) { //推送帧数据 ZLM_API.mk_media_input_h264( mkMedia, buffer.share(0), bufferSize, pts, pts ); } else if (videoType == 0x24) { //推送帧数据 ZLM_API.mk_media_input_h265( mkMedia, buffer.share(0), bufferSize, pts, pts ); } bufferSize = 0; } } int naluLength = pesLength - pts_dts_length_info.length - pesHeaderLength; byte[] temp = new byte[naluLength]; pointer.read(offset, temp, 0, naluLength); buffer.write(bufferSize, temp, 0, naluLength); bufferSize = naluLength + bufferSize; } } /** * 读取音频数据 * * @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) { //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 = 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; } } return offset; } /** * 释放资源 * * @return */ public void release() { ZLM_API.mk_media_release(mkMedia); buffer.close(); } }
5. 预览画面与延迟对比
1. 观察到对应的媒体流已经注册上去,即可使用播放器观看
2024-05-30 14:38:48.514 I [java.exe] [13388-event poller 0] MediaSource.cpp:517 emitEvent | 媒体注册:fmp4://defaultVhost/live/test 2024-05-30 14:38:48.514 I [java.exe] [13388-event poller 0] MultiMediaSourceMuxer.cpp:561 onAllTrackReady | stream: schema://defaultVhost/app/stream , codec info: H264[2688/1520/25] mpeg4-generic[8000/1/16] 2024-05-30 14:38:48.514 I [java.exe] [13388-event poller 0] MediaSource.cpp:517 emitEvent | 媒体注册:rtsp://defaultVhost/live/test 2024-05-30 14:38:48.514 I [java.exe] [13388-event poller 0] MediaSource.cpp:517 emitEvent | 媒体注册:rtmp://defaultVhost/live/test 2024-05-30 14:38:48.515 I [java.exe] [13388-event poller 0] MediaSource.cpp:517 emitEvent | 媒体注册:ts://defaultVhost/live/test 2024-05-30 14:38:52.080 I [java.exe] [13388-event poller 0] MediaSource.cpp:517 emitEvent | 媒体注册:hls://defaultVhost/live/test
2. 使用WS-FLV协议与直接使用摄像头RTSP协议播放对比
3. 使用WS-FLV协议与摄像头管理界面播放对比
4. 可以看到与摄像头RTSP协议对比画面快1-2s左右,与摄像头管理界面对比画面基本一样。
6. 总结
通过实战打通海康SDK与ZLM4J实现超低延迟实时预览监控案例,我们可以学到ZLM4J的接入流程和简单使用步骤,通过这个示例展示集成流媒体的带来的强大功能,完整项目我已上传至GITEE: https://gitee.com/daofuli/zlm4j_hk,后续将分享更多ZLM4J使用案例。
还没有评论,来说两句吧...