目前摄像头直播的方案主要有以下几种方式:
-
rtsp方式接入,只能实现视频预览
-
国标协议接入,实现比较复杂,需要多实现SIP服务器
-
通过netsdk获取到视频码流,推流到流媒体服务器,通过wsflv,flv,hls等流媒体协议播放,H265不支持
一、采用方案
对比后最终采用了第三种方式,java使用jna的方式接入大华netsdk,获取到dav视频码流后去除大华头尾,拿到H264裸码流,通过javacv(对ffmpeg、opencv等库的封装)推送到流媒体服务器(选用了一个开源的node写的nms)
二、JNA
2.1 JNI java调用c的过程
若已有编译好的.dll/.so文件—>需先用是C语言另外写一个.dll/.so共享库,使用SUN规定的数据结构代替C语言的数据结构,调用已有的dll/so中公布的函数—>java中载入这个库—>java编写Native函数作为链接库中函数的代理
2.2 JNA
JNA是建立在JNI技术基础之上的一个java类库,它提供了一个动态的C语言编写的转发器,可以自动实现java和C的数据类型映射,开发者只需要使用java接口描述目标本地库的功能和结构,不需要再编写C动态链接库,可方便使用java直接访问动态链接库中的函数。此外JNA包括一个已与许多本地函数映射的平台库,以及一组简化本地访问的共用接口。JNA性能上有些微损失,无法实现c调用java。
2.3 windows下简单使用
(1))编译一个dll库,新建cal.cpp、cal.h,使用vs编译出dll文件(linux下编译so文件)
cal.h
extern "C" __declspec(dllexport) int cal(int a, int b, int calType);
cal.cpp
#include "cal.h"
int cal(int a,int b,int calType) {
if (calType == 1) {
return a + b;
}else{
return a - b;
}
}
(2)新建java项目,maven引入jna
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>4.5.1</version>
</dependency>
将上面编译出的cal.dll放到项目根路径,新建java接口描述dll库的功能和结构
import com.sun.jna.Library;
import com.sun.jna.Native;
public interface CalSDK extends Library {
//实例由JNA通过反射自动生成,可以拓展为根据不同的系统加载不同的动态链接库
CalSDK CalSDK_INSTANCE = (CalSDK)Native.loadLibrary("cal", CalSDK.class);
//此方法为链接库中的方法
int cal(int a, int b, int calType);
}
调用
public class Main {
public static void main(String[] args) {
System.out.println(CalSDK.CalSDK_INSTANCE.cal(1,2,1));
}
}
2.4 类型映射
jna官方提供的
2.5 函数回调
以大华netsdk设备实时预览回调为例,c代码
// 实时监视数据回调函数原形
typedef void (CALLBACK *fRealDataCallBackEx)(
LLONG lRealHandle,
DWORD dwDataType,
BYTE *pBuffer,
DWORD dwBufSize,
LONG param,
LDWORD dwUser);
java代码,可以在实现类的invoke方法里获取到视频流
public interface fRealDataCallBackEx extends StdCallCallback {
public void invoke(
LLong lRealHandle,
int dwDataType,
Pointer pBuffer,
int dwBufSize,
int param,
Pointer dwUser);
}
开启播放后掉接口设置回调
public void startRealplay(LLong loginHandle,Integer playSign,CameraPojo cameraPojo){
//默认为主码流
int streamType = 0;
//开启预览
LLong lRealHandle= NetSDKConfig.netSdk.CLIENT_RealPlayEx(
loginHandle,
cameraPojo.getChannel(),
null,
streamType);
//设置视频流回调
if(lRealHandle.longValue()!=0){
try {
NetSDKConfig.netSdk.CLIENT_SetRealDataCallBackEx(
lRealHandle,
RealDataCallBack.getInstance(),
null,
31)
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
2.6 指针
c中void * 指针,指向一个地址, 对应jna中的Pointer ,下面主动注册回调代码中就是将指针中的数据取出的一种方法
public int invoke(NetSDKLib.LLong lHandle, final String pIp, final int wPort,
int lCommand, Pointer pParam, int dwParamLen,
Pointer dwUserData) {
// 将 pParam 转化为序列号
byte[] buffer = new byte[dwParamLen];
pParam.read(0, buffer, 0, dwParamLen);
String serNum = "";
try {
serNum = new String(buffer, "GBK").trim();
} catch (UnsupportedEncodingException e) {
log.error(e.getMessage());
}
}
2.7 结构体对应java类
类中字段顺序,需要与c++中对齐,不然报NoSuchFieldError错误!
c
// 定时录像配置信息
typedef struct tagCFG_RECORD_INFO
{
int nChannelID; // 通道号(0开始)
CFG_TIME_SECTION stuTimeSection[WEEK_DAY_NUM][MAX_REC_TSECT]; // 时间表
int nPreRecTime; // 预录时间,为零时表示关闭(0~300)
BOOL bRedundancyEn; // 录像冗余开关
int nStreamType; // 0-主码流,1-辅码流1,2-辅码流2,3-辅码流3
int nProtocolVer; // 协议版本号, 只读
// 能力
BOOL abHolidaySchedule; // 为true时有假日配置信息,bHolidayEn、stuHolTimeSection才有效;
BOOL bHolidayEn; // 假日录像使能TRUE:使能,FALSE:未使能
CFG_TIME_SECTION stuHolTimeSection[MAX_REC_TSECT]; // 假日录像时间表
} CFG_RECORD_INFO;
java
// 定时录像配置信息
public static class CFG_RECORD_INFO extends Structure
{
public int nChannelID; // 通道号(0开始)
public TIME_SECTION_WEEK_DAY_6[] stuTimeSection = (TIME_SECTION_WEEK_DAY_6[])new TIME_SECTION_WEEK_DAY_6().toArray(WEEK_DAY_NUM); // 时间表
public int nPreRecTime; // 预录时间,为零时表示关闭(0~300)
public int bRedundancyEn; // 录像冗余开关
public int nStreamType; // 0-主码流,1-辅码流1,2-辅码流2,3-辅码流3
public int nProtocolVer; // 协议版本号, 只读
public int abHolidaySchedule; // 为true时有假日配置信息,bHolidayEn、stuHolTimeSection才有效;
public int bHolidayEn; // 假日录像使能TRUE:使能,FALSE:未使能
public TIME_SECTION_WEEK_DAY_6 stuHolTimeSection; // 假日录像时间表
}
//将c的指针转为jna结构体
public static void GetPointerDataToStruct(Pointer pNativeData, long OffsetOfpNativeData, Structure pJavaStu) {
pJavaStu.write();
Pointer pJavaMem = pJavaStu.getPointer();
pJavaMem.write(0, pNativeData.getByteArray(OffsetOfpNativeData, pJavaStu.size()), 0,
pJavaStu.size());
pJavaStu.read();
}
三、JAVACV
JavaCV 提供了在计算机视觉领域的封装库,包括:OpenCV、ARToolKitPlus、libdc1394 2.x 、PGR FlyCapture和FFmpeg。可以对音视频图片等进行处理。
3.1 javacv引入
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.2.2-1.5.3</version>
</dependency>
//可以配成根据不同的profile引入不同环境下的依赖
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.2.2-1.5.3</version>
<classifier>windows-x86_64</classifier>
//linux环境下使用
<!--<classifier>linux-x86_64</classifier>-->
</dependency>
3.2 使用
上面通过netsdk拿到的大华视频流经过去大华封装,拿到H264裸码流,判断为关键帧后,开始通过Java的管道流,将H264裸码流写入PipedOutputStream中,然后将对应的PipedInputStream当做参数传入到javacv的FFmpegFrameGrabber的构造方法中,通过JAVACV中的FFMPEG推到流媒体服务器
主要使用两个类FFmpegFrameGrabber(帧抓取器),FFmpegFrameRecorder(帧录制器/推流器)
/**
* @return RtmpPush
* @Title: from
* @Description:分析视频流
**/
public RtmpPush from(){
try {
grabber = new FFmpegFrameGrabber(inputStream, 0);
grabber.setVideoOption("vcodec", "copy");
grabber.setFormat("h264");
grabber.setOption("rtsp_transport", "tcp");
grabber.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
grabber.setVideoCodec(avcodec.AV_CODEC_ID_HEVC);
// 用于检测大华sdk回调函数是否有数据流产生,从而避免没有数据流导致avformat_open_input()函数阻塞
long stime = new Date().getTime();
while (true) {
Thread.sleep(100);
if (new Date().getTime() - stime > 20*1000) {
log.error("[id:" + pojo.getSerNum() + " ch:" + pojo.getChannel() + "] 无视频流数据超过20s");
release();
return null;
}
if (inputStream.available() == 1024) {
log.error("[id:" + pojo.getSerNum() + " ch:"+ pojo.getChannel() +"]inputStream is 1024");
break;
}
}
// grabber.setOption("stimeout", "20000");
//此处在设备信号不好的时候会阻塞,暂时修改源码去掉org.bytedeco.ffmpeg.global.avcodec.class的synchronized
grabber.start();
isGrabberStartOver=true;
log.error("[id:" + pojo.getSerNum() + " ch:" + pojo.getChannel() +"]Grabber strat over!");
// 一般来说摄像头的帧率是25
if (grabber.getFrameRate() > 0 && grabber.getFrameRate() < 100) {
framerate = grabber.getFrameRate();
} else {
framerate = 25.0;
}
} catch (Exception e) {
log.error(e.getMessage());
release();
return null;
}
return this;
}
/**
* @return void
* @Title: push
* @Description:推送视频
**/
public void push() {
try {
//封装为flv格式,通过rtmp协议推送到流媒体服务器
this.recorder = new FFmpegFrameRecorder(pojo.getRtmp(), grabber.getImageWidth(), grabber.getImageHeight(),0);
this.recorder.setInterleaved(true);
this.recorder.setVideoOptions(this.videoOption);
// 设置比特率
this.recorder.setVideoBitrate(bitrate);
// h264编/解码器
this.recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
// 封装flv格式
this.recorder.setFormat("flv");
this.recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
// 视频帧率(保证视频质量的情况下最低25,低于25会出现闪屏)
this.recorder.setFrameRate(framerate);
// 关键帧间隔,一般与帧率相同或者是视频帧率的两倍
this.recorder.setGopSize((int) framerate * 2);
AVFormatContext fc = null;
fc = grabber.getFormatContext();
this.recorder.start(fc);
AVPacket pkt = null;
long dts = 0;
long pts = 0;
//空包时长大于30s退出
Long startTs = CurrentTimeMillisClock.getInstance().now();
Long nullTs;
for (int no_frame_index = 0; err_index < 5 && !isExit; ) {
if (exitcode == 1) {
break;
}
pkt = grabber.grabPacket();
if (pkt == null || pkt.size() <= 0 || pkt.data() == null) {
if(no_frame_index==0){
log.error("[id:" + pojo.getSerNum() + " ch:" + pojo.getChannel() +"]pkt is null,continue...");
}
nullTs = CurrentTimeMillisClock.getInstance().now();
//空包记录次数跳过
no_frame_index++;
exitcode = 2;
if(nullTs-startTs>30*1000){
break;
}
Thread.sleep(100);
continue;
}
startTs = CurrentTimeMillisClock.getInstance().now();
if(no_frame_index!=0){
log.error("pkt is not null");
}
no_frame_index=0;
// 过滤音频
if (pkt.stream_index() == 1) {
continue;
} else {
// 矫正sdk回调数据的dts,pts每次不从0开始累加所导致的播放器无法续播问题
pkt.pts(pts);
pkt.dts(dts);
err_index += (recorder.recordPacket(pkt) ? 0 : 1);
}
// pts,dts累加
timebase = grabber.getFormatContext().streams(pkt.stream_index()).time_base().den();
pts += timebase / (int) framerate;
dts += timebase / (int) framerate;
// 将缓存空间的引用计数-1,并将Packet中的其他字段设为初始值。如果引用计数为0,自动的释放缓存空间。
av_packet_unref(pkt);
}
} catch (Exception e) {
log.error(e.getMessage());
} finally {
release();
}
}
四、实现效果
配制好摄像头,部署好nms之后,运行项目,等待摄像头主动注册上来 swagger调用实现的开启视频流的接口,将主码流的视频推送到nms 将推送成功后的flv地址复制,在vlc播放网络串流查看 同时也可以在其他流媒体播放器上播放,页面可以使用b站的flv.js等播放。可以在nms的管理页面也可以看到这一视频流。