文章目录
1. 前言2. 对接过程 以海康SDK取流推流为例1. 引入JavaCv Maven依赖,按需引入2. 流处理类3. 注册海康SDK取流回调函数4. 取流回调函数 3. 小结
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 **/@Slf4jpublic class StreamPushHandle { private FFmpegFrameGrabber grabber = null; private FFmpegFrameRecorder recorder = null; private PipedOutputStream outputStream; private PipedInputStream inputStream; private String pushAddress; private SdkStreamClose streamClose; private Long handleId; private AVPacket avPacket = null; private Frame frame = null; private boolean isPlayBack=false; private double frameRate = 25.0; public StreamPushHandle(String pushAddress, SdkStreamClose streamClose, Boolean isPlayBack) { this.pushAddress = pushAddress; this.streamClose = streamClose; this.outputStream = new PipedOutputStream(); this.inputStream = new PipedInputStream(256* 1024); this.isPlayBack = isPlayBack; 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(); // 部分监控设备流信息里携带的帧率为9000,如出现此问题,会导致dts、pts时间戳计算失败,播放器无法播放,故出现错误的帧率时,默认为25帧 if (grabber.getFrameRate() > 0 && grabber.getFrameRate() < 100) { frameRate = grabber.getFrameRate(); } 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()); int videoIndex=0; AVFormatContext context = grabber.getFormatContext(); for (int i = 0; i < context.nb_streams(); i++) { if (context.streams(i).codecpar().codec_type()==avutil.AVMEDIA_TYPE_VIDEO){ videoIndex=i; } } //需要等待的时间 long waitTime = (long) (1000 / frameRate); //h264只需要转封装 if (grabber.getVideoCodec() == avcodec.AV_CODEC_ID_H264) { recorder.start(context); //初次执行时间 long exStartTime = System.currentTimeMillis(); while ((avPacket = grabber.grabPacket()) != null) { recorder.recordPacket(avPacket); //如果是回放则使用视频来控制流速度 if (isPlayBack && avPacket.stream_index() == videoIndex) { long exEndTime = System.currentTimeMillis(); long diffTime = exEndTime - exStartTime; //计算需要等待的时间 if (diffTime < waitTime) { Thread.sleep(waitTime - diffTime); } exStartTime = System.currentTimeMillis(); } } } 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(); //初次执行时间 long exStartTime = System.currentTimeMillis(); while ((frame = grabber.grab()) != null) { recorder.record(frame); //如果是回放则使用视频来控制流速度 if (isPlayBack && frame.streamIndex == videoIndex) { long exEndTime = System.currentTimeMillis(); long diffTime = exEndTime - exStartTime; //计算需要等待的时间 if (diffTime < waitTime) { Thread.sleep(waitTime - diffTime); } exStartTime = System.currentTimeMillis(); } } } } 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,fasle); //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 一起研究。