init
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package com.ruoyi.video.common;
|
||||
|
||||
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
|
||||
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.AsyncConfigurer;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
@Configuration
|
||||
public class AsyncConfig implements AsyncConfigurer {
|
||||
|
||||
@Override
|
||||
public Executor getAsyncExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(5);
|
||||
executor.setMaxPoolSize(10);
|
||||
executor.setQueueCapacity(25);
|
||||
executor.setThreadNamePrefix("AsyncThread-");
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
|
||||
return new SimpleAsyncUncaughtExceptionHandler();
|
||||
}
|
||||
}
|
||||
|
||||
114
ruoyi-video/src/main/java/com/ruoyi/video/common/CacheMap.java
Normal file
114
ruoyi-video/src/main/java/com/ruoyi/video/common/CacheMap.java
Normal file
@@ -0,0 +1,114 @@
|
||||
package com.ruoyi.video.common;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 存储短暂对象的缓存类,实现Map接口,内部有一个定时器用来清除过期(30秒)的对象。
|
||||
* 为避免创建过多线程,没有特殊要求请使用getDefault()方法来获取本类的实例。
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
public class CacheMap<K, V> extends AbstractMap<K, V> {
|
||||
|
||||
private static final long DEFAULT_TIMEOUT = 30000;
|
||||
private static CacheMap<Object, Object> defaultInstance;
|
||||
|
||||
public static synchronized final CacheMap<Object, Object> getDefault() {
|
||||
if (defaultInstance == null) {
|
||||
defaultInstance = new CacheMap<Object, Object>(DEFAULT_TIMEOUT);
|
||||
}
|
||||
return defaultInstance;
|
||||
}
|
||||
|
||||
private class CacheEntry implements Entry<K, V> {
|
||||
long time;
|
||||
V value;
|
||||
K key;
|
||||
|
||||
CacheEntry(K key, V value) {
|
||||
super();
|
||||
this.value = value;
|
||||
this.key = key;
|
||||
this.time = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public K getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V setValue(V value) {
|
||||
return this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private class ClearThread extends Thread {
|
||||
ClearThread() {
|
||||
setName("clear cache thread");
|
||||
}
|
||||
|
||||
public void run() {
|
||||
while (true) {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
Object[] keys = map.keySet().toArray();
|
||||
for (Object key : keys) {
|
||||
CacheEntry entry = map.get(key);
|
||||
if (now - entry.time >= cacheTimeout) {
|
||||
synchronized (map) {
|
||||
map.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
Thread.sleep(cacheTimeout);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long cacheTimeout;
|
||||
private Map<K, CacheEntry> map = new HashMap<K, CacheEntry>();
|
||||
|
||||
public CacheMap(long timeout) {
|
||||
this.cacheTimeout = timeout;
|
||||
new ClearThread().start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<K, V>> entrySet() {
|
||||
Set<Entry<K, V>> entrySet = new HashSet<Map.Entry<K, V>>();
|
||||
Set<Entry<K, CacheEntry>> wrapEntrySet = map.entrySet();
|
||||
for (Entry<K, CacheEntry> entry : wrapEntrySet) {
|
||||
entrySet.add(entry.getValue());
|
||||
}
|
||||
return entrySet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V get(Object key) {
|
||||
CacheEntry entry = map.get(key);
|
||||
return entry == null ? null : entry.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V put(K key, V value) {
|
||||
CacheEntry entry = new CacheEntry(key, value);
|
||||
synchronized (map) {
|
||||
map.put(key, entry);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.ruoyi.video.common;
|
||||
|
||||
/**
|
||||
* 定义一个枚举类,表示客户端类型
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
public enum ClientType {
|
||||
|
||||
// 定义一个HTTP类型的客户端,类型为0,信息为"http"
|
||||
HTTP(0,"http"),
|
||||
// 定义一个WEBSOCKET类型的客户端,类型为1,信息为"websocket"
|
||||
WEBSOCKET(1,"websocket"),
|
||||
;
|
||||
|
||||
// 定义客户端类型
|
||||
private int type;
|
||||
// 定义客户端信息
|
||||
private String info;
|
||||
|
||||
// 构造方法,初始化客户端类型和信息
|
||||
private ClientType(int type, String info){
|
||||
this.type = type;
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
// 获取客户端类型
|
||||
public int getType(){
|
||||
return type;
|
||||
}
|
||||
|
||||
// 获取客户端信息
|
||||
public String getInfo(){
|
||||
return info;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ruoyi.video.common;
|
||||
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 常量类
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
public class MediaConstant {
|
||||
// 服务器名称
|
||||
public static String serverName = "EasyMedia";
|
||||
// 线程池
|
||||
public static ThreadPoolExecutor threadpool;
|
||||
// ffmpeg路径键
|
||||
public static String ffmpegPathKey;
|
||||
|
||||
// 构造方法
|
||||
public MediaConstant() {
|
||||
}
|
||||
|
||||
// 静态代码块,初始化线程池和ffmpeg路径键
|
||||
static {
|
||||
// 创建线程池,核心线程数为20,最大线程数为500,线程空闲时间60秒,任务队列使用LinkedBlockingDeque,拒绝策略为CallerRunsPolicy
|
||||
threadpool = new ThreadPoolExecutor(20, 500, 60L, TimeUnit.SECONDS, new LinkedBlockingDeque(), new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
// ffmpeg路径键
|
||||
ffmpegPathKey = "EasyMediaFFmpeg";
|
||||
}
|
||||
}
|
||||
137
ruoyi-video/src/main/java/com/ruoyi/video/domain/Device.java
Normal file
137
ruoyi-video/src/main/java/com/ruoyi/video/domain/Device.java
Normal file
@@ -0,0 +1,137 @@
|
||||
package com.ruoyi.video.domain;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 设备对象 v_device
|
||||
*
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
public class Device extends BaseEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private Long deviceId;
|
||||
@Excel(name = "IP地址")
|
||||
private String ip;
|
||||
@Excel(
|
||||
name = "设备类型(1=haikan,2=dahua)"
|
||||
)
|
||||
private String type;
|
||||
@Excel(
|
||||
name = "区分流"
|
||||
)
|
||||
private String mediaKey;
|
||||
@Excel(
|
||||
name = "设备账号"
|
||||
)
|
||||
private String userName;
|
||||
@Excel(
|
||||
name = "设备密码"
|
||||
)
|
||||
private String password;
|
||||
@Excel(
|
||||
name = "播放地址"
|
||||
)
|
||||
private String url;
|
||||
@Excel(
|
||||
name = "启用flv(0=true,1=false)"
|
||||
)
|
||||
private String enabledFlv;
|
||||
@Excel(
|
||||
name = "启用hls(0=true,1=false)"
|
||||
)
|
||||
private String enabledHls;
|
||||
@Excel(
|
||||
name = "javacv/ffmpeg(默认不开启)"
|
||||
)
|
||||
private String mode;
|
||||
|
||||
public Device() {
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "Device{deviceId=" + this.deviceId + ", ip='" + this.ip + "', type='" + this.type + "', mediaKey='" + this.mediaKey + "', userName='" + this.userName + "', password='" + this.password + "', url='" + this.url + "', enabledFlv='" + this.enabledFlv + "', enabledHls='" + this.enabledHls + "', mode='" + this.mode + "'}";
|
||||
}
|
||||
|
||||
public Long getDeviceId() {
|
||||
return this.deviceId;
|
||||
}
|
||||
|
||||
public void setDeviceId(Long deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public String getIp() {
|
||||
return this.ip;
|
||||
}
|
||||
|
||||
public void setIp(String ip) {
|
||||
this.ip = ip;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getMediaKey() {
|
||||
return this.mediaKey;
|
||||
}
|
||||
|
||||
public void setMediaKey(String mediaKey) {
|
||||
this.mediaKey = mediaKey;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return this.userName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName) {
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getEnabledFlv() {
|
||||
return this.enabledFlv;
|
||||
}
|
||||
|
||||
public void setEnabledFlv(String enabledFlv) {
|
||||
this.enabledFlv = enabledFlv;
|
||||
}
|
||||
|
||||
public String getEnabledHls() {
|
||||
return this.enabledHls;
|
||||
}
|
||||
|
||||
public void setEnabledHls(String enabledHls) {
|
||||
this.enabledHls = enabledHls;
|
||||
}
|
||||
|
||||
public String getMode() {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
public void setMode(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
71
ruoyi-video/src/main/java/com/ruoyi/video/domain/VImage.java
Normal file
71
ruoyi-video/src/main/java/com/ruoyi/video/domain/VImage.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package com.ruoyi.video.domain;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
|
||||
/**
|
||||
* 图片存储对象 v_image
|
||||
*
|
||||
* @author orange
|
||||
* @date 2025-01-16
|
||||
*/
|
||||
public class VImage extends BaseEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 图片名称
|
||||
*/
|
||||
@Excel(name = "图片名称")
|
||||
private String imageName;
|
||||
|
||||
/**
|
||||
* 图片数据
|
||||
*/
|
||||
@Excel(name = "图片数据")
|
||||
private String imageData;
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setImageName(String imageName) {
|
||||
this.imageName = imageName;
|
||||
}
|
||||
|
||||
public String getImageName() {
|
||||
return imageName;
|
||||
}
|
||||
|
||||
public void setImageData(String imageData) {
|
||||
this.imageData = imageData;
|
||||
}
|
||||
|
||||
public String getImageData() {
|
||||
return imageData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
|
||||
.append("id", getId())
|
||||
.append("imageName", getImageName())
|
||||
.append("imageData", getImageData())
|
||||
.append("createBy", getCreateBy())
|
||||
.append("createTime", getCreateTime())
|
||||
.append("updateBy", getUpdateBy())
|
||||
.append("updateTime", getUpdateTime())
|
||||
.append("remark", getRemark())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.ruoyi.video.domain.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* camera相机
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class CameraDto implements Serializable {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = -5575352151805386129L;
|
||||
|
||||
/**
|
||||
* rtsp、rtmp、d:/flv/test.mp4、desktop
|
||||
*/
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 流备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* flv开启状态
|
||||
*/
|
||||
private boolean enabledFlv = true;
|
||||
|
||||
/**
|
||||
* hls开启状态
|
||||
*/
|
||||
private boolean enabledHls = false;
|
||||
|
||||
/**
|
||||
* 是否启用ffmpeg,启用ffmpeg则不用javacv
|
||||
*/
|
||||
private boolean enabledFFmpeg = false;
|
||||
|
||||
/**
|
||||
* 无人拉流观看是否自动关闭流
|
||||
*/
|
||||
private boolean autoClose;
|
||||
|
||||
/**
|
||||
* md5 key,媒体标识,区分不同媒体
|
||||
*/
|
||||
private String mediaKey;
|
||||
|
||||
/**
|
||||
* 网络超时 ffmpeg默认5秒,这里设置15秒
|
||||
*/
|
||||
private String netTimeout = "15000000";
|
||||
/**
|
||||
* 读写超时,默认5秒
|
||||
*/
|
||||
private String readOrWriteTimeout = "15000000";
|
||||
|
||||
/**
|
||||
* 无人拉流观看持续多久自动关闭,默认1分钟
|
||||
*/
|
||||
private long noClientsDuration = 60000;
|
||||
|
||||
/**
|
||||
* 0网络流,1本地视频
|
||||
*/
|
||||
private int type = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ruoyi.video.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* CameraVo
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
@Data
|
||||
public class CameraVo {
|
||||
|
||||
private String id;
|
||||
/**
|
||||
* 播放地址
|
||||
*/
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 启用flv
|
||||
*/
|
||||
private boolean enabledFlv = false;
|
||||
|
||||
/**
|
||||
* 启用hls
|
||||
*/
|
||||
private boolean enabledHls = false;
|
||||
|
||||
/**
|
||||
* javacv/ffmpeg
|
||||
*/
|
||||
private String mode = "未开启";
|
||||
}
|
||||
|
||||
|
||||
134
ruoyi-video/src/main/java/com/ruoyi/video/init/InitServer.java
Normal file
134
ruoyi-video/src/main/java/com/ruoyi/video/init/InitServer.java
Normal file
@@ -0,0 +1,134 @@
|
||||
package com.ruoyi.video.init;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.ruoyi.video.common.MediaConstant;
|
||||
import com.ruoyi.video.domain.Device;
|
||||
import com.ruoyi.video.domain.dto.CameraDto;
|
||||
import com.ruoyi.video.mapper.DeviceMapper;
|
||||
import com.ruoyi.video.server.MediaServer;
|
||||
import com.ruoyi.video.service.HlsService;
|
||||
import com.ruoyi.video.service.MediaService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.bytedeco.ffmpeg.ffmpeg;
|
||||
import org.bytedeco.javacpp.Loader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
@Component
|
||||
public class InitServer implements CommandLineRunner {
|
||||
// 定义日志对象
|
||||
private static final Logger log = LoggerFactory.getLogger(InitServer.class);
|
||||
// 注入mediasServer.port的值
|
||||
@Value("${mediasServer.port}")
|
||||
private int port;
|
||||
// 注入MediaServer对象
|
||||
@Autowired
|
||||
private MediaServer mediaServer;
|
||||
// 注入MediaService对象
|
||||
@Autowired
|
||||
private MediaService mediaService;
|
||||
// 注入HlsService对象
|
||||
@Autowired
|
||||
private HlsService hlsService;
|
||||
// 注入DeviceMapper对象
|
||||
@Autowired
|
||||
private DeviceMapper deviceMapper;
|
||||
// 注入Environment对象
|
||||
@Autowired
|
||||
private Environment env;
|
||||
|
||||
public InitServer() {
|
||||
}
|
||||
|
||||
// 实现CommandLineRunner接口的run方法
|
||||
public void run(String... args) throws Exception {
|
||||
// 初始化自动播放
|
||||
this.initAutoPlay();
|
||||
// 获取本机IP地址
|
||||
String ip = InetAddress.getLocalHost().getHostAddress();
|
||||
// 获取http端口号
|
||||
String httpPort = this.env.getProperty("server.port");
|
||||
// 获取web路径
|
||||
String path = this.env.getProperty("server.servlet.context-path");
|
||||
if (StrUtil.isEmpty(path)) {
|
||||
path = "";
|
||||
}
|
||||
|
||||
// 打印启动信息
|
||||
log.info("\n--------------------------------------------------------- \n" +
|
||||
"\t RTSP视频分析平台启动成功! Access address: \n" +
|
||||
"\t media port at : \t {} \n" +
|
||||
"\t http port at : \t {} \n" +
|
||||
"\t web Local: \t http://localhost:{} \n" +
|
||||
"\t web External: \t http://{}:{}{} \n" +
|
||||
"\t httpflv: \t http://{}:{}/live?url={您的源地址} \n" +
|
||||
"\t wsflv: \t ws://{}:{}/live?url={您的源地址} \n" +
|
||||
"\t hls(m3u8): \t http://{}:{}/hls?url={您的源地址} \n" +
|
||||
"\t h2-database: \t http://{}:{}/h2-console \n" +
|
||||
"\t 作者微信: \t chenbai0511 \n" +
|
||||
"\t 请我喝杯咖啡吧qwq \n" +
|
||||
"--------------------------------------------------------- \n",
|
||||
port,
|
||||
httpPort,
|
||||
httpPort,
|
||||
ip, httpPort, path,
|
||||
ip, port,
|
||||
ip, port,
|
||||
ip, httpPort,
|
||||
ip, httpPort);
|
||||
// 启动MediaServer
|
||||
this.mediaServer.start(new InetSocketAddress("0.0.0.0", this.port));
|
||||
}
|
||||
|
||||
// 初始化自动播放
|
||||
public void initAutoPlay() {
|
||||
// 获取设备列表
|
||||
List<Device> devices = this.deviceMapper.selectDeviceList(new Device());
|
||||
if (null != devices && !devices.isEmpty()) {
|
||||
log.info("已启动自动拉流!");
|
||||
Iterator var2 = devices.iterator();
|
||||
|
||||
while (var2.hasNext()) {
|
||||
Device device = (Device) var2.next();
|
||||
CameraDto cameraDto = new CameraDto();
|
||||
cameraDto.setUrl(device.getUrl());
|
||||
cameraDto.setAutoClose(false);
|
||||
cameraDto.setEnabledFFmpeg(device.getEnabledFlv().equals("0"));
|
||||
cameraDto.setEnabledFlv(device.getEnabledFlv().equals("0"));
|
||||
cameraDto.setEnabledHls(device.getEnabledHls().equals("0"));
|
||||
cameraDto.setMediaKey(device.getMediaKey());
|
||||
if (device.getEnabledFlv().equals("0")) {
|
||||
this.mediaService.playForApi(cameraDto);
|
||||
}
|
||||
|
||||
if (device.getEnabledHls().equals("0")) {
|
||||
this.hlsService.startConvertToHls(cameraDto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 在初始化后加载FFmpeg
|
||||
@PostConstruct
|
||||
public void loadFFmpeg() {
|
||||
String ffmpeg = Loader.load(org.bytedeco.ffmpeg.ffmpeg.class);
|
||||
System.setProperty(MediaConstant.ffmpegPathKey, ffmpeg);
|
||||
log.info("初始化资源成功");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.ruoyi.video.mapper;
|
||||
|
||||
import com.ruoyi.video.domain.Device;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设备Mapper接口
|
||||
*
|
||||
* @author orange
|
||||
* @date 2025-01-14
|
||||
*/
|
||||
public interface DeviceMapper {
|
||||
/**
|
||||
* 查询设备
|
||||
*
|
||||
* @param deviceId 设备主键
|
||||
* @return 设备
|
||||
*/
|
||||
public Device selectDeviceByDeviceId(Long deviceId);
|
||||
|
||||
/**
|
||||
* 查询设备列表
|
||||
*
|
||||
* @param device 设备
|
||||
* @return 设备集合
|
||||
*/
|
||||
public List<Device> selectDeviceList(Device device);
|
||||
|
||||
/**
|
||||
* 新增设备
|
||||
*
|
||||
* @param device 设备
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertDevice(Device device);
|
||||
|
||||
/**
|
||||
* 修改设备
|
||||
*
|
||||
* @param device 设备
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateDevice(Device device);
|
||||
|
||||
/**
|
||||
* 删除设备
|
||||
*
|
||||
* @param deviceId 设备主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteDeviceByDeviceId(Long deviceId);
|
||||
|
||||
/**
|
||||
* 批量删除设备
|
||||
*
|
||||
* @param deviceIds 需要删除的数据主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteDeviceByDeviceIds(Long[] deviceIds);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.ruoyi.video.mapper;
|
||||
|
||||
import com.ruoyi.video.domain.VImage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图片存储Mapper接口
|
||||
*
|
||||
* @author orange
|
||||
* @date 2025-01-16
|
||||
*/
|
||||
public interface VImageMapper
|
||||
{
|
||||
/**
|
||||
* 查询图片存储
|
||||
*
|
||||
* @param id 图片存储主键
|
||||
* @return 图片存储
|
||||
*/
|
||||
public VImage selectVImageById(Long id);
|
||||
|
||||
/**
|
||||
* 查询图片存储列表
|
||||
*
|
||||
* @param vImage 图片存储
|
||||
* @return 图片存储集合
|
||||
*/
|
||||
public List<VImage> selectVImageList(VImage vImage);
|
||||
|
||||
/**
|
||||
* 新增图片存储
|
||||
*
|
||||
* @param vImage 图片存储
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertVImage(VImage vImage);
|
||||
|
||||
/**
|
||||
* 修改图片存储
|
||||
*
|
||||
* @param vImage 图片存储
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateVImage(VImage vImage);
|
||||
|
||||
/**
|
||||
* 删除图片存储
|
||||
*
|
||||
* @param id 图片存储主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteVImageById(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除图片存储
|
||||
*
|
||||
* @param ids 需要删除的数据主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteVImageByIds(Long[] ids);
|
||||
}
|
||||
281
ruoyi-video/src/main/java/com/ruoyi/video/server/FlvHandler.java
Normal file
281
ruoyi-video/src/main/java/com/ruoyi/video/server/FlvHandler.java
Normal file
@@ -0,0 +1,281 @@
|
||||
package com.ruoyi.video.server;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.MD5;
|
||||
import com.ruoyi.video.common.MediaConstant;
|
||||
import com.ruoyi.video.domain.dto.CameraDto;
|
||||
import com.ruoyi.video.service.MediaService;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.DefaultChannelPromise;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.ChannelHandler.Sharable;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpResponse;
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.FullHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.QueryStringDecoder;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
|
||||
import io.netty.util.CharsetUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Sharable
|
||||
public class FlvHandler extends SimpleChannelInboundHandler<Object> {
|
||||
|
||||
@Autowired
|
||||
private MediaService mediaService;
|
||||
|
||||
private WebSocketServerHandshaker handshaker;
|
||||
|
||||
/**
|
||||
* 网络超时
|
||||
*/
|
||||
@Value("${mediaserver.netTimeout:15000000}")
|
||||
private String netTimeout;
|
||||
/**
|
||||
* 读写超时
|
||||
*/
|
||||
@Value("${mediaserver.readOrWriteTimeout:15000000}")
|
||||
private String readOrWriteTimeout;
|
||||
|
||||
/**
|
||||
* 无人拉流观看是否自动关闭流
|
||||
*/
|
||||
@Value("${mediaserver.autoClose:true}")
|
||||
private boolean autoClose;
|
||||
|
||||
/**
|
||||
* 无人拉流观看持续多久自动关闭,1分钟
|
||||
*/
|
||||
@Value("${mediaserver.autoClose.noClientsDuration:60000}")
|
||||
private long noClientsDuration;
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
|
||||
if (msg instanceof FullHttpRequest) {
|
||||
FullHttpRequest req = (FullHttpRequest) msg;
|
||||
// Map<String, String> parmMap = new RequestParser(msg).parse();
|
||||
QueryStringDecoder decoder = new QueryStringDecoder(req.uri());
|
||||
|
||||
// 判断请求uri
|
||||
if (!"/live".equals(decoder.path())) {
|
||||
log.info("uri有误");
|
||||
sendError(ctx, HttpResponseStatus.BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
CameraDto cameraDto = buildCamera(req.uri());
|
||||
|
||||
if (StrUtil.isBlank(cameraDto.getUrl())) {
|
||||
log.info("url有误");
|
||||
sendError(ctx, HttpResponseStatus.BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
|
||||
// http请求
|
||||
sendFlvReqHeader(ctx);
|
||||
mediaService.playForHttp(cameraDto, ctx);
|
||||
|
||||
} else {
|
||||
// websocket握手,请求升级
|
||||
|
||||
// 参数分别是ws地址,子协议,是否扩展,最大frame长度
|
||||
WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(
|
||||
getWebSocketLocation(req), null, true, 5 * 1024 * 1024);
|
||||
handshaker = factory.newHandshaker(req);
|
||||
if (handshaker == null) {
|
||||
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
|
||||
} else {
|
||||
HttpResponse rsp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
rsp.headers().set(HttpHeaderNames.SERVER, MediaConstant.serverName);
|
||||
DefaultChannelPromise channelPromise = new DefaultChannelPromise(ctx.channel());
|
||||
|
||||
handshaker.handshake(ctx.channel(), req, rsp.headers(), channelPromise);
|
||||
mediaService.playForWs(cameraDto, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (msg instanceof WebSocketFrame) {
|
||||
handleWebSocketRequest(ctx, (WebSocketFrame) msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||
// 添加连接
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
// 断开连接
|
||||
}
|
||||
|
||||
/**
|
||||
* ws握手地址
|
||||
*/
|
||||
private String getWebSocketLocation(FullHttpRequest request) {
|
||||
String location = request.headers().get(HttpHeaderNames.HOST) + request.uri();
|
||||
return "ws://" + location;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送req header,告知浏览器是flv格式
|
||||
*
|
||||
* @param ctx
|
||||
*/
|
||||
private void sendFlvReqHeader(ChannelHandlerContext ctx) {
|
||||
HttpResponse rsp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
|
||||
rsp.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
|
||||
.set(HttpHeaderNames.CONTENT_TYPE, "video/x-flv").set(HttpHeaderNames.ACCEPT_RANGES, "bytes")
|
||||
.set(HttpHeaderNames.PRAGMA, "no-cache").set(HttpHeaderNames.CACHE_CONTROL, "no-cache")
|
||||
.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED).set(HttpHeaderNames.SERVER, MediaConstant.serverName);
|
||||
ctx.writeAndFlush(rsp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误请求响应
|
||||
*
|
||||
* @param ctx
|
||||
* @param status
|
||||
*/
|
||||
private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status,
|
||||
Unpooled.copiedBuffer("请求地址有误: " + status + "\r\n", CharsetUtil.UTF_8));
|
||||
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
|
||||
|
||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* websocket处理
|
||||
*
|
||||
* @param ctx
|
||||
* @param frame
|
||||
*/
|
||||
private void handleWebSocketRequest(ChannelHandlerContext ctx, WebSocketFrame frame) {
|
||||
// 关闭
|
||||
if (frame instanceof CloseWebSocketFrame) {
|
||||
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
|
||||
return;
|
||||
}
|
||||
|
||||
// 握手PING/PONG
|
||||
if (frame instanceof PingWebSocketFrame) {
|
||||
ctx.write(new PongWebSocketFrame(frame.content().retain()));
|
||||
return;
|
||||
}
|
||||
|
||||
// 文本
|
||||
if (frame instanceof TextWebSocketFrame) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame instanceof BinaryWebSocketFrame) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析参数,构建camera,&&&参数必须加在url参数值后面,&&&autoClose=false&&&hls=true
|
||||
* ws://localhost:8866/live?url=rtsp://admin:VZCDOY@192.168.2.84:554/Streaming/Channels/102&&&autoClose=false
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
private CameraDto buildCamera(String url) {
|
||||
CameraDto cameraDto = new CameraDto();
|
||||
setConfig(cameraDto);
|
||||
|
||||
String[] split = url.split("url=");
|
||||
String urlParent = split[1];
|
||||
|
||||
String[] split2 = urlParent.split("&&&");
|
||||
if (split2.length > 0) {
|
||||
for (String string : split2) {
|
||||
if (string.indexOf("autoClose=") != -1) {
|
||||
String[] as = string.split("=");
|
||||
if (as.length <= 1) {
|
||||
throw new RuntimeException("autoClose参数有误");
|
||||
}
|
||||
cameraDto.setAutoClose(Convert.toBool(as[1], false));
|
||||
} else if (string.indexOf("ffmpeg=") != -1) {
|
||||
String[] as = string.split("=");
|
||||
if (as.length <= 1) {
|
||||
throw new RuntimeException("ffmpeg参数有误");
|
||||
}
|
||||
cameraDto.setEnabledFFmpeg(Convert.toBool(as[1], false));
|
||||
} else if (string.indexOf("hls=") != -1) {
|
||||
String[] as = string.split("=");
|
||||
if (as.length <= 1) {
|
||||
throw new RuntimeException("hls参数有误");
|
||||
}
|
||||
cameraDto.setEnabledHls(Convert.toBool(as[1], false));
|
||||
} else {
|
||||
cameraDto.setUrl(string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isLocalFile(cameraDto.getUrl())) {
|
||||
cameraDto.setType(1);
|
||||
}
|
||||
|
||||
// 区分不同媒体
|
||||
String mediaKey = MD5.create().digestHex(cameraDto.getUrl());
|
||||
cameraDto.setMediaKey(mediaKey);
|
||||
|
||||
return cameraDto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置默认参数
|
||||
*/
|
||||
private void setConfig(CameraDto cameraDto) {
|
||||
cameraDto.setNetTimeout(netTimeout);
|
||||
cameraDto.setReadOrWriteTimeout(readOrWriteTimeout);
|
||||
cameraDto.setAutoClose(autoClose);
|
||||
cameraDto.setNoClientsDuration(noClientsDuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是本地文件,判断前面长度是不是小于1个字符,认为是盘符
|
||||
* @return
|
||||
*/
|
||||
private boolean isLocalFile(String streamUrl) {
|
||||
String[] split = streamUrl.trim().split("\\:");
|
||||
if (split.length > 0) {
|
||||
if (split[0].length() <= 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.ruoyi.video.server;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.WriteBufferWaterMark;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.channel.unix.PreferredDirectByteBufAllocator;
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.HttpRequestDecoder;
|
||||
import io.netty.handler.codec.http.HttpResponseEncoder;
|
||||
import io.netty.handler.codec.http.cors.CorsConfig;
|
||||
import io.netty.handler.codec.http.cors.CorsConfigBuilder;
|
||||
import io.netty.handler.codec.http.cors.CorsHandler;
|
||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* flv流媒体服务
|
||||
*
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MediaServer {
|
||||
|
||||
@Autowired
|
||||
private FlvHandler flvHandler;
|
||||
|
||||
public void start(InetSocketAddress socketAddress) {
|
||||
//new 一个主线程组
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
|
||||
//new 一个工作线程组
|
||||
EventLoopGroup workGroup = new NioEventLoopGroup(200);
|
||||
ServerBootstrap bootstrap = new ServerBootstrap()
|
||||
.group(bossGroup, workGroup)
|
||||
.channel(NioServerSocketChannel.class)
|
||||
.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
CorsConfig corsConfig = CorsConfigBuilder.forAnyOrigin().allowNullOrigin().allowCredentials().build();
|
||||
|
||||
socketChannel.pipeline()
|
||||
.addLast(new HttpResponseEncoder())
|
||||
.addLast(new HttpRequestDecoder())
|
||||
.addLast(new ChunkedWriteHandler())
|
||||
.addLast(new HttpObjectAggregator(64 * 1024))
|
||||
.addLast(new CorsHandler(corsConfig))
|
||||
.addLast(flvHandler);
|
||||
}
|
||||
})
|
||||
.localAddress(socketAddress)
|
||||
.option(ChannelOption.SO_BACKLOG, 128)
|
||||
//首选直接内存
|
||||
.option(ChannelOption.ALLOCATOR, PreferredDirectByteBufAllocator.DEFAULT)
|
||||
// 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
|
||||
.childOption(ChannelOption.TCP_NODELAY, true)
|
||||
.childOption(ChannelOption.SO_KEEPALIVE, true)
|
||||
.childOption(ChannelOption.SO_RCVBUF, 128 * 1024)
|
||||
.childOption(ChannelOption.SO_SNDBUF, 1024 * 1024)
|
||||
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(1024 * 1024 / 2, 1024 * 1024));
|
||||
//绑定端口,开始接收进来的连接
|
||||
try {
|
||||
ChannelFuture future = bootstrap.bind(socketAddress).sync();
|
||||
future.channel().closeFuture().sync();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
//关闭主线程组
|
||||
bossGroup.shutdownGracefully();
|
||||
//关闭工作线程组
|
||||
workGroup.shutdownGracefully();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.ruoyi.video.service;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.crypto.digest.MD5;
|
||||
import com.ruoyi.video.common.CacheMap;
|
||||
import com.ruoyi.video.domain.dto.CameraDto;
|
||||
import com.ruoyi.video.thread.MediaTransferHls;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 处理hls
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
@Service
|
||||
public class HlsService {
|
||||
|
||||
@Autowired
|
||||
private Environment env;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public static ConcurrentHashMap<String, MediaTransferHls> cameras = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 定义ts缓存10秒
|
||||
*/
|
||||
public static CacheMap<String, byte[]> cacheTs = new CacheMap<>(10000);
|
||||
public static CacheMap<String, byte[]> cacheM3u8 = new CacheMap<>(10000);
|
||||
|
||||
/**
|
||||
* 保存ts
|
||||
*/
|
||||
public void processTs(String mediaKey, String tsName, InputStream in) {
|
||||
byte[] readBytes = IoUtil.readBytes(in);
|
||||
String tsKey = mediaKey.concat("-").concat(tsName);
|
||||
cacheTs.put(tsKey, readBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存hls
|
||||
* @param mediaKey
|
||||
* @param in
|
||||
*/
|
||||
public void processHls(String mediaKey, InputStream in) {
|
||||
byte[] readBytes = IoUtil.readBytes(in);
|
||||
cacheM3u8.put(mediaKey, readBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭hls切片
|
||||
*
|
||||
* @param cameraDto
|
||||
*/
|
||||
public void closeConvertToHls(CameraDto cameraDto) {
|
||||
|
||||
// 区分不同媒体
|
||||
String mediaKey = MD5.create().digestHex(cameraDto.getUrl());
|
||||
|
||||
if (cameras.containsKey(mediaKey)) {
|
||||
MediaTransferHls mediaTransferHls = cameras.get(mediaKey);
|
||||
mediaTransferHls.stop();
|
||||
cameras.remove(mediaKey);
|
||||
cacheTs.remove(mediaKey);
|
||||
cacheM3u8.remove(mediaKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始hls切片
|
||||
*
|
||||
* @param cameraDto
|
||||
* @return
|
||||
*/
|
||||
public boolean startConvertToHls(CameraDto cameraDto) {
|
||||
|
||||
// 区分不同媒体
|
||||
String mediaKey = MD5.create().digestHex(cameraDto.getUrl());
|
||||
cameraDto.setMediaKey(mediaKey);
|
||||
|
||||
MediaTransferHls mediaTransferHls = cameras.get(mediaKey);
|
||||
|
||||
if (null == mediaTransferHls) {
|
||||
mediaTransferHls = new MediaTransferHls(cameraDto, Convert.toInt(env.getProperty("server.port")));
|
||||
cameras.put(mediaKey, mediaTransferHls);
|
||||
mediaTransferHls.execute();
|
||||
}
|
||||
|
||||
mediaTransferHls = cameras.get(mediaKey);
|
||||
|
||||
// 15秒还没true认为启动不了
|
||||
for (int i = 0; i < 30; i++) {
|
||||
if (mediaTransferHls.isRunning()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.ruoyi.video.service;
|
||||
|
||||
import com.ruoyi.video.domain.Device;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设备Service接口
|
||||
*
|
||||
* @author orange
|
||||
* @date 2025-01-14
|
||||
*/
|
||||
public interface IDeviceService
|
||||
{
|
||||
/**
|
||||
* 查询设备
|
||||
*
|
||||
* @param deviceId 设备主键
|
||||
* @return 设备
|
||||
*/
|
||||
public Device selectDeviceByDeviceId(Long deviceId);
|
||||
|
||||
/**
|
||||
* 查询设备列表
|
||||
*
|
||||
* @param device 设备
|
||||
* @return 设备集合
|
||||
*/
|
||||
public List<Device> selectDeviceList(Device device);
|
||||
|
||||
/**
|
||||
* 新增设备
|
||||
*
|
||||
* @param device 设备
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertDevice(Device device);
|
||||
|
||||
/**
|
||||
* 修改设备
|
||||
*
|
||||
* @param device 设备
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateDevice(Device device);
|
||||
|
||||
/**
|
||||
* 批量删除设备
|
||||
*
|
||||
* @param deviceIds 需要删除的设备主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteDeviceByDeviceIds(Long[] deviceIds);
|
||||
|
||||
/**
|
||||
* 删除设备信息
|
||||
*
|
||||
* @param deviceId 设备主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteDeviceByDeviceId(Long deviceId);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.ruoyi.video.service;
|
||||
|
||||
import com.ruoyi.video.domain.VImage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图片存储Service接口
|
||||
*
|
||||
* @author orange
|
||||
* @date 2025-01-16
|
||||
*/
|
||||
public interface IVImageService
|
||||
{
|
||||
/**
|
||||
* 查询图片存储
|
||||
*
|
||||
* @param id 图片存储主键
|
||||
* @return 图片存储
|
||||
*/
|
||||
public VImage selectVImageById(Long id);
|
||||
|
||||
/**
|
||||
* 查询图片存储列表
|
||||
*
|
||||
* @param vImage 图片存储
|
||||
* @return 图片存储集合
|
||||
*/
|
||||
public List<VImage> selectVImageList(VImage vImage);
|
||||
|
||||
/**
|
||||
* 新增图片存储
|
||||
*
|
||||
* @param vImage 图片存储
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertVImage(VImage vImage);
|
||||
|
||||
/**
|
||||
* 修改图片存储
|
||||
*
|
||||
* @param vImage 图片存储
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateVImage(VImage vImage);
|
||||
|
||||
/**
|
||||
* 批量删除图片存储
|
||||
*
|
||||
* @param ids 需要删除的图片存储主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteVImageByIds(Long[] ids);
|
||||
|
||||
/**
|
||||
* 删除图片存储信息
|
||||
*
|
||||
* @param id 图片存储主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteVImageById(Long id);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package com.ruoyi.video.service;
|
||||
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.crypto.digest.MD5;
|
||||
import com.ruoyi.video.common.ClientType;
|
||||
import com.ruoyi.video.domain.dto.CameraDto;
|
||||
import com.ruoyi.video.thread.MediaTransfer;
|
||||
import com.ruoyi.video.thread.MediaTransferFlvByFFmpeg;
|
||||
import com.ruoyi.video.thread.MediaTransferFlvByJavacv;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 媒体服务,支持全局网络超时、读写超时、无人拉流持续时长自动关闭流等配置
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
@Service
|
||||
public class MediaService {
|
||||
|
||||
/**
|
||||
* 缓存流转换线程
|
||||
*/
|
||||
public static ConcurrentHashMap<String, MediaTransfer> cameras = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* http-flv播放
|
||||
* @param cameraDto
|
||||
* @param ctx
|
||||
*/
|
||||
public void playForHttp(CameraDto cameraDto, ChannelHandlerContext ctx) {
|
||||
|
||||
if (cameras.containsKey(cameraDto.getMediaKey())) {
|
||||
MediaTransfer mediaConvert = cameras.get(cameraDto.getMediaKey());
|
||||
if(mediaConvert instanceof MediaTransferFlvByJavacv) {
|
||||
MediaTransferFlvByJavacv mediaTransferFlvByJavacv = (MediaTransferFlvByJavacv) mediaConvert;
|
||||
//如果当前已经用ffmpeg,则重新拉流
|
||||
if(cameraDto.isEnabledFFmpeg()) {
|
||||
mediaTransferFlvByJavacv.setRunning(false);
|
||||
cameras.remove(cameraDto.getMediaKey());
|
||||
this.playForHttp(cameraDto, ctx);
|
||||
} else {
|
||||
mediaTransferFlvByJavacv.addClient(ctx, ClientType.HTTP);
|
||||
}
|
||||
} else if (mediaConvert instanceof MediaTransferFlvByFFmpeg) {
|
||||
MediaTransferFlvByFFmpeg mediaTransferFlvByFFmpeg = (MediaTransferFlvByFFmpeg) mediaConvert;
|
||||
//如果当前已经用javacv,则关闭再重新拉流
|
||||
if(!cameraDto.isEnabledFFmpeg()) {
|
||||
mediaTransferFlvByFFmpeg.stopFFmpeg();
|
||||
cameras.remove(cameraDto.getMediaKey());
|
||||
this.playForHttp(cameraDto, ctx);
|
||||
} else {
|
||||
mediaTransferFlvByFFmpeg.addClient(ctx, ClientType.HTTP);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if(cameraDto.isEnabledFFmpeg()) {
|
||||
MediaTransferFlvByFFmpeg mediaft = new MediaTransferFlvByFFmpeg(cameraDto);
|
||||
mediaft.execute();
|
||||
cameras.put(cameraDto.getMediaKey(), mediaft);
|
||||
mediaft.addClient(ctx, ClientType.HTTP);
|
||||
} else {
|
||||
MediaTransferFlvByJavacv mediaConvert = new MediaTransferFlvByJavacv(cameraDto);
|
||||
cameras.put(cameraDto.getMediaKey(), mediaConvert);
|
||||
ThreadUtil.execute(mediaConvert);
|
||||
mediaConvert.addClient(ctx, ClientType.HTTP);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ws-flv播放
|
||||
* @param cameraDto
|
||||
* @param ctx
|
||||
*/
|
||||
public void playForWs(CameraDto cameraDto, ChannelHandlerContext ctx) {
|
||||
|
||||
if (cameras.containsKey(cameraDto.getMediaKey())) {
|
||||
MediaTransfer mediaConvert = cameras.get(cameraDto.getMediaKey());
|
||||
if(mediaConvert instanceof MediaTransferFlvByJavacv) {
|
||||
MediaTransferFlvByJavacv mediaTransferFlvByJavacv = (MediaTransferFlvByJavacv) mediaConvert;
|
||||
//如果当前已经用ffmpeg,则重新拉流
|
||||
if(cameraDto.isEnabledFFmpeg()) {
|
||||
mediaTransferFlvByJavacv.setRunning(false);
|
||||
cameras.remove(cameraDto.getMediaKey());
|
||||
this.playForWs(cameraDto, ctx);
|
||||
} else {
|
||||
mediaTransferFlvByJavacv.addClient(ctx, ClientType.WEBSOCKET);
|
||||
}
|
||||
} else if (mediaConvert instanceof MediaTransferFlvByFFmpeg) {
|
||||
MediaTransferFlvByFFmpeg mediaTransferFlvByFFmpeg = (MediaTransferFlvByFFmpeg) mediaConvert;
|
||||
//如果当前已经用javacv,则关闭再重新拉流
|
||||
if(!cameraDto.isEnabledFFmpeg()) {
|
||||
mediaTransferFlvByFFmpeg.stopFFmpeg();
|
||||
cameras.remove(cameraDto.getMediaKey());
|
||||
this.playForWs(cameraDto, ctx);
|
||||
} else {
|
||||
mediaTransferFlvByFFmpeg.addClient(ctx, ClientType.WEBSOCKET);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(cameraDto.isEnabledFFmpeg()) {
|
||||
MediaTransferFlvByFFmpeg mediaft = new MediaTransferFlvByFFmpeg(cameraDto);
|
||||
mediaft.execute();
|
||||
cameras.put(cameraDto.getMediaKey(), mediaft);
|
||||
mediaft.addClient(ctx, ClientType.WEBSOCKET);
|
||||
} else {
|
||||
MediaTransferFlvByJavacv mediaConvert = new MediaTransferFlvByJavacv(cameraDto);
|
||||
cameras.put(cameraDto.getMediaKey(), mediaConvert);
|
||||
ThreadUtil.execute(mediaConvert);
|
||||
mediaConvert.addClient(ctx, ClientType.WEBSOCKET);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* api播放
|
||||
* @param cameraDto
|
||||
* @return
|
||||
*/
|
||||
public boolean playForApi(CameraDto cameraDto) {
|
||||
// 区分不同媒体
|
||||
String mediaKey = MD5.create().digestHex(cameraDto.getUrl());
|
||||
cameraDto.setMediaKey(mediaKey);
|
||||
cameraDto.setEnabledFlv(true);
|
||||
|
||||
MediaTransfer mediaTransfer = cameras.get(cameraDto.getMediaKey());
|
||||
if (null == mediaTransfer) {
|
||||
if(cameraDto.isEnabledFFmpeg()) {
|
||||
MediaTransferFlvByFFmpeg mediaft = new MediaTransferFlvByFFmpeg(cameraDto);
|
||||
mediaft.execute();
|
||||
cameras.put(cameraDto.getMediaKey(), mediaft);
|
||||
} else {
|
||||
MediaTransferFlvByJavacv mediaConvert = new MediaTransferFlvByJavacv(cameraDto);
|
||||
cameras.put(cameraDto.getMediaKey(), mediaConvert);
|
||||
ThreadUtil.execute(mediaConvert);
|
||||
}
|
||||
}
|
||||
|
||||
mediaTransfer = cameras.get(cameraDto.getMediaKey());
|
||||
//同步等待
|
||||
if(mediaTransfer instanceof MediaTransferFlvByJavacv) {
|
||||
MediaTransferFlvByJavacv mediaft = (MediaTransferFlvByJavacv) mediaTransfer;
|
||||
// 30秒还没true认为启动不了
|
||||
for (int i = 0; i < 60; i++) {
|
||||
if (mediaft.isRunning() && mediaft.isGrabberStatus() && mediaft.isRecorderStatus()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
} else if (mediaTransfer instanceof MediaTransferFlvByFFmpeg) {
|
||||
MediaTransferFlvByFFmpeg mediaft = (MediaTransferFlvByFFmpeg) mediaTransfer;
|
||||
// 30秒还没true认为启动不了
|
||||
for (int i = 0; i < 60; i++) {
|
||||
if (mediaft.isRunning()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭流
|
||||
* @param cameraDto
|
||||
*/
|
||||
public void closeForApi(CameraDto cameraDto) {
|
||||
cameraDto.setEnabledFlv(false);
|
||||
|
||||
if (cameras.containsKey(cameraDto.getMediaKey())) {
|
||||
MediaTransfer mediaConvert = cameras.get(cameraDto.getMediaKey());
|
||||
if(mediaConvert instanceof MediaTransferFlvByJavacv) {
|
||||
MediaTransferFlvByJavacv mediaTransferFlvByJavacv = (MediaTransferFlvByJavacv) mediaConvert;
|
||||
mediaTransferFlvByJavacv.setRunning(false);
|
||||
cameras.remove(cameraDto.getMediaKey());
|
||||
} else if (mediaConvert instanceof MediaTransferFlvByFFmpeg) {
|
||||
MediaTransferFlvByFFmpeg mediaTransferFlvByFFmpeg = (MediaTransferFlvByFFmpeg) mediaConvert;
|
||||
mediaTransferFlvByFFmpeg.stopFFmpeg();
|
||||
cameras.remove(cameraDto.getMediaKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.ruoyi.video.service.impl;
|
||||
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.video.domain.Device;
|
||||
import com.ruoyi.video.mapper.DeviceMapper;
|
||||
import com.ruoyi.video.service.IDeviceService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设备Service业务层处理
|
||||
*
|
||||
* @author orange
|
||||
* @date 2025-01-14
|
||||
*/
|
||||
@Service
|
||||
public class DeviceServiceImpl implements IDeviceService {
|
||||
@Autowired
|
||||
private DeviceMapper deviceMapper;
|
||||
|
||||
/**
|
||||
* 查询设备
|
||||
*
|
||||
* @param deviceId 设备主键
|
||||
* @return 设备
|
||||
*/
|
||||
@Override
|
||||
public Device selectDeviceByDeviceId(Long deviceId) {
|
||||
return deviceMapper.selectDeviceByDeviceId(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询设备列表
|
||||
*
|
||||
* @param device 设备
|
||||
* @return 设备
|
||||
*/
|
||||
@Override
|
||||
public List<Device> selectDeviceList(Device device) {
|
||||
return deviceMapper.selectDeviceList(device);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增设备
|
||||
*
|
||||
* @param device 设备
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int insertDevice(Device device) {
|
||||
device.setUrl(this.generateRtspUrl(device));
|
||||
device.setCreateTime(DateUtils.getNowDate());
|
||||
return deviceMapper.insertDevice(device);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改设备
|
||||
*
|
||||
* @param device 设备
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int updateDevice(Device device) {
|
||||
device.setUrl(this.generateRtspUrl(device));
|
||||
device.setUpdateTime(DateUtils.getNowDate());
|
||||
return deviceMapper.updateDevice(device);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除设备
|
||||
*
|
||||
* @param deviceIds 需要删除的设备主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteDeviceByDeviceIds(Long[] deviceIds) {
|
||||
return deviceMapper.deleteDeviceByDeviceIds(deviceIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除设备信息
|
||||
*
|
||||
* @param deviceId 设备主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteDeviceByDeviceId(Long deviceId) {
|
||||
return deviceMapper.deleteDeviceByDeviceId(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成rtsp地址
|
||||
* @param device
|
||||
* @return
|
||||
*/
|
||||
private String generateRtspUrl(Device device) {
|
||||
String ip = device.getIp();
|
||||
String userName = device.getUserName();
|
||||
String password = device.getPassword();
|
||||
int port = 554;
|
||||
|
||||
if ("1".equals(device.getType())) {
|
||||
return String.format("rtsp://%s:%s@%s:%d/Streaming/Channels/2", userName, password, ip, port);
|
||||
} else if ("2".equals(device.getType())) {
|
||||
return String.format("rtsp://%s:%s@%s:%d/cam/realmonitor?channel=1&subtype=0", userName, password, ip, port);
|
||||
} else {
|
||||
throw new RuntimeException("网络异常,请检查网络!");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.ruoyi.video.service.impl;
|
||||
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.video.domain.VImage;
|
||||
import com.ruoyi.video.mapper.VImageMapper;
|
||||
import com.ruoyi.video.service.IVImageService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图片存储Service业务层处理
|
||||
*
|
||||
* @author orange
|
||||
* @date 2025-01-16
|
||||
*/
|
||||
@Service
|
||||
public class VImageServiceImpl implements IVImageService
|
||||
{
|
||||
@Autowired
|
||||
private VImageMapper vImageMapper;
|
||||
|
||||
/**
|
||||
* 查询图片存储
|
||||
*
|
||||
* @param id 图片存储主键
|
||||
* @return 图片存储
|
||||
*/
|
||||
@Override
|
||||
public VImage selectVImageById(Long id)
|
||||
{
|
||||
return vImageMapper.selectVImageById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询图片存储列表
|
||||
*
|
||||
* @param vImage 图片存储
|
||||
* @return 图片存储
|
||||
*/
|
||||
@Override
|
||||
public List<VImage> selectVImageList(VImage vImage)
|
||||
{
|
||||
return vImageMapper.selectVImageList(vImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增图片存储
|
||||
*
|
||||
* @param vImage 图片存储
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int insertVImage(VImage vImage)
|
||||
{
|
||||
vImage.setCreateTime(DateUtils.getNowDate());
|
||||
return vImageMapper.insertVImage(vImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改图片存储
|
||||
*
|
||||
* @param vImage 图片存储
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int updateVImage(VImage vImage)
|
||||
{
|
||||
vImage.setUpdateTime(DateUtils.getNowDate());
|
||||
return vImageMapper.updateVImage(vImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除图片存储
|
||||
*
|
||||
* @param ids 需要删除的图片存储主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteVImageByIds(Long[] ids)
|
||||
{
|
||||
return vImageMapper.deleteVImageByIds(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图片存储信息
|
||||
*
|
||||
* @param id 图片存储主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteVImageById(Long id)
|
||||
{
|
||||
return vImageMapper.deleteVImageById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ruoyi.video.thread;
|
||||
|
||||
/**
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
public class MediaTransfer {
|
||||
public MediaTransfer() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
package com.ruoyi.video.thread;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.ruoyi.video.common.ClientType;
|
||||
import com.ruoyi.video.common.MediaConstant;
|
||||
import com.ruoyi.video.domain.dto.CameraDto;
|
||||
import com.ruoyi.video.service.MediaService;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
import io.netty.util.concurrent.Future;
|
||||
import io.netty.util.concurrent.GenericFutureListener;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bytedeco.javacv.FrameGrabber;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
@Slf4j
|
||||
public class MediaTransferFlvByFFmpeg extends MediaTransfer {
|
||||
|
||||
/**
|
||||
* ws客户端
|
||||
*/
|
||||
private ConcurrentHashMap<String, ChannelHandlerContext> wsClients = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* http客户端
|
||||
*/
|
||||
private ConcurrentHashMap<String, ChannelHandlerContext> httpClients = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* flv header
|
||||
*/
|
||||
private byte[] header = null;
|
||||
/**
|
||||
* 相机
|
||||
*/
|
||||
private CameraDto cameraDto;
|
||||
|
||||
private List<String> command = new ArrayList<>();
|
||||
|
||||
private ServerSocket tcpServer = null;
|
||||
|
||||
private Process process;
|
||||
private Thread inputThread;
|
||||
private Thread errThread;
|
||||
private Thread outputThread;
|
||||
private Thread listenThread;
|
||||
private boolean running = false; // 启动
|
||||
private boolean enableLog = true;
|
||||
|
||||
private int hcSize, wcSize = 0;
|
||||
|
||||
// 记录当前
|
||||
long currentTimeMillis = System.currentTimeMillis();
|
||||
|
||||
/**
|
||||
* 用于没有客户端时候的计时
|
||||
*/
|
||||
private int noClient = 0;
|
||||
|
||||
public MediaTransferFlvByFFmpeg(final String executable) {
|
||||
command.add(executable);
|
||||
buildCommand();
|
||||
}
|
||||
|
||||
public MediaTransferFlvByFFmpeg(CameraDto cameraDto) {
|
||||
command.add(System.getProperty(MediaConstant.ffmpegPathKey));
|
||||
this.cameraDto = cameraDto;
|
||||
buildCommand();
|
||||
}
|
||||
|
||||
public MediaTransferFlvByFFmpeg(final String executable, CameraDto cameraDto) {
|
||||
command.add(executable);
|
||||
this.cameraDto = cameraDto;
|
||||
buildCommand();
|
||||
}
|
||||
|
||||
public MediaTransferFlvByFFmpeg(final String executable, CameraDto cameraDto, boolean enableLog) {
|
||||
command.add(executable);
|
||||
this.cameraDto = cameraDto;
|
||||
this.enableLog = enableLog;
|
||||
buildCommand();
|
||||
}
|
||||
|
||||
public boolean isEnableLog() {
|
||||
return enableLog;
|
||||
}
|
||||
|
||||
public void setEnableLog(boolean enableLog) {
|
||||
this.enableLog = enableLog;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public void setRunning(boolean running) {
|
||||
this.running = running;
|
||||
}
|
||||
|
||||
private MediaTransferFlvByFFmpeg addArgument(String argument) {
|
||||
command.add(argument);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建ffmpeg命令
|
||||
*/
|
||||
private void buildCommand() {
|
||||
this.addArgument("-rtsp_transport").addArgument("tcp").addArgument("-i").addArgument(cameraDto.getUrl())
|
||||
.addArgument("-max_delay").addArgument("1")
|
||||
// .addArgument("-strict").addArgument("experimental")
|
||||
.addArgument("-g").addArgument("25").addArgument("-r").addArgument("25")
|
||||
// .addArgument("-b").addArgument("200000")
|
||||
// .addArgument("-filter_complex").addArgument("setpts='(RTCTIME - RTCSTART) / (TB * 1000000)'")
|
||||
.addArgument("-c:v").addArgument("libx264").addArgument("-preset:v").addArgument("ultrafast")
|
||||
// .addArgument("-preset:v").addArgument("fast")
|
||||
.addArgument("-tune:v").addArgument("zerolatency")
|
||||
// .addArgument("-crf").addArgument("26")
|
||||
.addArgument("-c:a").addArgument("aac")
|
||||
// .addArgument("-qmin").addArgument("28")
|
||||
// .addArgument("-qmax").addArgument("32")
|
||||
// .addArgument("-b:v").addArgument("448k")
|
||||
// .addArgument("-b:a").addArgument("64k")
|
||||
.addArgument("-f").addArgument("flv");
|
||||
}
|
||||
|
||||
// private void buildCommand() {
|
||||
// this
|
||||
//// .addArgument("-rtsp_transport").addArgument("tcp")
|
||||
// .addArgument("-i").addArgument(camera.getUrl())
|
||||
// .addArgument("-max_delay").addArgument("100")
|
||||
//// .addArgument("-strict").addArgument("experimental")
|
||||
// .addArgument("-g").addArgument("10")
|
||||
//// .addArgument("-r").addArgument("25")
|
||||
//// .addArgument("-b").addArgument("200000")
|
||||
//// .addArgument("-filter_complex").addArgument("setpts='(RTCTIME - RTCSTART) / (TB * 1000000)'")
|
||||
// .addArgument("-c:v").addArgument("libx264")
|
||||
// .addArgument("-preset:v").addArgument("ultrafast")
|
||||
// .addArgument("-tune:v").addArgument("zerolatency")
|
||||
//// .addArgument("-crf").addArgument("26")
|
||||
// .addArgument("-c:a").addArgument("aac")
|
||||
// .addArgument("-qmin").addArgument("28")
|
||||
// .addArgument("-qmax").addArgument("32")
|
||||
// .addArgument("-b:v").addArgument("448k")
|
||||
// .addArgument("-b:a").addArgument("64k")
|
||||
// .addArgument("-f").addArgument("flv");
|
||||
// }
|
||||
|
||||
/**
|
||||
* 执行推流
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public MediaTransferFlvByFFmpeg execute() {
|
||||
String output = getOutput();
|
||||
command.add(output);
|
||||
|
||||
String join = CollUtil.join(command, " ");
|
||||
log.info(join);
|
||||
try {
|
||||
process = new ProcessBuilder(command).start();
|
||||
running = true;
|
||||
listenNetTimeout();
|
||||
dealStream(process);
|
||||
outputData();
|
||||
listenClient();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* flv数据
|
||||
*/
|
||||
private void outputData() {
|
||||
outputThread = new Thread(new Runnable() {
|
||||
public void run() {
|
||||
Socket client = null;
|
||||
try {
|
||||
client = tcpServer.accept();
|
||||
DataInputStream input = new DataInputStream(client.getInputStream());
|
||||
|
||||
byte[] buffer = new byte[1024];
|
||||
int len = 0;
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
while (running) {
|
||||
|
||||
len = input.read(buffer);
|
||||
if (len == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
bos.write(buffer, 0, len);
|
||||
|
||||
if (header == null) {
|
||||
header = bos.toByteArray();
|
||||
// System.out.println(HexUtil.encodeHexStr(header));
|
||||
bos.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 帧数据
|
||||
byte[] data = bos.toByteArray();
|
||||
bos.reset();
|
||||
|
||||
// 发送到前端
|
||||
sendFrameData(data);
|
||||
}
|
||||
|
||||
try {
|
||||
client.close();
|
||||
} catch (java.lang.Exception e) {
|
||||
}
|
||||
try {
|
||||
input.close();
|
||||
} catch (java.lang.Exception e) {
|
||||
}
|
||||
try {
|
||||
bos.close();
|
||||
} catch (java.lang.Exception e) {
|
||||
}
|
||||
|
||||
log.info("关闭媒体流-ffmpeg,{} ", cameraDto.getUrl());
|
||||
|
||||
} catch (SocketTimeoutException e1) {
|
||||
// e1.printStackTrace();
|
||||
// 超时关闭
|
||||
} catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
} finally {
|
||||
MediaService.cameras.remove(cameraDto.getMediaKey());
|
||||
running = false;
|
||||
process.destroy();
|
||||
try {
|
||||
if (null != client) {
|
||||
client.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
try {
|
||||
if (null != tcpServer) {
|
||||
tcpServer.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
outputThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听客户端
|
||||
*/
|
||||
public void listenClient() {
|
||||
listenThread = new Thread(new Runnable() {
|
||||
public void run() {
|
||||
while (running) {
|
||||
hasClient();
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
listenThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听网络异常超时
|
||||
*/
|
||||
public void listenNetTimeout() {
|
||||
Thread listenNetTimeoutThread = new Thread(new Runnable() {
|
||||
public void run() {
|
||||
while (true) {
|
||||
|
||||
if ((System.currentTimeMillis() - currentTimeMillis) > 15000) {
|
||||
log.info("网络异常超时");
|
||||
MediaService.cameras.remove(cameraDto.getMediaKey());
|
||||
stopFFmpeg();
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
listenNetTimeoutThread.setDaemon(true);
|
||||
listenNetTimeoutThread.start();
|
||||
}
|
||||
|
||||
public static MediaTransferFlvByFFmpeg atPath() {
|
||||
return atPath(null);
|
||||
}
|
||||
|
||||
public static MediaTransferFlvByFFmpeg atPath(final String absPath) {
|
||||
final String executable;
|
||||
if (absPath != null) {
|
||||
executable = absPath;
|
||||
} else {
|
||||
executable = System.getProperty(MediaConstant.ffmpegPathKey);
|
||||
}
|
||||
return new MediaTransferFlvByFFmpeg(executable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制台输出
|
||||
*
|
||||
* @param process
|
||||
*/
|
||||
private void dealStream(Process process) {
|
||||
if (process == null) {
|
||||
return;
|
||||
}
|
||||
// 处理InputStream的线程
|
||||
inputThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
String line = null;
|
||||
try {
|
||||
while (running) {
|
||||
line = in.readLine();
|
||||
currentTimeMillis = System.currentTimeMillis();
|
||||
if (line == null) {
|
||||
break;
|
||||
}
|
||||
if (enableLog) {
|
||||
log.info("output: " + line);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
running = false;
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// 处理ErrorStream的线程
|
||||
errThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
||||
String line = null;
|
||||
try {
|
||||
while (running) {
|
||||
line = err.readLine();
|
||||
currentTimeMillis = System.currentTimeMillis();
|
||||
if (line == null) {
|
||||
break;
|
||||
}
|
||||
if (enableLog) {
|
||||
log.info("err: " + line);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
running = false;
|
||||
err.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
inputThread.start();
|
||||
errThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出到tcp
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private String getOutput() {
|
||||
try {
|
||||
tcpServer = new ServerSocket(0, 1, InetAddress.getLoopbackAddress());
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("tcp://");
|
||||
sb.append(tcpServer.getInetAddress().getHostAddress());
|
||||
sb.append(":");
|
||||
sb.append(tcpServer.getLocalPort());
|
||||
tcpServer.setSoTimeout(10000);
|
||||
return sb.toString();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
new RuntimeException("无法启用端口");
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭
|
||||
*/
|
||||
public void stopFFmpeg() {
|
||||
this.running = false;
|
||||
try {
|
||||
this.process.destroy();
|
||||
log.info("关闭媒体流-ffmpeg,{} ", cameraDto.getUrl());
|
||||
} catch (java.lang.Exception e) {
|
||||
process.destroyForcibly();
|
||||
}
|
||||
|
||||
// 媒体异常时,主动断开前端长连接
|
||||
for (Map.Entry<String, ChannelHandlerContext> entry : wsClients.entrySet()) {
|
||||
try {
|
||||
entry.getValue().close();
|
||||
} catch (java.lang.Exception e) {
|
||||
} finally {
|
||||
wsClients.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, ChannelHandlerContext> entry : httpClients.entrySet()) {
|
||||
try {
|
||||
entry.getValue().close();
|
||||
} catch (java.lang.Exception e) {
|
||||
} finally {
|
||||
httpClients.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭流
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public void hasClient() {
|
||||
|
||||
int newHcSize = httpClients.size();
|
||||
int newWcSize = wsClients.size();
|
||||
if (hcSize != newHcSize || wcSize != newWcSize) {
|
||||
hcSize = newHcSize;
|
||||
wcSize = newWcSize;
|
||||
log.info("\r\n{}\r\nhttp连接数:{}, ws连接数:{} \r\n", cameraDto.getUrl(), newHcSize, newWcSize);
|
||||
}
|
||||
|
||||
// 无需自动关闭
|
||||
if (!cameraDto.isAutoClose()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (httpClients.isEmpty() && wsClients.isEmpty()) {
|
||||
// 等待20秒还没有客户端,则关闭推流
|
||||
if (noClient > cameraDto.getNoClientsDuration()) {
|
||||
running = false;
|
||||
MediaService.cameras.remove(cameraDto.getMediaKey());
|
||||
} else {
|
||||
noClient += 1000;
|
||||
}
|
||||
} else {
|
||||
// 重置计时
|
||||
noClient = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送帧数据
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
private void sendFrameData(byte[] data) {
|
||||
// ws
|
||||
for (Map.Entry<String, ChannelHandlerContext> entry : wsClients.entrySet()) {
|
||||
try {
|
||||
if (entry.getValue().channel().isWritable()) {
|
||||
entry.getValue().writeAndFlush(new BinaryWebSocketFrame(Unpooled.copiedBuffer(data)));
|
||||
} else {
|
||||
wsClients.remove(entry.getKey());
|
||||
hasClient();
|
||||
}
|
||||
} catch (java.lang.Exception e) {
|
||||
wsClients.remove(entry.getKey());
|
||||
hasClient();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
// http
|
||||
for (Map.Entry<String, ChannelHandlerContext> entry : httpClients.entrySet()) {
|
||||
try {
|
||||
if (entry.getValue().channel().isWritable()) {
|
||||
entry.getValue().writeAndFlush(Unpooled.copiedBuffer(data));
|
||||
} else {
|
||||
httpClients.remove(entry.getKey());
|
||||
hasClient();
|
||||
}
|
||||
} catch (java.lang.Exception e) {
|
||||
httpClients.remove(entry.getKey());
|
||||
hasClient();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增客户端
|
||||
*
|
||||
* @param ctx netty client
|
||||
* @param ctype enum,ClientType
|
||||
*/
|
||||
public void addClient(ChannelHandlerContext ctx, ClientType ctype) {
|
||||
int timeout = 0;
|
||||
while (true) {
|
||||
try {
|
||||
if (header != null) {
|
||||
try {
|
||||
if (ctx.channel().isWritable()) {
|
||||
// 发送帧前先发送header
|
||||
if (ClientType.HTTP.getType() == ctype.getType()) {
|
||||
ChannelFuture future = ctx.writeAndFlush(Unpooled.copiedBuffer(header));
|
||||
future.addListener(new GenericFutureListener<Future<? super Void>>() {
|
||||
@Override
|
||||
public void operationComplete(Future<? super Void> future) throws FrameGrabber.Exception {
|
||||
if (future.isSuccess()) {
|
||||
httpClients.put(ctx.channel().id().toString(), ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (ClientType.WEBSOCKET.getType() == ctype.getType()) {
|
||||
ChannelFuture future = ctx
|
||||
.writeAndFlush(new BinaryWebSocketFrame(Unpooled.copiedBuffer(header)));
|
||||
future.addListener(new GenericFutureListener<Future<? super Void>>() {
|
||||
@Override
|
||||
public void operationComplete(Future<? super Void> future) throws FrameGrabber.Exception {
|
||||
if (future.isSuccess()) {
|
||||
wsClients.put(ctx.channel().id().toString(), ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (java.lang.Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 等待推拉流启动
|
||||
Thread.sleep(50);
|
||||
// 启动录制器失败
|
||||
timeout += 50;
|
||||
if (timeout > 30000) {
|
||||
break;
|
||||
}
|
||||
} catch (java.lang.Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,576 @@
|
||||
package com.ruoyi.video.thread;
|
||||
|
||||
import com.arcsoft.face.toolkit.ImageInfo;
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
import com.ruoyi.video.common.ClientType;
|
||||
import com.ruoyi.video.domain.dto.CameraDto;
|
||||
import com.ruoyi.video.service.MediaService;
|
||||
import com.ruoyi.video.utils.ArcFaceEngineUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
import io.netty.util.concurrent.Future;
|
||||
import io.netty.util.concurrent.GenericFutureListener;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.FFmpegLogCallback;
|
||||
import org.bytedeco.javacv.Frame;
|
||||
import org.bytedeco.javacv.FrameGrabber;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
/**
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
@Slf4j
|
||||
public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable {
|
||||
|
||||
static {
|
||||
avutil.av_log_set_level(avutil.AV_LOG_ERROR);
|
||||
FFmpegLogCallback.set();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ws客户端
|
||||
*/
|
||||
private ConcurrentHashMap<String, ChannelHandlerContext> wsClients = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* http客户端
|
||||
*/
|
||||
private ConcurrentHashMap<String, ChannelHandlerContext> httpClients = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 运行状态
|
||||
*/
|
||||
private volatile boolean running = false;
|
||||
|
||||
private boolean grabberStatus = false;
|
||||
|
||||
private boolean recorderStatus = false;
|
||||
|
||||
/**
|
||||
* 当前在线人数
|
||||
*/
|
||||
private int hcSize, wcSize = 0;
|
||||
|
||||
/**
|
||||
* 用于没有客户端时候的计时
|
||||
*/
|
||||
private int noClient = 0;
|
||||
|
||||
/**
|
||||
* flv header
|
||||
*/
|
||||
private byte[] header = null;
|
||||
// 输出流,视频最终会输出到此
|
||||
private ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
|
||||
FFmpegFrameGrabber grabber;// 拉流器
|
||||
FFmpegFrameRecorder recorder;// 推流录制器
|
||||
|
||||
/**
|
||||
* true:转复用,false:转码
|
||||
*/
|
||||
boolean transferFlag = false;// 默认转码
|
||||
|
||||
/**
|
||||
* 相机
|
||||
*/
|
||||
private CameraDto cameraDto;
|
||||
|
||||
/**
|
||||
* 监听线程,用于监听状态
|
||||
*/
|
||||
private Thread listenThread;
|
||||
|
||||
private ArcFaceEngineUtil arcFaceEngineUtil;
|
||||
|
||||
public MediaTransferFlvByJavacv(CameraDto cameraDto) {
|
||||
super();
|
||||
this.cameraDto = cameraDto;
|
||||
this.arcFaceEngineUtil = SpringUtils.getBean(ArcFaceEngineUtil.class);
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public void setRunning(boolean running) {
|
||||
this.running = running;
|
||||
}
|
||||
|
||||
public boolean isGrabberStatus() {
|
||||
return grabberStatus;
|
||||
}
|
||||
|
||||
public void setGrabberStatus(boolean grabberStatus) {
|
||||
this.grabberStatus = grabberStatus;
|
||||
}
|
||||
|
||||
public boolean isRecorderStatus() {
|
||||
return recorderStatus;
|
||||
}
|
||||
|
||||
public void setRecorderStatus(boolean recorderStatus) {
|
||||
this.recorderStatus = recorderStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建拉流器
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
protected boolean createGrabber() {
|
||||
// 拉流器
|
||||
grabber = new FFmpegFrameGrabber(cameraDto.getUrl());
|
||||
// 超时时间(15秒)
|
||||
grabber.setOption("stimeout", cameraDto.getNetTimeout());
|
||||
grabber.setOption("threads", "1");
|
||||
// grabber.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
|
||||
// 设置缓存大小,提高画质、减少卡顿花屏
|
||||
grabber.setOption("buffer_size", "1024000");
|
||||
|
||||
// 读写超时,适用于所有协议的通用读写超时
|
||||
grabber.setOption("rw_timeout", cameraDto.getReadOrWriteTimeout());
|
||||
// 探测视频流信息,为空默认5000000微秒
|
||||
grabber.setOption("probesize", cameraDto.getReadOrWriteTimeout());
|
||||
// 解析视频流信息,为空默认5000000微秒
|
||||
grabber.setOption("analyzeduration", cameraDto.getReadOrWriteTimeout());
|
||||
|
||||
// 如果为rtsp流,增加配置
|
||||
if ("rtsp".equals(cameraDto.getUrl().substring(0, 4))) {
|
||||
// 设置打开协议tcp / udp
|
||||
grabber.setOption("rtsp_transport", "tcp");
|
||||
// 首选TCP进行RTP传输
|
||||
grabber.setOption("rtsp_flags", "prefer_tcp");
|
||||
|
||||
} else if ("rtmp".equals(cameraDto.getUrl().substring(0, 4))) {
|
||||
// rtmp拉流缓冲区,默认3000毫秒
|
||||
grabber.setOption("rtmp_buffer", "1000");
|
||||
// 默认rtmp流为直播模式,不允许seek
|
||||
// grabber.setOption("rtmp_live", "live");
|
||||
|
||||
} else if ("desktop".equals(cameraDto.getUrl())) {
|
||||
// 支持本地屏幕采集,可以用于监控屏幕、局域网和wifi投屏等
|
||||
grabber.setFormat("gdigrab");
|
||||
grabber.setOption("draw_mouse", "1");// 绘制鼠标
|
||||
grabber.setNumBuffers(0);
|
||||
grabber.setOption("fflags", "nobuffer");
|
||||
grabber.setOption("framerate", "25");
|
||||
grabber.setFrameRate(25);
|
||||
}
|
||||
|
||||
try {
|
||||
grabber.start();
|
||||
log.info("\r\n{}\r\n启动拉流器成功", cameraDto.getUrl());
|
||||
return grabberStatus = true;
|
||||
} catch (FrameGrabber.Exception e) {
|
||||
MediaService.cameras.remove(cameraDto.getMediaKey());
|
||||
log.error("\r\n{}\r\n启动拉流器失败,网络超时或视频源不可用", cameraDto.getUrl());
|
||||
// e.printStackTrace();
|
||||
}
|
||||
return grabberStatus = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建转码推流录制器
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
protected boolean createTransterOrRecodeRecorder() {
|
||||
recorder = new FFmpegFrameRecorder(bos, grabber.getImageWidth(), grabber.getImageHeight(),
|
||||
grabber.getAudioChannels());
|
||||
recorder.setFormat("flv");
|
||||
if (!transferFlag) {
|
||||
// 转码
|
||||
recorder.setInterleaved(false);
|
||||
recorder.setVideoOption("tune", "zerolatency");
|
||||
recorder.setVideoOption("preset", "ultrafast");
|
||||
recorder.setVideoOption("crf", "26");
|
||||
recorder.setVideoOption("threads", "1");
|
||||
recorder.setFrameRate(25);// 设置帧率
|
||||
recorder.setGopSize(25);// 设置gop,与帧率相同,相当于间隔1秒chan's一个关键帧
|
||||
// recorder.setVideoBitrate(500 * 1000);// 码率500kb/s
|
||||
// recorder.setVideoCodecName("libx264"); //javacv 1.5.5无法使用libx264名称,请使用下面方法
|
||||
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
|
||||
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
|
||||
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
|
||||
// recorder.setAudioCodecName("aac");
|
||||
/**
|
||||
* 启用RDOQ算法,优化视频质量 1:在视频码率和视频质量之间取得平衡 2:最大程度优化视频质量(会降低编码速度和提高码率)
|
||||
*/
|
||||
recorder.setTrellis(1);
|
||||
recorder.setMaxDelay(0);// 设置延迟
|
||||
try {
|
||||
recorder.start();
|
||||
return recorderStatus = true;
|
||||
} catch (org.bytedeco.javacv.FrameRecorder.Exception e1) {
|
||||
log.info("启动转码录制器失败", e1);
|
||||
MediaService.cameras.remove(cameraDto.getMediaKey());
|
||||
e1.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
// 转复用
|
||||
// 不让recorder关联关闭outputStream
|
||||
recorder.setCloseOutputStream(false);
|
||||
try {
|
||||
recorder.start(grabber.getFormatContext());
|
||||
return recorderStatus = true;
|
||||
} catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
|
||||
log.warn("\r\n{}\r\n启动转复用录制器失败", cameraDto.getUrl());
|
||||
// 如果转复用失败,则自动切换到转码模式
|
||||
transferFlag = false;
|
||||
if (recorder != null) {
|
||||
try {
|
||||
recorder.stop();
|
||||
} catch (org.bytedeco.javacv.FrameRecorder.Exception e1) {
|
||||
}
|
||||
}
|
||||
if (createTransterOrRecodeRecorder()) {
|
||||
log.error("\r\n{}\r\n切换到转码模式", cameraDto.getUrl());
|
||||
return true;
|
||||
}
|
||||
log.error("\r\n{}\r\n切换转码模式失败", cameraDto.getUrl());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return recorderStatus = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否支持flv的音视频编码
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private boolean supportFlvFormatCodec() {
|
||||
int vcodec = grabber.getVideoCodec();
|
||||
int acodec = grabber.getAudioCodec();
|
||||
return (cameraDto.getType() == 0)
|
||||
&& ("desktop".equals(cameraDto.getUrl()) || avcodec.AV_CODEC_ID_H264 == vcodec
|
||||
|| avcodec.AV_CODEC_ID_H263 == vcodec)
|
||||
&& (avcodec.AV_CODEC_ID_AAC == acodec || avcodec.AV_CODEC_ID_AAC_LATM == acodec);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将视频源转换为flv
|
||||
*/
|
||||
protected void transferStream2Flv() {
|
||||
if (!createGrabber()) {
|
||||
return;
|
||||
}
|
||||
transferFlag = supportFlvFormatCodec();
|
||||
if (!createTransterOrRecodeRecorder()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
grabber.flush();
|
||||
} catch (FrameGrabber.Exception e) {
|
||||
log.info("清空拉流器缓存失败", e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (header == null) {
|
||||
header = bos.toByteArray();
|
||||
// System.out.println(HexUtil.encodeHexStr(header));
|
||||
bos.reset();
|
||||
}
|
||||
|
||||
running = true;
|
||||
|
||||
// 启动监听线程(用于判断是否需要自动关闭推流)
|
||||
listenClient();
|
||||
|
||||
// 时间戳计算
|
||||
long startTime = 0;
|
||||
long videoTS = 0;
|
||||
|
||||
for (; running && grabberStatus && recorderStatus; ) {
|
||||
|
||||
try {
|
||||
if (transferFlag) {
|
||||
// 转复用
|
||||
long startGrab = System.currentTimeMillis();
|
||||
AVPacket pkt = grabber.grabPacket();
|
||||
if ((System.currentTimeMillis() - startGrab) > 5000) {
|
||||
log.info("\r\n{}\r\n视频流网络异常>>>", cameraDto.getUrl());
|
||||
closeMedia();
|
||||
break;
|
||||
}
|
||||
if (null != pkt && !pkt.isNull()) {
|
||||
if (startTime == 0) {
|
||||
startTime = System.currentTimeMillis();
|
||||
}
|
||||
videoTS = 1000 * (System.currentTimeMillis() - startTime);
|
||||
// 判断时间偏移
|
||||
if (videoTS > recorder.getTimestamp()) {
|
||||
recorder.setTimestamp((videoTS));
|
||||
}
|
||||
recorder.recordPacket(pkt);
|
||||
}
|
||||
} else {
|
||||
// 转码
|
||||
long startGrab = System.currentTimeMillis();
|
||||
Frame frame = grabber.grab();
|
||||
if ((System.currentTimeMillis() - startGrab) > 5000) {
|
||||
log.info("\r\n{}\r\n视频流网络异常>>>", cameraDto.getUrl());
|
||||
closeMedia();
|
||||
break;
|
||||
}
|
||||
|
||||
if (frame != null) {
|
||||
|
||||
processFaceDetection(frame);
|
||||
|
||||
if (startTime == 0) {
|
||||
startTime = System.currentTimeMillis();
|
||||
}
|
||||
videoTS = 1000 * (System.currentTimeMillis() - startTime);
|
||||
// 判断时间偏移
|
||||
if (videoTS > recorder.getTimestamp()) {
|
||||
// System.out.println("矫正时间戳: " + videoTS + " : " + recorder.getTimestamp() + "
|
||||
// -> "
|
||||
// + (videoTS - recorder.getTimestamp()));
|
||||
recorder.setTimestamp((videoTS));
|
||||
}
|
||||
recorder.record(frame);
|
||||
}
|
||||
}
|
||||
} catch (FrameGrabber.Exception e) {
|
||||
grabberStatus = false;
|
||||
MediaService.cameras.remove(cameraDto.getMediaKey());
|
||||
} catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
|
||||
recorderStatus = false;
|
||||
MediaService.cameras.remove(cameraDto.getMediaKey());
|
||||
}
|
||||
|
||||
if (bos.size() > 0) {
|
||||
byte[] b = bos.toByteArray();
|
||||
bos.reset();
|
||||
|
||||
// 发送视频到前端
|
||||
sendFrameData(b);
|
||||
}
|
||||
}
|
||||
|
||||
// 启动失败,直接关闭, close包含stop和release方法。录制文件必须保证最后执行stop()方法
|
||||
try {
|
||||
recorder.close();
|
||||
grabber.close();
|
||||
bos.close();
|
||||
} catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
|
||||
e.printStackTrace();
|
||||
} catch (FrameGrabber.Exception e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
closeMedia();
|
||||
}
|
||||
log.info("关闭媒体流-javacv,{} ", cameraDto.getUrl());
|
||||
}
|
||||
|
||||
@Async
|
||||
public void processFaceDetection(Frame frame) {
|
||||
if (arcFaceEngineUtil != null) {
|
||||
ImageInfo imageInfo = arcFaceEngineUtil.frameToImageInfo(frame);
|
||||
if (imageInfo != null) {
|
||||
arcFaceEngineUtil.getUserInfo(imageInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送帧数据
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
private void sendFrameData(byte[] data) {
|
||||
// ws
|
||||
for (Map.Entry<String, ChannelHandlerContext> entry : wsClients.entrySet()) {
|
||||
try {
|
||||
if (entry.getValue().channel().isWritable()) {
|
||||
entry.getValue().writeAndFlush(new BinaryWebSocketFrame(Unpooled.copiedBuffer(data)));
|
||||
} else {
|
||||
wsClients.remove(entry.getKey());
|
||||
hasClient();
|
||||
}
|
||||
} catch (java.lang.Exception e) {
|
||||
wsClients.remove(entry.getKey());
|
||||
hasClient();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
// http
|
||||
for (Map.Entry<String, ChannelHandlerContext> entry : httpClients.entrySet()) {
|
||||
try {
|
||||
if (entry.getValue().channel().isWritable()) {
|
||||
entry.getValue().writeAndFlush(Unpooled.copiedBuffer(data));
|
||||
} else {
|
||||
httpClients.remove(entry.getKey());
|
||||
hasClient();
|
||||
}
|
||||
} catch (java.lang.Exception e) {
|
||||
httpClients.remove(entry.getKey());
|
||||
hasClient();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断有没有客户端,关闭流
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public void hasClient() {
|
||||
|
||||
int newHcSize = httpClients.size();
|
||||
int newWcSize = wsClients.size();
|
||||
if (hcSize != newHcSize || wcSize != newWcSize) {
|
||||
hcSize = newHcSize;
|
||||
wcSize = newWcSize;
|
||||
log.info("\r\n{}\r\nhttp连接数:{}, ws连接数:{} \r\n", cameraDto.getUrl(), newHcSize, newWcSize);
|
||||
}
|
||||
|
||||
// 无需自动关闭
|
||||
if (!cameraDto.isAutoClose()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (httpClients.isEmpty() && wsClients.isEmpty()) {
|
||||
// 等待20秒还没有客户端,则关闭推流
|
||||
if (noClient > cameraDto.getNoClientsDuration()) {
|
||||
closeMedia();
|
||||
} else {
|
||||
noClient += 1000;
|
||||
// log.info("\r\n{}\r\n {} 秒自动关闭推拉流 \r\n", camera.getUrl(), noClientsDuration-noClient);
|
||||
}
|
||||
} else {
|
||||
// 重置计时
|
||||
noClient = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听客户端,用于判断无人观看时自动关闭推流
|
||||
*/
|
||||
public void listenClient() {
|
||||
listenThread = new Thread(new Runnable() {
|
||||
public void run() {
|
||||
while (running) {
|
||||
hasClient();
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
listenThread.start();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 关闭流媒体
|
||||
*/
|
||||
private void closeMedia() {
|
||||
running = false;
|
||||
MediaService.cameras.remove(cameraDto.getMediaKey());
|
||||
|
||||
// 媒体异常时,主动断开前端长连接
|
||||
for (Map.Entry<String, ChannelHandlerContext> entry : wsClients.entrySet()) {
|
||||
try {
|
||||
entry.getValue().close();
|
||||
} catch (java.lang.Exception e) {
|
||||
} finally {
|
||||
wsClients.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, ChannelHandlerContext> entry : httpClients.entrySet()) {
|
||||
try {
|
||||
entry.getValue().close();
|
||||
} catch (java.lang.Exception e) {
|
||||
} finally {
|
||||
httpClients.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增客户端
|
||||
*
|
||||
* @param ctx netty client
|
||||
* @param ctype enum,ClientType
|
||||
*/
|
||||
public void addClient(ChannelHandlerContext ctx, ClientType ctype) {
|
||||
int timeout = 0;
|
||||
while (true) {
|
||||
try {
|
||||
if (header != null) {
|
||||
try {
|
||||
if (ctx.channel().isWritable()) {
|
||||
// 发送帧前先发送header
|
||||
if (ClientType.HTTP.getType() == ctype.getType()) {
|
||||
ChannelFuture future = ctx.writeAndFlush(Unpooled.copiedBuffer(header));
|
||||
future.addListener(new GenericFutureListener<Future<? super Void>>() {
|
||||
@Override
|
||||
public void operationComplete(Future<? super Void> future) throws FrameGrabber.Exception {
|
||||
if (future.isSuccess()) {
|
||||
httpClients.put(ctx.channel().id().toString(), ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (ClientType.WEBSOCKET.getType() == ctype.getType()) {
|
||||
ChannelFuture future = ctx
|
||||
.writeAndFlush(new BinaryWebSocketFrame(Unpooled.copiedBuffer(header)));
|
||||
future.addListener(new GenericFutureListener<Future<? super Void>>() {
|
||||
@Override
|
||||
public void operationComplete(Future<? super Void> future) throws FrameGrabber.Exception {
|
||||
if (future.isSuccess()) {
|
||||
wsClients.put(ctx.channel().id().toString(), ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (java.lang.Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 等待推拉流启动
|
||||
Thread.sleep(50);
|
||||
// 启动录制器失败
|
||||
timeout += 50;
|
||||
if (timeout > 30000) {
|
||||
break;
|
||||
}
|
||||
} catch (java.lang.Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
transferStream2Flv();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.ruoyi.video.thread;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.ruoyi.video.common.MediaConstant;
|
||||
import com.ruoyi.video.domain.dto.CameraDto;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* hls切片
|
||||
*/
|
||||
@Slf4j
|
||||
public class MediaTransferHls extends MediaTransfer {
|
||||
|
||||
/**
|
||||
* 运行状态
|
||||
*/
|
||||
private boolean running = false;
|
||||
|
||||
private boolean enableLog = false;
|
||||
private Process process;
|
||||
private Thread inputThread;
|
||||
private Thread errThread;
|
||||
|
||||
private int port = 8888;
|
||||
|
||||
/**
|
||||
* 相机
|
||||
*/
|
||||
private CameraDto cameraDto;
|
||||
|
||||
/**
|
||||
* cmd
|
||||
*/
|
||||
private List<String> command = new ArrayList<>();
|
||||
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public void setRunning(boolean running) {
|
||||
this.running = running;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param cameraDto
|
||||
*/
|
||||
public MediaTransferHls(CameraDto cameraDto, int port) {
|
||||
this.cameraDto = cameraDto;
|
||||
this.port = port;
|
||||
buildCommand();
|
||||
}
|
||||
|
||||
/**
|
||||
* String cmd = "ffmpeg -i rtsp://admin:VZCDOY@192.168.2.120:554/Streaming/Channels/102 -r 25 -g 25 -c:v libx264 -c:a aac -f hls -hls_list_size 1 -hls_wrap 6 -hls_time 1 -hls_base_url /ts/"+22+"/ -method put http://localhost:8888/record/"+22+"/out.m3u8";
|
||||
*/
|
||||
private void buildCommand() {
|
||||
|
||||
command.add(System.getProperty(MediaConstant.ffmpegPathKey));
|
||||
command.add("-i");
|
||||
command.add(cameraDto.getUrl());
|
||||
command.add("-r");
|
||||
command.add("25");
|
||||
command.add("-g");
|
||||
command.add("25");
|
||||
command.add("-c:v");
|
||||
command.add("h264"); //javacv 1.5.5 无法使用libx264
|
||||
command.add("-c:a");
|
||||
command.add("aac");
|
||||
command.add("-f");
|
||||
command.add("hls");
|
||||
command.add("-hls_list_size");
|
||||
command.add("1");
|
||||
command.add("-hls_wrap");
|
||||
command.add("6");
|
||||
command.add("-hls_time");
|
||||
command.add("1");
|
||||
command.add("-hls_base_url");
|
||||
command.add("/ts/"+cameraDto.getMediaKey()+"/");
|
||||
command.add("-method");
|
||||
command.add("put");
|
||||
command.add("http://localhost:"+port+"/record/"+cameraDto.getMediaKey()+"/out.m3u8");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行
|
||||
*/
|
||||
public void execute() {
|
||||
String join = CollUtil.join(command, " ");
|
||||
log.info(join);
|
||||
|
||||
try {
|
||||
process = new ProcessBuilder(command).start();
|
||||
running = true;
|
||||
dealStream(process);
|
||||
} catch (IOException e) {
|
||||
running = false;
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭
|
||||
*/
|
||||
public void stop() {
|
||||
this.running = false;
|
||||
try {
|
||||
process.destroy();
|
||||
log.info("关闭媒体流-ffmpeg,{} ", cameraDto.getUrl());
|
||||
} catch (Exception e) {
|
||||
process.destroyForcibly();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 控制台输出
|
||||
*
|
||||
* @param process
|
||||
*/
|
||||
private void dealStream(Process process) {
|
||||
if (process == null) {
|
||||
return;
|
||||
}
|
||||
// 处理InputStream的线程
|
||||
inputThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
String line = null;
|
||||
try {
|
||||
while (running) {
|
||||
line = in.readLine();
|
||||
if (line == null) {
|
||||
break;
|
||||
}
|
||||
if (enableLog) {
|
||||
log.info("output: " + line);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
running = false;
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// 处理ErrorStream的线程
|
||||
errThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
||||
String line = null;
|
||||
try {
|
||||
while (running) {
|
||||
line = err.readLine();
|
||||
if (line == null) {
|
||||
break;
|
||||
}
|
||||
if (enableLog) {
|
||||
log.info("err: " + line);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
running = false;
|
||||
err.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
inputThread.start();
|
||||
errThread.start();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package com.ruoyi.video.utils;
|
||||
|
||||
import com.arcsoft.face.*;
|
||||
import com.arcsoft.face.enums.DetectMode;
|
||||
import com.arcsoft.face.enums.DetectOrient;
|
||||
import com.arcsoft.face.enums.ErrorInfo;
|
||||
import com.arcsoft.face.enums.ImageFormat;
|
||||
import com.arcsoft.face.toolkit.ImageFactory;
|
||||
import com.arcsoft.face.toolkit.ImageInfo;
|
||||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.utils.file.FileUploadUtils;
|
||||
import com.ruoyi.video.domain.VImage;
|
||||
import com.ruoyi.video.service.IVImageService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bytedeco.javacv.Frame;
|
||||
import org.bytedeco.javacv.Java2DFrameConverter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ArcFaceEngineUtil {
|
||||
|
||||
@Value("${arcFace.appId}")
|
||||
public String appId;
|
||||
|
||||
@Value("${arcFace.sdkKey}")
|
||||
public String sdkKey;
|
||||
|
||||
int errorCode;
|
||||
|
||||
String projectPath = System.getProperty("user.dir");
|
||||
|
||||
@Autowired
|
||||
private IVImageService ivImageService;
|
||||
|
||||
FaceEngine faceEngine = new FaceEngine(projectPath+"\\ruoyi-video\\src\\main\\resources\\libs\\WIN64");
|
||||
|
||||
private final AtomicLong lastSaveTime = new AtomicLong(0); // 记录上次保存的时间戳
|
||||
|
||||
private static final long SAVE_INTERVAL = 10 * 1000; // 10 秒的间隔
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.errorCode = this.faceEngine.activeOnline(this.appId, this.sdkKey);
|
||||
if (this.errorCode != ErrorInfo.MOK.getValue() && this.errorCode != ErrorInfo.MERR_ASF_ALREADY_ACTIVATED.getValue()) {
|
||||
log.error("引擎激活失败");
|
||||
}
|
||||
|
||||
ActiveFileInfo activeFileInfo = new ActiveFileInfo();
|
||||
this.errorCode = this.faceEngine.getActiveFileInfo(activeFileInfo);
|
||||
if (this.errorCode != ErrorInfo.MOK.getValue() && this.errorCode != ErrorInfo.MERR_ASF_ALREADY_ACTIVATED.getValue()) {
|
||||
log.error("获取激活文件信息失败");
|
||||
}
|
||||
|
||||
EngineConfiguration engineConfiguration = new EngineConfiguration();
|
||||
engineConfiguration.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);
|
||||
engineConfiguration.setDetectFaceOrientPriority(DetectOrient.ASF_OP_ALL_OUT);
|
||||
engineConfiguration.setDetectFaceMaxNum(10);
|
||||
engineConfiguration.setDetectFaceScaleVal(16);
|
||||
FunctionConfiguration functionConfiguration = new FunctionConfiguration();
|
||||
functionConfiguration.setSupportAge(true);
|
||||
functionConfiguration.setSupportFace3dAngle(true);
|
||||
functionConfiguration.setSupportFaceDetect(true);
|
||||
functionConfiguration.setSupportFaceRecognition(true);
|
||||
functionConfiguration.setSupportGender(true);
|
||||
functionConfiguration.setSupportLiveness(true);
|
||||
functionConfiguration.setSupportIRLiveness(true);
|
||||
engineConfiguration.setFunctionConfiguration(functionConfiguration);
|
||||
this.errorCode = this.faceEngine.init(engineConfiguration);
|
||||
if (this.errorCode != ErrorInfo.MOK.getValue()) {
|
||||
log.error("初始化引擎失败");
|
||||
}
|
||||
|
||||
log.info("------------------------");
|
||||
log.info("ruoyi-video初始化引擎成功");
|
||||
log.info("微信: chenbai0511");
|
||||
log.info("请我喝杯咖啡吧qwq");
|
||||
log.info("------------------------");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Frame对象转换为ImageInfo对象
|
||||
* @param frame
|
||||
* @return
|
||||
*/
|
||||
public ImageInfo frameToImageInfo(Frame frame) {
|
||||
if (frame == null || frame.image == null) {
|
||||
log.error("Frame是空或非图像帧------");
|
||||
return null;
|
||||
} else {
|
||||
Java2DFrameConverter converter = new Java2DFrameConverter();
|
||||
BufferedImage image = converter.convert(frame);
|
||||
if (image == null) {
|
||||
log.error("Converted image is null");
|
||||
return null;
|
||||
} else {
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
byte[] imageData = ((DataBufferByte)image.getRaster().getDataBuffer()).getData();
|
||||
ImageInfo imageInfo = new ImageInfo();
|
||||
imageInfo.setWidth(width);
|
||||
imageInfo.setHeight(height);
|
||||
imageInfo.setImageFormat(ImageFormat.CP_PAF_BGR24);
|
||||
imageInfo.setImageData(imageData);
|
||||
return imageInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 虹软检测代码
|
||||
* @param imageInfo
|
||||
* @return
|
||||
*/
|
||||
public HashMap<String, Object> getUserInfo(ImageInfo imageInfo) {
|
||||
ImageInfo imageInfoFc = ImageFactory.getRGBData(new File("D:\\cat\\fc.jpg"));
|
||||
List<FaceInfo> faceInfoListFc = new ArrayList();
|
||||
this.faceEngine.detectFaces(imageInfoFc.getImageData(), imageInfoFc.getWidth(), imageInfoFc.getHeight(), imageInfoFc.getImageFormat(), faceInfoListFc);
|
||||
if (faceInfoListFc.isEmpty()) {
|
||||
log.error("未检测到人脸(faceInfoListFc)");
|
||||
return null;
|
||||
} else {
|
||||
FaceFeature faceFeatureFc = new FaceFeature();
|
||||
this.faceEngine.extractFaceFeature(imageInfoFc.getImageData(), imageInfoFc.getWidth(), imageInfoFc.getHeight(), imageInfoFc.getImageFormat(), (FaceInfo)faceInfoListFc.get(0), faceFeatureFc);
|
||||
List<FaceInfo> faceInfoList = new ArrayList();
|
||||
this.errorCode = this.faceEngine.detectFaces(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(), imageInfo.getImageFormat(), faceInfoList);
|
||||
if (faceInfoList.isEmpty()) {
|
||||
log.error("未检测到人脸(faceInfoList)");
|
||||
return null;
|
||||
} else {
|
||||
FaceFeature faceFeature = new FaceFeature();
|
||||
this.faceEngine.extractFaceFeature(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(), imageInfo.getImageFormat(), (FaceInfo)faceInfoList.get(0), faceFeature);
|
||||
FaceFeature targetFaceFeature = new FaceFeature();
|
||||
targetFaceFeature.setFeatureData(faceFeatureFc.getFeatureData());
|
||||
FaceFeature sourceFaceFeature = new FaceFeature();
|
||||
sourceFaceFeature.setFeatureData(faceFeature.getFeatureData());
|
||||
FaceSimilar faceSimilar = new FaceSimilar();
|
||||
this.faceEngine.compareFaceFeature(targetFaceFeature, sourceFaceFeature, faceSimilar);
|
||||
System.out.println("相似度:" + faceSimilar.getScore());
|
||||
if (faceSimilar.getScore() < 0.8F) {
|
||||
this.saveImage(imageInfo);
|
||||
}
|
||||
|
||||
this.errorCode = this.faceEngine.setLivenessParam(0.5F, 0.7F);
|
||||
FunctionConfiguration configuration = new FunctionConfiguration();
|
||||
configuration.setSupportAge(true);
|
||||
configuration.setSupportFace3dAngle(true);
|
||||
configuration.setSupportGender(true);
|
||||
configuration.setSupportLiveness(true);
|
||||
this.errorCode = this.faceEngine.process(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(), imageInfo.getImageFormat(), faceInfoList, configuration);
|
||||
HashMap<String, Object> map = new HashMap();
|
||||
List<GenderInfo> genderInfoList = new ArrayList();
|
||||
this.errorCode = this.faceEngine.getGender(genderInfoList);
|
||||
if (!genderInfoList.isEmpty()) {
|
||||
System.out.println("性别:" + ((GenderInfo)genderInfoList.get(0)).getGender());
|
||||
map.put("gender", ((GenderInfo)genderInfoList.get(0)).getGender());
|
||||
} else {
|
||||
log.error("未获取到性别信息");
|
||||
}
|
||||
|
||||
List<AgeInfo> ageInfoList = new ArrayList();
|
||||
this.errorCode = this.faceEngine.getAge(ageInfoList);
|
||||
if (!ageInfoList.isEmpty()) {
|
||||
System.out.println("年龄:" + ((AgeInfo)ageInfoList.get(0)).getAge());
|
||||
map.put("age", ((AgeInfo)ageInfoList.get(0)).getAge());
|
||||
} else {
|
||||
log.error("未获取到年龄信息");
|
||||
}
|
||||
|
||||
List<Face3DAngle> face3DAngleList = new ArrayList();
|
||||
this.errorCode = this.faceEngine.getFace3DAngle(face3DAngleList);
|
||||
if (!face3DAngleList.isEmpty()) {
|
||||
PrintStream var10000 = System.out;
|
||||
float var10001 = ((Face3DAngle)face3DAngleList.get(0)).getPitch();
|
||||
var10000.println("3D角度:" + var10001 + "," + ((Face3DAngle)face3DAngleList.get(0)).getRoll() + "," + ((Face3DAngle)face3DAngleList.get(0)).getYaw());
|
||||
map.put("3DAngle", face3DAngleList.get(0));
|
||||
} else {
|
||||
log.error("未获取到3D角度信息");
|
||||
}
|
||||
|
||||
List<LivenessInfo> livenessInfoList = new ArrayList();
|
||||
this.errorCode = this.faceEngine.getLiveness(livenessInfoList);
|
||||
if (!livenessInfoList.isEmpty()) {
|
||||
System.out.println("活体:" + ((LivenessInfo)livenessInfoList.get(0)).getLiveness());
|
||||
map.put("liveness", ((LivenessInfo)livenessInfoList.get(0)).getLiveness());
|
||||
} else {
|
||||
log.error("未获取到活体信息");
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图片
|
||||
* @param imageInfo
|
||||
*/
|
||||
private void saveImage(ImageInfo imageInfo) {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long lastTime = lastSaveTime.get();
|
||||
|
||||
if (currentTime - lastTime < SAVE_INTERVAL) {
|
||||
log.info("10 秒内已保存过照片,跳过本次保存");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lastSaveTime.compareAndSet(lastTime, currentTime)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
File dir = new File("D:\\cat");
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
}
|
||||
String fileName = "image_" + currentTime + ".jpg";
|
||||
File outputFile = new File(dir, fileName);
|
||||
BufferedImage bufferedImage = this.convertImageInfoToBufferedImage(imageInfo);
|
||||
if (bufferedImage != null) {
|
||||
ImageIO.write(bufferedImage, "jpg", outputFile);
|
||||
log.info("图片已保存:{}", outputFile.getAbsolutePath());
|
||||
String filePath = RuoYiConfig.getUploadPath();
|
||||
MultipartFile multipartFile = new CustomMultipartFile(outputFile, "image/jpeg");
|
||||
String file_name = FileUploadUtils.upload(filePath, multipartFile);
|
||||
VImage vImage = new VImage();
|
||||
vImage.setImageName(file_name);
|
||||
vImage.setImageData(fileName);
|
||||
this.ivImageService.insertVImage(vImage);
|
||||
} else {
|
||||
log.error("无法转换 ImageInfo 为 BufferedImage");
|
||||
}
|
||||
} catch (IOException var16) {
|
||||
log.error("保存图片失败:{}", var16.getMessage(), var16);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将ImageInfo转换为BufferedImage
|
||||
* @param imageInfo
|
||||
* @return
|
||||
*/
|
||||
private BufferedImage convertImageInfoToBufferedImage(ImageInfo imageInfo) {
|
||||
if (imageInfo != null && imageInfo.getImageData() != null) {
|
||||
if (imageInfo.getImageFormat() == ImageFormat.CP_PAF_BGR24) {
|
||||
int width = imageInfo.getWidth();
|
||||
int height = imageInfo.getHeight();
|
||||
byte[] imageData = imageInfo.getImageData();
|
||||
BufferedImage bufferedImage = new BufferedImage(width, height, 5);
|
||||
byte[] targetPixels = ((DataBufferByte)bufferedImage.getRaster().getDataBuffer()).getData();
|
||||
System.arraycopy(imageData, 0, targetPixels, 0, imageData.length);
|
||||
return bufferedImage;
|
||||
} else {
|
||||
log.error("不支持的图像格式:{}", imageInfo.getImageFormat());
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
log.error("ImageInfo 或 imageData 为空");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.ruoyi.video.utils;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* File包装成MultipartFile
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
public class CustomMultipartFile implements MultipartFile {
|
||||
// 定义一个File类型的变量file
|
||||
private final File file;
|
||||
// 定义一个String类型的变量contentType
|
||||
private final String contentType;
|
||||
|
||||
// 构造方法,传入一个File类型的变量file和一个String类型的变量contentType
|
||||
public CustomMultipartFile(File file, String contentType) {
|
||||
this.file = file;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
public String getName() {
|
||||
return this.file.getName();
|
||||
}
|
||||
|
||||
// 获取原始文件名
|
||||
public String getOriginalFilename() {
|
||||
return this.file.getName();
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
public String getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
// 判断文件是否为空
|
||||
public boolean isEmpty() {
|
||||
return this.file.length() == 0L;
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
public long getSize() {
|
||||
return this.file.length();
|
||||
}
|
||||
|
||||
// 获取文件的字节数组
|
||||
public byte[] getBytes() throws IOException {
|
||||
// 创建一个FileInputStream对象,传入file
|
||||
InputStream inputStream = new FileInputStream(this.file);
|
||||
|
||||
byte[] var2;
|
||||
try {
|
||||
// 读取所有字节数组
|
||||
var2 = inputStream.readAllBytes();
|
||||
} catch (Throwable var5) {
|
||||
try {
|
||||
// 关闭输入流
|
||||
inputStream.close();
|
||||
} catch (Throwable var4) {
|
||||
var5.addSuppressed(var4);
|
||||
}
|
||||
|
||||
throw var5;
|
||||
}
|
||||
|
||||
// 关闭输入流
|
||||
inputStream.close();
|
||||
return var2;
|
||||
}
|
||||
|
||||
// 获取文件的输入流
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return new FileInputStream(this.file);
|
||||
}
|
||||
|
||||
// 将文件传输到指定的文件
|
||||
public void transferTo(File dest) throws IOException, IllegalStateException {
|
||||
// 如果目标文件不存在,则创建
|
||||
if (!dest.exists()) {
|
||||
dest.createNewFile();
|
||||
}
|
||||
|
||||
// 创建一个FileInputStream对象,传入file
|
||||
InputStream inputStream = new FileInputStream(this.file);
|
||||
|
||||
try {
|
||||
// 创建一个FileOutputStream对象,传入dest
|
||||
OutputStream outputStream = new FileOutputStream(dest);
|
||||
|
||||
try {
|
||||
// 创建一个字节数组,大小为1024
|
||||
byte[] buffer = new byte[1024];
|
||||
|
||||
int bytesRead;
|
||||
// 循环读取文件,直到读取完毕
|
||||
while((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
// 将读取的字节数组写入目标文件
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
} catch (Throwable var8) {
|
||||
try {
|
||||
// 关闭输出流
|
||||
outputStream.close();
|
||||
} catch (Throwable var7) {
|
||||
var8.addSuppressed(var7);
|
||||
}
|
||||
|
||||
throw var8;
|
||||
}
|
||||
|
||||
// 关闭输出流
|
||||
outputStream.close();
|
||||
} catch (Throwable var9) {
|
||||
try {
|
||||
// 关闭输入流
|
||||
inputStream.close();
|
||||
} catch (Throwable var6) {
|
||||
var9.addSuppressed(var6);
|
||||
}
|
||||
|
||||
throw var9;
|
||||
}
|
||||
|
||||
// 关闭输入流
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user