最近需要对接大华和海康摄像头,用SDK的方式,实现登录、OSD设置、预览、回放等功能,其他的功能都还好,实时预览和回放,里面涉及的东西太多了,对于刚接触摄像头开发相关的小伙伴来说,简直要崩溃,而且这方面的资料少之又少,我也是被这么折磨过来的,查阅各种资料、博客、包括QQ群都加了不少,最后总算是有点成果了,所以就把成果记录下来,希望这篇文章能帮助大家吧。
首先,小的功能点(登录,加载SDK文件,获取通道信息等等)就不讲了,有问题可以评论留言,主要讲讲预览和回放,其实预览和回放功能是一样的,我就讲一下预览吧,讲之前有个点需要注意,海康的视频回调流是可以直接用的,大华的需要设置一下回调流的格式,SDK中有具体的方法,可以直接用。调用播放函数以后,会有一个回调,我们来看看回调方法:
public class HuaRealPlayCallBack implements NetSDKLib.fRealDataCallBackEx {
public HuaRealPlayCallBack() {
}
private static class CallBackHolder {
private static HuaRealPlayCallBack instance = new HuaRealPlayCallBack();
}
public static HuaRealPlayCallBack getInstance() {
return HuaRealPlayCallBack.CallBackHolder.instance;
}
@Override
public void invoke(NetSDKLib.LLong lRealHandle, int dwDataType, Pointer pBuffer,
int dwBufSize, int param, Pointer dwUser) {
// 指定回调流为PS格式
if (dwDataType == 1001) {
try {
HuaSdkHandle huaSdkHandle = (HuaSdkHandle) HuaSdkMapCache.getCache(lRealHandle);
if (huaSdkHandle!=null) {
huaSdkHandle.onMediaStream(pBuffer.getByteArray(0, dwBufSize), true);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这里我用的是大华的,所以判断了接收的回调流格式,不然默认是私有流,在回调方法中,通过Map获取到当前播放的句柄,播放时put进去的,然后泛型指定为一个单独的处理类 : HuaSdkHandle,在回调中调用这个类的一个方法,就把回调的流数据传到那个方法中了,然后在那个方法中接收到流以后,要结合管道流PIpedInputStream,方便使用FFmpegGrabber拉流,因为要使用管道流作为入参进行拉流,然后在使用FFmpegRecord进行推流,关键代码如下:
FFmpegFrameGrabber grabber = null;
FFmpegFrameRecorder recorder = null;
PipedInputStream inputStream;
PipedOutputStream outputStream;
String pushAddress;
/**
* 异步接收海康/大华/宇视设备sdk回调实时视频裸流数据
*
* @param data
* @param size
*/
public void push(byte[] data, int size) {
try {
outputStream.write(data, 0, size);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
grabber = new FFmpegFrameGrabber(inputStream, 0);
grabber.setOption("rtsp_transport", "tcp");
grabber.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
grabber.setVideoCodec(avcodec.AV_CODEC_ID_H264);
grabber.setAudioStream(Integer.MIN_VALUE);
grabber.setFormat("mpeg");
long stime = System.currentTimeMillis();
// 检测回调函数书否有数据流产生,防止avformat_open_input函数阻塞
do {
Thread.sleep(100);
if (System.currentTimeMillis() - stime > 2000) {
log.info("-----SDK回调无视频流产生------");
return;
}
} while (inputStream.available() != 2048);
// 只打印错误日志
avutil.av_log_set_level(avutil.AV_LOG_QUIET);
FFmpegLogCallback.set();
grabber.start();
log.info("--------开始推送视频流---------");
recorder = new FFmpegFrameRecorder(pushAddress, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
recorder.setInterleaved(true);
// 画质参数
recorder.setVideoOption("crf", "28");
// H264编/解码器
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setVideoBitrate(grabber.getVideoBitrate());
// 封装flv格式
recorder.setFormat("flv");
// 视频帧率,最低保证25
recorder.setFrameRate(25);
// 关键帧间隔 一般与帧率相同或者是帧率的两倍
recorder.setGopSize(50);
// yuv420p
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
recorder.start();
int count = 0;
Frame frame;
while (grabber.hasVideo() && (frame = grabber.grab()) != null) {
count++;
if (count % 100 == 0) {
log.info("推送视频帧次数:{}", count);
}
if (frame.samples != null) {
log.info("检测到音频");
}
recorder.record(frame);
}
if (grabber != null) {
grabber.stop();
grabber.release();
}
if (recorder != null) {
recorder.stop();
recorder.release();
}
其中pushAddress为流媒体服务的地址,流媒体服务用的是:ZLMediaKit,可以去看看相关文档,部署很简单的,我这里用的是rtmp协议推送的,到流媒体服务上以后,会自动注册上mp4 flv m3u8等播放格式,使用流媒体服务对应格式的播放地址就可以播放了,下面来看看回放的效果:
大概思路就是这样,直播是没任何问题的,当然这种方式很占用服务的网络和带宽,需要根据具体场景做优化,同时播放多少路,或者自动推流的机制等等,也踩了不少坑,大家多研究研究就会实现的。