上一篇文章介绍了通用协议onvif获取到rtsp地址Java onvif协议通用协议获取rtsp地址
当然也有很多其他的方式获取rtsp地址
首先还是引入包:
org.bytedeco
javacv-platform
1.5.4
这里我是使用的rtsp砖udp (h264)的方式推流,还有其他方式例如rtmp 或者rtp 实现方式差不多只用修改一些参数
需要用到的测试软件:VLC
这里是利用转封装的方式进行转码(由于rtsp本身就支持h264编码格式,有两种方式:1.转码2.转封装(转封装消耗的资源更少))
详细解释就不多说了,注释里面有详细说明:
/**
* rtsp转 udp(转封装方式)
* @author zf
*/
public class RecordVideo {
private FFmpegFrameGrabber grabber = null;
private FFmpegFrameRecorder recorder = null;
// 视频参数
/**
* 选择视频源
* @param src
* @author eguid
* @throws Exception
*/
public RecordVideo sourcesRtsp(String src) throws Exception {
// 采集/抓取器
grabber = MediaUtils.createGrabber(src);
grabber.start();// 开始之后ffmpeg会采集视频信息,之后就可以获取音视频信息
return this;
}
/**
* 选择输出
* @param out
* @author eguid
* @throws IOException
*/
public RecordVideo target(String out) throws IOException {
// 流媒体输出地址,分辨率(长,高),是否录制音频(0:不录制/1:录制) ?overrun_nonfatal=1&fifo_size=50000000
//这里udp地址增加参数扩大udp缓存
recorder = new FFmpegFrameRecorder(out + "?overrun_nonfatal=1&fifo_size=50000000", MediaUtils.FRAME_WIDTH, MediaUtils.FRAME_HEIGHT, 0);
// 直播流格式
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
// 降低编码延时
recorder.setVideoOption("tune", "zerolatency");
recorder.setMaxDelay(500);
recorder.setGopSize(10);
// 提升编码速度
recorder.setVideoOption("preset", "ultrafast");
// 录制的视频格式 flv(rtmp格式) h264(udp格式) mpegts(未压缩的udp) rawvideo
recorder.setFormat("h264");
// 帧数
double frameLength = grabber.getLengthInFrames();
long frameTime = grabber.getLengthInTime();
double v = frameLength * 1000 * 1000 / frameTime;
recorder.setFrameRate(v);
//百度翻译的比特率,默认400000
recorder.setVideoBitrate(200000);
// recorder.setAudioOption("crf", "23");
// 建议从grabber获取AudioChannels
// recorder.setAudioChannels(grabber.getAudioChannels());
// recorder.setInterleaved(true);
// yuv420p
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
recorder.start(grabber.getFormatContext());
return this;
}
/**
* 转封装
* @author eguid
* @throws IOException
*/
public RecordVideo go() throws IOException {
System.out.println("开始推送...");
long err_index = 0;//采集或推流导致的错误次数
// 释放探测时缓存下来的数据帧,避免pts初始值不为0导致画面延时
grabber.flush();
//错误采集判断
for(int no_frame_index = 0; no_frame_index < 10 || err_index > 1;) {
AVPacket pkt;
try {
pkt = grabber.grabPacket();
if(pkt == null || pkt.size() <= 0 || pkt.data() == null) {
//空包记录次数跳过
no_frame_index ++;
continue;
}
//不需要编码频帧推出去
err_index += (recorder.recordPacket(pkt) ? 0 : 1);//如果失败err_index自增1
av_packet_unref(pkt);
} catch (IOException e) {//推流失败
err_index++;
}
}
return this;
}
public static void main(String[] args) throws Exception, IOException {
//运行,设置视频源和推流地址
new RecordVideo().sourcesRtsp("rtsp://{username}:{password}@{ip}:{port}/Streaming/Unicast/channels/1602")
.target("udp://{ip}:{port}")
.go();
}
}
package com.onvif.java.utils;
import com.onvif.java.common.RrException;
import com.onvif.java.model.OnvifCredentials;
import com.onvif.java.service.OnvifDevice;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.FrameGrabber;
import org.onvif.ver10.schema.GetRecordingsResponseItem;
import org.onvif.ver10.schema.Profile;
import org.onvif.ver10.schema.TransportProtocol;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import javax.xml.soap.SOAPException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executor;
import static org.bytedeco.ffmpeg.global.avcodec.av_packet_unref;
/**
* @program: javaOnvif
* @description: 获取rtsp地址
* @author: zf
* @create: 2020-09-08 10:50
**/
@Component
public class MediaUtils {
@Autowired
ThreadPoolTaskExecutor taskExecutor;
/**
* 视频帧率
*/
public static final int FRAME_RATE = 25;
/**
* 视频宽度
*/
public static final int FRAME_WIDTH = 480;
/**
* 视频高度
*/
public static final int FRAME_HEIGHT = 270;
/**
* 流编码格式
*/
public static final int VIDEO_CODEC = avcodec.AV_CODEC_ID_H264;
/**
* 编码延时 zerolatency(零延迟)
*/
public static final String TUNE = "zerolatency";
/**
* 编码速度 ultrafast(极快)
*/
public static final String PRESET = "ultrafast";
/**
* 录制的视频格式 flv(rtmp格式) h264(udp格式) mpegts(未压缩的udp) rawvideo
*/
public static final String FORMAT = "h264";
/**
* 比特率
*/
public static final int VIDEO_BITRATE = 200000;
private static FFmpegFrameGrabber grabber = null;
private static FFmpegFrameRecorder recorder = null;
/**
* 构造视频抓取器
* @param rtsp 拉流地址
* @return
*/
public static FFmpegFrameGrabber createGrabber(String rtsp) {
// 获取视频源
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(rtsp);
grabber.setOption("rtsp_transport","tcp");
//设置帧率
grabber.setFrameRate(FRAME_RATE);
//设置获取的视频宽度
grabber.setImageWidth(FRAME_WIDTH);
//设置获取的视频高度
grabber.setImageHeight(FRAME_HEIGHT);
//设置视频bit率
grabber.setVideoBitrate(2000000);
return grabber;
}
/**
* 选择视频源
* @param src
* @author eguid
* @throws FrameGrabber.Exception
*/
public MediaUtils from(String src) throws FrameGrabber.Exception {
start = System.currentTimeMillis();
// 采集/抓取器
grabber = createGrabber(src);
// 开始之后ffmpeg会采集视频信息
grabber.start();
grabber.flush();
form = src.substring(src.indexOf("@") + 1);
return this;
}
}
udp的地址是udp/h264://@{ip}:{port}
这里格式说明下:
// 录制的视频格式 flv(rtmp格式) h264(udp格式) mpegts(未压缩的udp) rawvideo
recorder.setFormat("h264");
udp 设置的是h264 其他的根据注释选择就可以输出不同的协议,上门代码中注释部分是音频相关的如果有需要可以使用
下面是对比的海康摄像头提供的原始sdk获取和我们推流转码后的时间对比 延时在1-2秒范围内,推流稳定
再提醒下,详细参数可以根据实际情况调整,根据所需调整 例如清晰度
setVideoBitrate设置比特率 设置画面连续性setFrameRate等
排坑指南:
视频的长宽只能设置4的倍数,不然会强制使用默认
比特率的设置则需要视频源的清晰度压缩,有一个极限值越过值了再小也没用了
更新日志:
2020年11月27日
目前的所有方式中,在拉取流的时候比较耗时大约需要2到3秒的加载
在项目启动的时候提前加载一次会有比较好的效果,但是garber.start依然会消耗一定时间,根据询问大佬这个应该目前java上没有一个好的解决方案,使用c效果阔能比较好
//提前加载资源,解决第一次推流慢
FFmpegFrameGrabber.tryLoad();
FFmpegFrameRecorder.tryLoad();
推流方式二
直接推送图片方式(更低延迟,验证控制在1秒以内,当然也会提高一定的cpu占用量,目前i5-4590 最多推送7到8路视频流就会占用cpu80%)
ps 如果追求超低延迟可以参考
这种方式是直接抓取流帧 转换成图片直接websocket推送出去
/**
* 推送图片流
* @throws Exception
*/
public MediaUtils startPush(String ip, Integer port) throws Exception {
Long end = System.currentTimeMillis();
System.out.println(form + " 开始推送 耗时:" + (end - start) + "ms");
Java2DFrameConverter java2DFrameConverter = new Java2DFrameConverter();
try {
Frame frame;
while ((frame = grabber.grabImage()) != null) {
//线程控制中断
if (Thread.currentThread().isInterrupted()) {
System.out.println(form + " 停止推送...");
return this;
}
BufferedImage bufferedImage = java2DFrameConverter.getBufferedImage(frame);
byte[] bytes = imageToBytes(bufferedImage, "jpg");
//使用udp发送图片数据
udpService.sendMessageBytes(bytes, ip, port);
//使用websocket发送数据
// MyWebSocket.sendAll(channel, bytes);
}
} catch (Exception e){
Thread.currentThread().interrupt();
}finally {
if (grabber != null) {
grabber.stop();
}
}
return this;
}
调用方式是首先调用上面的
new MediaUtils().from(from).startPush(ip, port);
然后直接websocket推送到前端,前端只能不断替换图片即可,目前测试海康,大华和宇视延迟都在1秒以内
2021年2月7日
测试vlc 软件:
链接:百度网盘 请输入提取码
提取码:z7ox
udpSever(Netty实现的udp服务端):
package com.onvif.java.service;
import com.alibaba.fastjson.JSONObject;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.MessageToMessageEncoder;
import io.netty.util.CharsetUtil;
import lombok.Data;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* UDP
*
* @author zf
* @since 2019/8/16
*/
@Service
public class UdpService {
private static final Logger LOG = LoggerFactory.getLogger(UdpService.class);
private Channel channel;
private NioEventLoopGroup group;
@Data
public static class Net{
private String type;
private Integer tcpPort;
private String udpPort;
private String webPort;
private String ip;
}
public UdpEntity start(String ip, Integer port) {
group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_RCVBUF, 128 * 1024 * 1024)
.option(ChannelOption.SO_SNDBUF, 128 * 1024 * 1024)
//增加发送长度
.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(128 * 1024))
.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioDatagramChannel socketChannel) {
//解决粘包和半包问题 接收数据全部要以$next$分割
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(128 * 1024,
Unpooled.wrappedBuffer("$next$".getBytes())));
socketChannel.pipeline().addLast(outboundHandler());
socketChannel.pipeline().addLast(inboundHandler());
}
});
System.out.println("###### Udp ######启动 端口:" + port);
try {
channel = bootstrap.bind(ip, port).sync().channel();
} catch (InterruptedException e) {
e.printStackTrace();
}
return new UdpEntity().setChannel(channel)
.setGroup(group);
}
@Data
@Accessors(chain = true)
public static class UdpEntity {
private Channel channel;
private NioEventLoopGroup group;
}
public static class MsgEvent {
private final InetSocketAddress inetSocketAddress;
public InetSocketAddress getInetSocketAddress() {
return inetSocketAddress;
}
public String getMsg() {
return msg;
}
private final String msg;
public MsgEvent(InetSocketAddress inetSocketAddress, String msg) {
this.inetSocketAddress = inetSocketAddress;
this.msg = msg;
}
}
public void sendMessageBytes(byte[] data, String ip, Integer port) {
if (channel != null) {
try {
channel.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer(data),
new InetSocketAddress(ip,port))).sync();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// e.printStackTrace();
}
}
}
/**
* 关闭
*/
public void shutdown(){
group.shutdownGracefully();
}
/**
* 出参数据
*
* @return
*/
private MessageToMessageEncoder outboundHandler() {
return new MessageToMessageEncoder() {
@Override
protected void encode(ChannelHandlerContext ctx, MsgEvent msgEvent, List
2020-12-21
目前发现windows server 2012R2 无法启动,经过询问javacv作者得到回复
在新版本中,目前我测过最新版本1.5.4 是无法启动,会出现无法找到acode,Could not initialize class org.bytedeco.ffmpeg.global.avutil等各种错误
作者解释是在新版本中新功能使用到了Media Foundation 的功能 且在初始化garber时会加载,若操作系统环境没有该功能则报错
解决方案1:使用1.5.1版本无问题
解决方案2:在没有Media Foundation 的系统中安装该功能,再使用最新版本