标签: spring-boot 视频直播 rtsp javacv
最近项目需要实时直播和回放,集成海康威视摄像头:(适合少量用户,或者内部系统使用)
-
<!-- 视频处理库 -->
-
<dependency>
-
<groupId>org.bytedeco</groupId>
-
<artifactId>javacv-platform</artifactId>
-
<version>1.5.1</version>
-
</dependency>
这里是利用javacv抓取rtsp地址的视频流,通过转换成图片使用websocket实时推送
首先了解rtsp地址,这里举例就用海康威视的规则
-
通道号下面详细介绍
-
# 单播
-
rtsp://{用户名}:{密码}@{IP地址}:{端口号}/Streaming/Channels/{通道号}?transportmode=unicast
-
# 多播
-
rtsp://{用户名}:{密码}@{IP地址}:{端口号}/Streaming/Channels/{通道号}?transportmode=multicast
-
# 分时获取
-
rtsp://{用户名}:{密码}@{IP地址}:{端口号}/Streaming/tracks/{通道号}?starttime=20191008t063812z&endtime=20191008t064816z
-
上面地址获取不到时可以试试下面的其他版本地址:
-
rtsp://{用户名}:{密码}@{IP地址}:{端口号}/Streaming/Unicast/channels/{通道号}
-
rtsp://{用户名}:{密码}@{IP地址}:{端口号}/h264/{通道号}/main/av_stream
这里通道号分类是IP通道号和模拟通道号,如果是12年以前的老机器,地址建议使用最后一个获取实时视频
可以使用官方提供的java demo 来查看,测试是否能获取视频
这里有打包好的jar包和环境可以直接运行jar包使用,解压后将里面内容放置在System32下,cmd执行jar
地址 https://pan.baidu.com/s/1QxELvKLmgqjeKu6NKUGP0A 密码 67v1
下图就是IP通道号 若只是Camera20 则为模拟通道号
通道号可以参考这里https://www.cnblogs.com/elesos/p/9881690.html
接下来就是java代码:
首先yml配置动态RTSP地址
-
myconfig:
-
rtsp: rtsp://%s:%s@%s:%s/Streaming/Channels/%s01
-
replay-rtsp: rtsp://%s:%s@%s:%s/Streaming/tracks/%s01?starttime=%s&endtime=%s
媒体工具类:
-
@Slf4j
-
@Component
-
public class MediaUtils {
-
/**
-
* 直播摄像机id集合,避免重复拉流
-
*/
-
private static Set<Long> liveSet = new ConcurrentSet<>();
-
/**
-
* 用于构造回放rtsp地址
-
*/
-
@Value("${myconfig.replay-rtsp}")
-
private String rtspReplayPattern;
-
/**
-
* 视频帧率
-
*/
-
public static int frameRate = 15;
-
/**
-
* 视频宽度
-
*/
-
public static int frameWidth = 480;
-
/**
-
* 视频高度
-
*/
-
public static int frameHeight = 270;
-
/**
-
* 摄像机直播
-
* @param rtsp 摄像机直播地址
-
* @param cameraName 摄像机名称
-
* @param cameraId 摄像机id
-
* @throws Exception e
-
*/
-
@Async
-
public void live(String rtsp, String cameraName, Long cameraId) throws Exception {
-
if (liveSet.contains(cameraId)) {
-
return;
-
}
-
liveSet.add(cameraId);
-
FFmpegFrameGrabber grabber = createGrabber(rtsp);
-
startCameraPush(grabber, cameraName, cameraId);
-
}
-
/**
-
* 构造视频抓取器
-
* @param rtsp 拉流地址
-
* @return
-
*/
-
public FFmpegFrameGrabber createGrabber(String rtsp) {
-
// 获取视频源
-
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(rtsp);
-
grabber.setOption("rtsp_transport","tcp");
-
//设置帧率
-
grabber.setFrameRate(frameRate);
-
//设置获取的视频宽度
-
grabber.setImageWidth(frameWidth);
-
//设置获取的视频高度
-
grabber.setImageHeight(frameHeight);
-
//设置视频bit率
-
grabber.setVideoBitrate(2000000);
-
return grabber;
-
}
-
/**
-
* 推送图片(摄像机直播)
-
* @param grabber
-
* @throws Exception
-
*/
-
@Async
-
public void startCameraPush(FFmpegFrameGrabber grabber, String cameraName, Long cameraId) throws Exception {
-
Java2DFrameConverter java2DFrameConverter = new Java2DFrameConverter();
-
try {
-
grabber.start();
-
int i = 1;
-
while (liveSet.contains(cameraId)) {
-
Frame frame = grabber.grabImage();
-
if (null == frame) {
-
continue;
-
}
-
BufferedImage bufferedImage = java2DFrameConverter.getBufferedImage(frame);
-
byte[] bytes = imageToBytes(bufferedImage, "jpg");
-
//使用websocket发送图片数据
-
LiveWebsocket.sendImage(ByteBuffer.wrap(bytes), cameraId);
-
}
-
} finally {
-
if (grabber != null) {
-
grabber.stop();
-
}
-
}
-
}
-
/**
-
* 图片转字节数组
-
* @param bImage 图片数据
-
* @param format 格式
-
* @return 图片字节码
-
*/
-
private byte[] imageToBytes(BufferedImage bImage, String format) {
-
ByteArrayOutputStream out = new ByteArrayOutputStream();
-
try {
-
ImageIO.write(bImage, format, out);
-
} catch (IOException e) {
-
e.printStackTrace();
-
}
-
return out.toByteArray();
-
}
-
/**
-
* 回放视频播放超期检查
-
* @param userId 用户id
-
* @return
-
*/
-
private boolean replayOverTime(Integer userId) {
-
if (replayMap.containsKey(userId)) {
-
Long updateTime = replayMap.get(userId);
-
if (updateTime != null) {
-
if (System.currentTimeMillis() - updateTime < 10000) {
-
return false;
-
}
-
}
-
}
-
return true;
-
}
-
/**
-
* 构造监控回放查询字段
-
* @param date 时间
-
* @param start
-
* @return
-
*/
-
private String formatPullTime(Date date, boolean start) {
-
Calendar calendar = Calendar.getInstance();
-
if (date != null) {
-
calendar.setTime(date);
-
}
-
if (start) {
-
calendar.add(Calendar.SECOND, -10);
-
} else {
-
calendar.add(Calendar.SECOND, 10);
-
}
-
//海康威视取回放的时间格式
-
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd#HHmmss$");
-
String ret = sdf.format(calendar.getTime());
-
ret = ret.replace("#", "t");
-
ret = ret.replace("$", "z");
-
return ret;
-
}
-
/**
-
* 回放视频播放超期检查
-
* @param userId 用户id
-
* @return
-
*/
-
private boolean replayOverTime(Integer userId) {
-
if (replayMap.containsKey(userId)) {
-
Long updateTime = replayMap.get(userId);
-
if (updateTime != null) {
-
if (System.currentTimeMillis() - updateTime < 10000) {
-
return false;
-
}
-
}
-
}
-
return true;
-
}
-
/**
-
* 监控回放
-
* @param userId 用户id websocket 用户编号,作为心跳标识检测心跳
-
* @param startDate 起始时间
-
* @param endDate 结束时间
-
* @param channel 通道号
-
* @param username 用户名
-
* @param password 密码
-
* @param ip
-
* @param port
-
* @throws Exception e
-
*/
-
@Async
-
public void replayVideo(Integer userId, Date startDate, Date endDate,
-
Integer channel, String username, String password, String ip, Integer port) throws Exception {
-
Java2DFrameConverter java2DFrameConverter = new Java2DFrameConverter();
-
FFmpegFrameGrabber grabber = null;
-
try {
-
if (grabber != null) {
-
grabber.stop();
-
}
-
if (channel != null) {
-
String st = formatPullTime(startDate, true);
-
String et = formatPullTime(endDate, false);
-
//构造rtsp回放流地址 username password ip port
-
String rtsp = String.format(
-
rtspReplayPattern,
-
username,
-
password,
-
ip,
-
port,
-
channel,
-
st,
-
et
-
);
-
if (grabber != null) {
-
grabber.stop();
-
}
-
grabber = createGrabber(rtsp);
-
grabber.setTimeout(10000);
-
grabber.start();
-
//心跳消失停止推流
-
while (!replayOverTime(userId)) {
-
Frame frame = grabber.grabImage();
-
if (null == frame) {
-
continue;
-
}
-
BufferedImage bufferedImage = java2DFrameConverter.getBufferedImage(frame);
-
byte[] bytes = imageToBytes(bufferedImage, "jpg");
-
ByteBuffer buffer = ByteBuffer.wrap(bytes);
-
//使用websocket发送图片数据
-
ReplayWebsocke.sendImage(buffer, userId);
-
}
-
}
-
} catch (Exception e){
-
e.printStackTrace();
-
} finally {
-
if (grabber != null) {
-
grabber.stop();
-
}
-
}
-
}
-
}
视频水印添加:
-
@Component
-
public class ImgMarker {
-
/**
-
* 视频水印图片
-
*/
-
BufferedImage logoImg;
-
private Font font;
-
private Font font2;
-
private FontDesignMetrics metrics;
-
private FontDesignMetrics metrics2;
-
@PostConstruct
-
private void init() {
-
// 加水印图片
-
try {
-
ImageIO.read(new File("图片地址"));
-
} catch (IOException e) {
-
e.printStackTrace();
-
}
-
font = new Font("黑体", Font.BOLD, 16);
-
font2 = new Font("黑体", Font.BOLD, 24);
-
metrics = FontDesignMetrics.getMetrics(font);
-
metrics2 = FontDesignMetrics.getMetrics(font2);
-
}
-
/**
-
* 加水印
-
* @param bufImg 视频帧
-
*/
-
public void mark(BufferedImage bufImg) {
-
if (bufImg == null || logoImg == null) {
-
return;
-
}
-
int width = bufImg.getWidth();
-
int height = bufImg.getHeight();
-
Graphics2D graphics = bufImg.createGraphics();
-
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
-
graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
-
//设置图片背景
-
graphics.drawImage(bufImg, 0, 0, width, height, null);
-
//添加右上角水印
-
graphics.drawImage(logoImg, width - 130, 8, 121, 64, null);
-
}
-
/**
-
*
-
* @param bufImg 视频帧
-
*/
-
public void markTag(BufferedImage bufImg, String msg, int videoWidth) {
-
int width = bufImg.getWidth();
-
int height = bufImg.getHeight();
-
Graphics2D graphics = bufImg.createGraphics();
-
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
-
graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
-
//设置图片背景
-
graphics.drawImage(bufImg, 0, 0, width, height, null);
-
//设置由上方标签号
-
graphics.setColor(Color.orange);
-
if (videoWidth <= 400) {
-
graphics.setFont(font2);
-
graphics.drawString(msg, width - metrics2.stringWidth(tagId) - 24, metrics2.getAscent());
-
} else {
-
graphics.setFont(font);
-
graphics.drawString(msg, width - metrics.stringWidth(msg) - 12, metrics.getAscent());
-
}
-
graphics.dispose();
-
}
-
}
至此可以调用了(还有其他方式是直接调用SDK和推送视频流媒体服务器,后续文章更新)
websocket 涉及到高并发阻塞情况 后续更新,建议使用netty