@[toc]
1. 前言
支持H265转H264编码
本文主要介绍海康、大华SDK取流推流过程,这里就不展示对接海康、大华SDK了
这个是重点 Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "HikRealStream-" + RandomUtil.randomNumbers(8)));
2. 对接过程 以海康SDK取流推流为例
1. 引入JavaCv Maven依赖,按需引入
<!--JavaCV相关依赖-->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.9</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>6.0-1.5.9</version>
<classifier>windows-x86_64-gpl</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>6.0-1.5.9</version>
<classifier>linux-x86_64-gpl</classifier>
</dependency>
2. 流处理类
/**
* 推流处理
*
* @author lidaofu
* @since 2023/11/10
**/
@Slf4j
public class StreamPushHandle {
private FFmpegFrameGrabber grabber = null;
private FFmpegFrameRecorder recorder = null;
private PipedOutputStream outputStream;
private PipedInputStream inputStream;
private String pushAddress;
private SdkStreamClose streamClose;
private LinkedBlockingDeque<byte[]> frameDataQueue;
private Long handleId;
private AVPacket avPacket = null;
public StreamPushHandle(String pushAddress, SdkStreamClose streamClose) {
this.pushAddress = pushAddress;
this.streamClose = streamClose;
this.outputStream = new PipedOutputStream();
this.inputStream = new PipedInputStream(256* 1024);
try {
//建立管道连接
inputStream.connect(outputStream);
} catch (IOException e) {
throw new FFmpegException("创建输入管道失败");
}
}
/**
* 设置播放句柄
*
* @param handleId
*/
public void setHandleId(Long handleId) {
this.handleId = handleId;
}
/**
* 异步接收海康/大华/宇视设备sdk回调实时视频裸流数据
*/
public void write(byte[] data,int dwBufSize) {
try {
outputStream.write(data, 0, dwBufSize);
} catch (IOException e) {
throw new FFmpegException("写入数据失败", e);
}
}
/**
* 推流
*/
public void push() {
try {
FFmpegLogCallback.setLevel(avutil.AV_LOG_ERROR);
grabber = new FFmpegFrameGrabber(inputStream, 0);
//有些码率什么可以自己设置、不过没有必要
grabber.setVideoCodec(avcodec.AV_CODEC_ID_H264);
// 设置读取的最大数据,单位字节 为了加快首播速度
grabber.setOption("probesize", "8192");
// 设置分析的最长时间,单位微秒 为了加快首播速度
grabber.setOption("analyzeduration", "1000000");
// 5秒超时 单位微秒
grabber.setOption("stimeout", "5000000");
// 5秒超时 单位微秒
grabber.setOption("rw_timeout", "5000000");
// 设置缓存大小,提高画质、减少卡顿花屏
grabber.setOption("buffer_size", "1024000");
grabber.start();
recorder = new FFmpegFrameRecorder(pushAddress, grabber.getImageWidth(), grabber.getImageHeight());
recorder.setFormat("flv");
recorder.setInterleaved(true);
recorder.setVideoOption("preset", "ultrafast");
recorder.setVideoOption("tune", "zerolatency");
recorder.setVideoOption("crf", "25");
recorder.setSampleRate(grabber.getSampleRate());
recorder.setFrameRate(grabber.getFrameRate());
recorder.setVideoBitrate(grabber.getVideoBitrate());
//h264只需要转封装
if (grabber.getVideoCodec() == avcodec.AV_CODEC_ID_H264) {
recorder.start(grabber.getFormatContext());
while ((avPacket = grabber.grabPacket()) != null) {
recorder.recordPacket(avPacket);
}
} else {
if (grabber.getAudioChannels() > 0) {
recorder.setAudioChannels(grabber.getAudioChannels());
recorder.setAudioBitrate(grabber.getAudioBitrate());
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
}
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
//使用libx264加速解码 注意需要使用gpl版的 不然还是默认是思科的h264编解码器
recorder.setVideoCodecName("libx264");
recorder.start();
while ((frame = grabber.grab()) != null) {
recorder.record(frame);
}
}
} catch (Exception e) {
log.warn("【FFMPEG】推送SDK流失败 推流地址:{}", pushAddress);
} finally {
//回调SDK关流
streamClose.closeStream(handleId);
try {
if (recorder != null) {
recorder.close();
}
if (grabber != null) {
grabber.close();
}
} catch (Exception e) {
throw new FFmpegException("关闭取流器失败");
}
}
}
}
3. 注册海康SDK取流回调函数
//流处理器 HkSdkRequest::stopSdkPlay是关流回调函数的实现
StreamPushHandle streamPushHandle = new StreamPushHandle(pushUrl, HkSdkRequest::stopSdkPlay);
//SDK实时取流回调
FRealDataCallBack fRealDataCallBack = new FRealDataCallBack(streamPushHandle);
HCNetSDK.NET_DVR_PREVIEWINFO netDvrPreviewinfo = new HCNetSDK.NET_DVR_PREVIEWINFO();
netDvrPreviewinfo.lChannel = channelId;
netDvrPreviewinfo.dwStreamType = isMain ? 0 : 1;
netDvrPreviewinfo.bBlocked = 0;
netDvrPreviewinfo.dwLinkMode = 0;
netDvrPreviewinfo.byProtoType = 0;
//开启实时预览及设置回调函数
long ret = HkSdkClientContext.HCNETSDK.NET_DVR_RealPlay_V40(userId, netDvrPreviewinfo, fRealDataCallBack, null);
if (ret == -1) {
log.error("【海康SDK】通过sdk播放视频失败! 错误码:{}", HkSdkClientContext.HCNETSDK.NET_DVR_GetLastError());
return false;
}
//设置播放句柄 为了回调关流用的,懂得都懂
streamPushHandle.setHandleId(ret);
//我这里需要同时播放多台设备则需要添加回调函数到map中增加索引防止回调函数被gc回收 如果不需要同时预览可以使用全局final staic回调函数
REAL_PLAY_STREAM_MAP.put(ret, fRealDataCallBack);
4. 取流回调函数
public class FRealDataCallBack implements HCNetSDK.FRealDataCallBack_V30 {
private StreamPushHandle streamPushHandle;
private Boolean start = false;
public FRealDataCallBack(StreamPushHandle streamPushHandle) {
//这里是关键,默认回调是一个数据包产生一个线程,因为管道的机制,第一次写入和读取的线程会和管道绑定,如果线程G了管道也会关闭会出现 Write end dead \ Pipe closed错误,所以设置一个线程回调解决这个错误问题,如果不想设置这里可以用队列来解决,这里不详细阐述
Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "HikRealStream-" + RandomUtil.randomNumbers(8)));
this.streamPushHandle = streamPushHandle;
}
/**
* 这个回调方法会使用海康sdk申请的线程来调用 同一个设备也是由不同的线程来提调用
*/
public void invoke(long lRealHandle, int dwDataType, ByteByReference pBuffer, int dwBufSize, Pointer pUser) {
//混合码流
if (dwDataType == HCNetSDK.NET_DVR_STREAMDATA) {
streamPushHandle.write(pBuffer.getByteArray(0L, dwBufSize), dwBufSize);
if (!start) {
start = true;
//这里用的线程池 保证管道读取和写入分别用的是一个独立的线程
FfmpegThreadPool.execute(streamPushHandle::push);
}
}
}
}
/**
* sdk关流回调接口
*
* @author lidaofu
* @since 2023/11/13
**/
public interface SdkStreamClose {
/**
* 当推流停止或者发生异常会调用此接口 此接口再去调用海康、大华的sdk关流接口
* @param handle
*/
void closeStream(Long handle);
}
3. 小结
上面就是全部对接流程,可能会有问题,但不多,有问题可以wx联系我:L746101210 一起研究。