liusuyi
2024-10-22 5141265c6214e84d4ef8a48c6cead6c372cc3892
增加:tcp连接注册和心跳
32 files added
10 files modified
2441 ■■■■■ changed files
ard-admin/src/main/java/com/ard/ArdUavApplication.java 2 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/resources/application-dev.yml 15 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/resources/application.yml 2 ●●●●● patch | view | raw | blame | history
ard-work/pom.xml 8 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/config/TyConfiguration.java 17 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/controller/UavController.java 7 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/http/TyClient.java 23 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/http/TyConst.java 2 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/model/dto/BoxSettingsDTO.java 74 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/netty/NettyConfig.java 39 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/netty/TyBoxSession.java 21 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/netty/TyNettyTcpClient.java 103 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/netty/command/BaseCommand.java 14 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/netty/command/HeartbeatCommand.java 22 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/netty/command/HeartbeatReplyCommand.java 26 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/netty/command/PointFlyCommand.java 30 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/netty/command/RegisterCommand.java 24 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/netty/handler/ClientHandler.java 89 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/netty/handler/MessageProtocolDecoder.java 51 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/netty/handler/MessageProtocolEncoder.java 16 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/service/ITyService.java 14 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/tianyu/service/impl/TyServiceImpl.java 70 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/client/MediaClient.java 123 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/controller/MediaController.java 216 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/ArdVtdu.java 74 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/Conf.java 46 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/Item.java 29 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/PathConfig.java 20 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/Paths.java 23 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/Readers.java 16 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/RtmpSession.java 27 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/RtmpSessions.java 21 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/RtspSession.java 29 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/RtspSessions.java 21 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/Segment.java 22 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/Source.java 16 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/StreamInfo.java 85 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/WebrtcSession.java 31 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/domain/WebrtcSessions.java 21 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/service/IMediaService.java 69 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/service/impl/MediaServiceImpl.java 669 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/vtdu/util/ArdTool.java 214 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/ArdUavApplication.java
@@ -3,12 +3,14 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
 * 启动程序
 * 
 * @author ruoyi
 */
@EnableScheduling
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class ArdUavApplication
{
ard-admin/src/main/resources/application-dev.yml
@@ -23,8 +23,19 @@
# 天宇服务器配置
tianyu:
  # 天宇服务器地址
  # 天宇http服务器地址
  endpoint: 112.98.106.22:12000
  accesskeyid: qmfmd3Ws7M4dMcylnrTU
  accesskeysecret: 5WBU2uEhHZaVefpZdaxpFh4LJW9fZZaa
  # 天宇tcp服务器地址
  tcpserver: 112.98.106.22
  tcpport: 13002
# netty配置
netty:
  tcp:
    enabled: true
    reConnectInterval: 30000
#流媒体配置
mediamtx:
  # 流媒体服务器地址
  host: 192.168.1.27
ard-admin/src/main/resources/application.yml
@@ -89,7 +89,7 @@
  read-timeout: 3000           # 数据读取超时时间,单位为毫秒(默认为 timeout)
  max-retry-count: 0           # 请求失败后重试次数(默认为 0 次不重试)
  ssl-protocol: TLS            # 单向验证的HTTPS的默认TLS协议(默认为 TLS)
  log-enabled: true            # 打开或关闭日志(默认为 true)
  log-enabled: false            # 打开或关闭日志(默认为 true)
  log-request: true            # 打开/关闭Forest请求日志(默认为 true)
  log-response-status: true    # 打开/关闭Forest响应状态日志(默认为 true)
  log-response-content: true   # 打开/关闭Forest响应内容日志(默认为 false)
ard-work/pom.xml
@@ -35,6 +35,14 @@
            <artifactId>forest-spring-boot-starter</artifactId>
            <version>1.5.33</version>
        </dependency>
        <!-- netty依赖 springboot2.x自动导入版本 -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
ard-work/src/main/java/com/ard/tianyu/config/TyConfiguration.java
@@ -33,17 +33,20 @@
    private String accesskeysecret;
    private String tcpserver;
    private Integer tcpport;
    private long timestamp;
    public long getTimeStamp()
    {
    public long getTimeStamp() {
        timestamp = new Date().getTime();
        return timestamp;
    }
    private String encryptstr;
    public String getEncryptStr()
    {
    public String getEncryptStr() {
        String sb = new StringBuilder()
                .append(accesskeyid)
                .append(accesskeysecret)
@@ -53,13 +56,17 @@
        return encryptstr;
    }
    public String getIpPort() {
        return tcpserver + "/" + tcpport;
    }
    @Resource
    private TyClient tyClient;
    private AccessTokenReceiver accessToken;
    public AccessTokenReceiver getAccessToken() {
        if(accessToken != null)
        if (accessToken != null)
            return accessToken;
        // 准备请求的json
        TokenDTO gtd = TokenDTO.builder()
ard-work/src/main/java/com/ard/tianyu/controller/UavController.java
@@ -3,6 +3,7 @@
import com.ard.common.core.domain.AjaxResult;
import com.ard.tianyu.model.dto.BoxHistoryDTO;
import com.ard.tianyu.model.dto.BoxPhotosDTO;
import com.ard.tianyu.model.dto.BoxSettingsDTO;
import com.ard.tianyu.model.receiver.BoxHistoryReceiver;
import com.ard.tianyu.model.receiver.BoxPhotosReceiver;
import com.ard.tianyu.model.receiver.BoxReceiver;
@@ -62,4 +63,10 @@
        List<BoxPhotosReceiver> boxPhotos = tyService.getBoxPhotos(boxPhotosDTO);
        return AjaxResult.success(boxPhotos);
    }
    @ApiOperation("修改云盒设置")
    @PostMapping("/updateBoxSettings")
    public AjaxResult updateBoxSettings(@RequestBody BoxSettingsDTO boxSettingsDTO) {
        Boolean state = tyService.updateBoxSettings(boxSettingsDTO);
        return AjaxResult.success(state);
    }
}
ard-work/src/main/java/com/ard/tianyu/http/TyClient.java
@@ -2,6 +2,7 @@
import com.ard.tianyu.model.dto.BoxHistoryDTO;
import com.ard.tianyu.model.dto.BoxPhotosDTO;
import com.ard.tianyu.model.dto.BoxSettingsDTO;
import com.ard.tianyu.model.dto.TokenDTO;
import com.ard.tianyu.model.receiver.*;
import com.dtflys.forest.annotation.*;
@@ -19,19 +20,39 @@
        baseURL = "http://#{tianyu.endpoint}"    // 默认域名
)
public interface TyClient {
    /**
     * 获取云盒TOKEN
     */
    @Post(TyConst.POST_TOKEN)
    TyHttpCommonReceiver<AccessTokenReceiver> getToken(@JSONBody TokenDTO tokenDTO);
    /**
     * 获取云盒列表
     */
    @Get(TyConst.GET_BOX_LIST)
    TyHttpCommonReceiver<List<BoxReceiver>> getBoxList(@Header Map<String, String> headers);
    /**
     * 获取直播地址
     */
    @Get(TyConst.GET_BOX_LIVE_URL)
    TyHttpCommonReceiver<PullStreamUrlReceiver> getBoxLiveUrl(@Var("boxSn") String boxSn, @Header Map<String, String> headers);
    /**
     * 获取飞行历史
     */
    @Post(TyConst.POST_BOX_HISTORY)
    TyHttpCommonReceiver<List<BoxHistoryReceiver>> getBoxHistory(@JSONBody BoxHistoryDTO boxHistoryDTO, @Header Map<String, String> headers);
    /**
     * 获取任务照片
     */
    @Post(TyConst.POST_BOX_PHOTOS)
    TyHttpCommonReceiver<List<BoxPhotosReceiver>> getBoxPhotos(@JSONBody BoxPhotosDTO boxPhotosDTO, @Header Map<String, String> headers);
    /**
     * 修改云盒设置
     */
    @Post(TyConst.POST_BOX_UPDATE)
    TyHttpCommonReceiver updateBoxSettings(@JSONBody BoxSettingsDTO boxSettingsDTO, @Header Map<String, String> headers);
}
ard-work/src/main/java/com/ard/tianyu/http/TyConst.java
@@ -5,7 +5,7 @@
    public static final String GET_BOX_LIST = "/eapi/box/list";
    public static final String POST_BOX_HISTORY = "/eapi/box/history";
    public static final String POST_BOX_NAME = "/eapi/box/name";
    public static final String POST_BOX_UPDATE = "/eapi/box/update";
    public static final String POST_UPDATE_LIVE = "/eapi/update/live";
    public static final String GET_BOX_LIVE_URL = "/eapi/box/pullUrl/{boxSn}";
    public static final String POST_BOX_PHOTOS = "/eapi/box/photos";
ard-work/src/main/java/com/ard/tianyu/model/dto/BoxSettingsDTO.java
New file
@@ -0,0 +1,74 @@
package com.ard.tianyu.model.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * BoxSettingsDTO
 *
 * @author liusuyi
 * @date 2024年10月22日11:54
 **/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BoxSettingsDTO {
    /**
     * 云盒SN编号
     */
    @JsonProperty("boxSn")
    private String boxSn;
    /**
     * 云盒名称(20个字符以内)
     */
    @JsonProperty("boxName")
    private String boxName;
    /**
     * 照片实时回传开关,0-关闭,1-开启
     */
    @JsonProperty("openImgRtp")
    private String openImgRtp;
    /**
     * 视频包排序开关,0-关闭,1-开启(排序会增加视频延时)
     */
    @JsonProperty("openVideoSort")
    private String openVideoSort;
    /**
     * 云端视频存储开关,0-关闭,1-开启
     */
    @JsonProperty("openVideoStorage")
    private String openVideoStorage;
    /**
     * 视频自动推流开关,0-关闭,1-开启
     */
    @JsonProperty("videoAutoPush")
    private String videoAutoPush;
    /**
     * 云盒固件自动升级开关,0-关闭,1-开启
     */
    @JsonProperty("autoUpdate")
    private String autoUpdate;
    /**
     * 云盒与平台失联xx秒后自动返航
     */
    @JsonProperty("lostTime")
    private String lostTime;
    /**
     * 低电量自动返航开关:0-关闭,1-智能计算,xx-指定的返航电量
     */
    @JsonProperty("minHomeBattery")
    private String minHomeBattery;
}
ard-work/src/main/java/com/ard/tianyu/netty/NettyConfig.java
New file
@@ -0,0 +1,39 @@
package com.ard.tianyu.netty;
import com.ard.tianyu.config.TyConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
@Slf4j
public class NettyConfig {
    @Resource
    private TyConfiguration tianyuConfiguration;
    @Resource
    private TyNettyTcpClient tyNettyTcpClient;
    @PostConstruct
    public void createTyConnect() {
        try {
            // 将Tcp访问的IP和地址加入到字典
            Map<String,Object> mapParam =new HashMap<String, Object>();
            mapParam.put("IP",tianyuConfiguration.getTcpserver());
            mapParam.put("PORT",tianyuConfiguration.getTcpport());
            //要么从数据库查询信息,要么从配置文件读取信息
            List<Map<String, Object>> list = new ArrayList<>();
            list.add(mapParam);
            tyNettyTcpClient.initAllChannel(list);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}
ard-work/src/main/java/com/ard/tianyu/netty/TyBoxSession.java
New file
@@ -0,0 +1,21 @@
package com.ard.tianyu.netty;
import io.netty.channel.ChannelFuture;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TyBoxSession {
    public String subBoxSn;
    public ChannelFuture future;
    public Boolean isReConnect;
}
ard-work/src/main/java/com/ard/tianyu/netty/TyNettyTcpClient.java
New file
@@ -0,0 +1,103 @@
package com.ard.tianyu.netty;
import com.ard.tianyu.netty.handler.ClientHandler;
import com.ard.tianyu.netty.handler.MessageProtocolDecoder;
import com.ard.tianyu.netty.handler.MessageProtocolEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class TyNettyTcpClient {
    public static Map<String, TyBoxSession> channels = new HashMap<>();
    EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
    @Resource
    ClientHandler clientHandler;
    /**
     * 初始化Bootstrap
     */
    public Bootstrap getBootstrap(EventLoopGroup group) {
        if (null == group) {
            group = eventLoopGroup;
        }
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline.addLast("heart",new IdleStateHandler(0, 3, 0, TimeUnit.SECONDS));
                        pipeline.addLast("encoder",new MessageProtocolEncoder());
                        pipeline.addLast("decoder",new MessageProtocolDecoder());
                        pipeline.addLast("handler",clientHandler);
                    }
                });
        return bootstrap;
    }
    //   获取所有连接
    public void initAllChannel(List<Map<String,Object>> portHosts) {
        for (Map<String, Object> map : portHosts) {
            String host = (String) map.get("IP");
            Integer port = (Integer) map.get("PORT");
            initOneConnection(host,port);
        }
    }
    public void initOneConnection(String host,Integer port){
        log.info("调用一次{}:{}",host,port);
        //判断该账号下,是否有盒子被控制
        //当前正在被控制的盒子
        String currentBoxSn = "";
        if(channels.containsKey(host+"/"+port))
        {
            currentBoxSn = channels.get(host+"/"+port).getSubBoxSn();
            channels.get(host+"/"+port).getFuture().channel().close();
        }
        //异步连接tcp服务端
        Bootstrap bootstrap = getBootstrap(null);
        ChannelFuture future = bootstrap.connect(host,port).addListener((ChannelFuture futureListener) -> {
            if (futureListener.isSuccess()) {
                log.info("连接天宇TCP服务端成功:"+host+":"+port);
            } else {
                final EventLoop eventLoop = futureListener.channel().eventLoop();
                eventLoop.schedule(new Runnable() {
                    @Override
                    public void run() {
                        log.info("连接天宇TCP服务端失败ip:{},port:{},正尝试重连...",host,port);
                        try {
                            initOneConnection(host,port);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }, 5, TimeUnit.SECONDS);
            }
        });
        //判断是否有盒子被订阅
        channels.put(host+"/"+port, TyBoxSession.builder().subBoxSn(currentBoxSn).future(future)
                .isReConnect(!currentBoxSn.equals("")).build());
    }
}
ard-work/src/main/java/com/ard/tianyu/netty/command/BaseCommand.java
New file
@@ -0,0 +1,14 @@
package com.ard.tianyu.netty.command;
import io.netty.buffer.ByteBuf;
import lombok.Data;
@Data
public abstract class BaseCommand {
    private short dataHeader; // 帧头
    private short dataLength; // 数据长度
    private byte cmdCode; // 指令编号
    // 编码方法,子类需要实现
    public abstract void encode(ByteBuf out);
}
ard-work/src/main/java/com/ard/tianyu/netty/command/HeartbeatCommand.java
New file
@@ -0,0 +1,22 @@
package com.ard.tianyu.netty.command;
import io.netty.buffer.ByteBuf;
public class HeartbeatCommand extends BaseCommand {
    private final long timestamp;
    public HeartbeatCommand(long timestamp) {
        setDataHeader((short)0x7479);
        setDataLength((short)0x09);
        setCmdCode((byte)0x02);
        this.timestamp = timestamp;
    }
    public void encode(ByteBuf out) {
        out.writeShort(getDataHeader());
        out.writeShort(getDataLength());
        out.writeByte(getCmdCode());
        out.writeLong(timestamp); // 写入时间戳
    }
}
ard-work/src/main/java/com/ard/tianyu/netty/command/HeartbeatReplyCommand.java
New file
@@ -0,0 +1,26 @@
package com.ard.tianyu.netty.command;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class HeartbeatReplyCommand extends BaseCommand {
    private long timestamp; // 时间戳
    public HeartbeatReplyCommand(long timestamp) {
        this.timestamp = timestamp;
        setDataHeader((short) 0x6A77); // 设置帧头
        setCmdCode((byte) 0x02); // 设置指令编号
        setDataLength((short) (13)); // 数据长度 (指令编号 + 时间戳)
    }
    public void encode(ByteBuf out) {
        out.writeShort(getDataHeader());
        out.writeShort(getDataLength());
        out.writeByte(getCmdCode());
        out.writeLong(timestamp); // 写入时间戳
    }
}
ard-work/src/main/java/com/ard/tianyu/netty/command/PointFlyCommand.java
New file
@@ -0,0 +1,30 @@
package com.ard.tianyu.netty.command;// PointFlyCommand.java
import io.netty.buffer.ByteBuf;
public class PointFlyCommand extends BaseCommand {
    private double longitude;
    private double latitude;
    private float height;
    private float speed;
    public PointFlyCommand(double longitude, double latitude, float height, float speed) {
        this.longitude = longitude;
        this.latitude = latitude;
        this.height = height;
        this.speed = speed;
        setDataHeader((short) 0x7479);
        setCmdCode((byte) 0xD1);
        setDataLength((short) 17); // 数据长度计算
    }
    @Override
    public void encode(ByteBuf out) {
        out.writeShort(getDataHeader());    // 写入帧头
        out.writeShort(getDataLength());    // 写入数据长度
        out.writeByte(getCmdCode());        // 写入指令编号
        out.writeDouble(longitude);         // 写入经度
        out.writeDouble(latitude);          // 写入纬度
        out.writeFloat(height);             // 写入高度
        out.writeFloat(speed);              // 写入速度
    }
}
ard-work/src/main/java/com/ard/tianyu/netty/command/RegisterCommand.java
New file
@@ -0,0 +1,24 @@
package com.ard.tianyu.netty.command;// RegisterMessage.java
import io.netty.buffer.ByteBuf;
public class RegisterCommand extends BaseCommand {
    private int userId;
    private byte[] accessToken;
    public RegisterCommand(int userId, byte[] accessToken) {
        setDataHeader((short) 0x7479); // 设置帧头
        setDataLength((short) (5 + accessToken.length)); // 数据长度
        setCmdCode((byte) 0x01); // 设置命令编号
        this.userId = userId;
        this.accessToken = accessToken;
    }
    @Override
    public void encode(ByteBuf out) {
        out.writeShort(getDataHeader());    // 写入帧头
        out.writeShort(getDataLength());    // 写入数据长度
        out.writeByte(getCmdCode());        // 写入指令编号
        out.writeInt(userId);               // 写入用户编号
        out.writeBytes(accessToken);        // 写入访问令牌
    }
}
ard-work/src/main/java/com/ard/tianyu/netty/handler/ClientHandler.java
New file
@@ -0,0 +1,89 @@
package com.ard.tianyu.netty.handler;
import com.ard.tianyu.netty.TyNettyTcpClient;
import com.ard.tianyu.netty.command.BaseCommand;
import com.ard.tianyu.netty.command.HeartbeatCommand;
import com.ard.tianyu.service.ITyService;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
@ChannelHandler.Sharable
public class ClientHandler extends SimpleChannelInboundHandler<BaseCommand> {
    @Autowired
    ITyService tyService;
    @Resource
    private TyNettyTcpClient tyNettyTcpClient;
    @Override
    public void channelRead0(ChannelHandlerContext ctx, BaseCommand command){
        InetSocketAddress ipSocket = (InetSocketAddress) ctx.channel().remoteAddress();
        String host = ipSocket.getHostString();
        int port = ipSocket.getPort();
        //tyService.processMessage(host + "/" + port, msg);
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //当客户端connect成功后,可以在最后一个handler等待用户输入
        log.info("连接成功建立!");
        tyService.registerMessage();
        super.channelActive(ctx);
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object obj) {
        if (obj instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) obj;
            if (event.state() == IdleState.READER_IDLE) {
                log.info("客户端读超时");
            } else if (event.state() == IdleState.WRITER_IDLE) {
//                HeartbeatCommand msg = new HeartbeatCommand(System.currentTimeMillis());
//                log.info("发送⼼跳!");//1652345657305  1652345707631
//                ctx.writeAndFlush(msg);
            } else if (event.state() == IdleState.ALL_IDLE) {
                log.info("客户端所有操作超时");
            }
        }
    }
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        InetSocketAddress ipSocket = (InetSocketAddress) ctx.channel().remoteAddress();
        int port = ipSocket.getPort();
        String host = ipSocket.getHostString();
        log.error("与天宇设备" + host + ":" + port + "连接断开!");
        ctx.channel().eventLoop().schedule(new Runnable() {
            @Override
            public void run() {
                log.error("服务端链接不上,开始重连操作...");
                try {
                    tyNettyTcpClient.initOneConnection(host, port);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, 5, TimeUnit.SECONDS);
        super.channelInactive(ctx);
    }
}
ard-work/src/main/java/com/ard/tianyu/netty/handler/MessageProtocolDecoder.java
New file
@@ -0,0 +1,51 @@
package com.ard.tianyu.netty.handler;// MessageProtocolDecoder.java
import com.ard.tianyu.netty.command.BaseCommand;
import com.ard.tianyu.netty.command.HeartbeatReplyCommand;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Slf4j
public class MessageProtocolDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // 检查可读字节数是否足够
        if (in.readableBytes() < 4) {
            return; // 不足以读取头部
        }
        in.markReaderIndex(); // 标记当前读取位置
        short dataHead = in.readShort();
        if(dataHead != 0x6A77){
            return;
        }
        short dataLength = in.readShort();
        if (in.readableBytes() < dataLength) {
            in.resetReaderIndex(); // 如果不足,重置读取位置
            return;
        }
        byte cmdCode = in.readByte();
        BaseCommand command = null;
        // 根据 cmdCode 创建相应消息对象
        if (cmdCode == 0x01) { // 注册回复消息
            byte success = in.readByte(); // 读取是否成功
            log.info("注册回复: {}", success == 0x01 ? "成功" : "失败");
        }
        if (cmdCode == 0x02) { // 注册回复消息
            long timestamp = in.readLong(); // 读取时间戳
            command = new HeartbeatReplyCommand(timestamp); // 创建心跳回复对象
            log.info("心跳回复: 时间戳 {}", timestamp);
        }
        if (command != null) {
            out.add(command); // 添加解码的命令对象到输出列表
        } else {
           // log.warn("无法识别的命令码: {}", cmdCode);
        }
    }
}
ard-work/src/main/java/com/ard/tianyu/netty/handler/MessageProtocolEncoder.java
New file
@@ -0,0 +1,16 @@
package com.ard.tianyu.netty.handler;// MessageProtocolEncoder.java
import com.ard.tianyu.netty.command.BaseCommand;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MessageProtocolEncoder extends MessageToByteEncoder<BaseCommand> {
    @Override
    protected void encode(ChannelHandlerContext ctx, BaseCommand command, ByteBuf out) throws Exception {
        out.writeShort(command.getDataHeader()); // 写入帧头
        out.writeShort(command.getDataLength()); // 写入数据长度
        out.writeByte(command.getCmdCode()); // 写入指令编号
        command.encode(out); // 编码具体负载
    }
}
ard-work/src/main/java/com/ard/tianyu/service/ITyService.java
@@ -2,12 +2,11 @@
import com.ard.tianyu.model.dto.BoxHistoryDTO;
import com.ard.tianyu.model.dto.BoxPhotosDTO;
import com.ard.tianyu.model.dto.BoxSettingsDTO;
import com.ard.tianyu.model.receiver.*;
import com.dtflys.forest.annotation.Header;
import com.dtflys.forest.annotation.Var;
import java.util.List;
import java.util.Map;
/**
 * ITyService
@@ -48,4 +47,15 @@
     */
    List<BoxPhotosReceiver> getBoxPhotos(BoxPhotosDTO boxPhotosDTO);
    /**
     * 修改云盒设置
     * @return
     */
    Boolean updateBoxSettings(BoxSettingsDTO boxSettingsDTO);
    /**
     * 注册
     * @return
     */
    void registerMessage();
}
ard-work/src/main/java/com/ard/tianyu/service/impl/TyServiceImpl.java
@@ -4,12 +4,23 @@
import com.ard.tianyu.http.TyClient;
import com.ard.tianyu.model.dto.BoxHistoryDTO;
import com.ard.tianyu.model.dto.BoxPhotosDTO;
import com.ard.tianyu.model.dto.BoxSettingsDTO;
import com.ard.tianyu.model.receiver.*;
import com.ard.tianyu.netty.command.BaseCommand;
import com.ard.tianyu.netty.command.HeartbeatCommand;
import com.ard.tianyu.netty.command.RegisterCommand;
import com.ard.tianyu.netty.TyNettyTcpClient;
import com.ard.tianyu.service.ITyService;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
import java.net.InetSocketAddress;
import java.util.List;
/**
@@ -18,11 +29,13 @@
 * @author liusuyi
 * @date 2024年10月22日8:37
 **/
@Slf4j
@Service
public class TyServiceImpl implements ITyService {
    @Resource
    private TyClient tyClient;
    @Resource
    private TyConfiguration tyConfiguration;
@@ -81,4 +94,57 @@
        TyHttpCommonReceiver<List<BoxPhotosReceiver>> boxPhotos = tyClient.getBoxPhotos(boxPhotosDTO, tyConfiguration.getAccessToken().convertFromDTO());
        return boxPhotos.getContent();
    }
}
    /**
     * 修改云盒设置
     */
    @Override
    public Boolean updateBoxSettings(BoxSettingsDTO boxSettingsDTO) {
        TyHttpCommonReceiver<String> result = tyClient.updateBoxSettings(boxSettingsDTO, tyConfiguration.getAccessToken().convertFromDTO());
        return result.getState() == 1;
    }
    @Override
    public void registerMessage() {
        AccessTokenReceiver tyToken = tyConfiguration.getAccessToken();
        RegisterCommand registerMessage = new RegisterCommand(tyToken.getCompanyId(), tyToken.getAccessToken().getBytes());
        this.sendMessage(tyConfiguration.getIpPort(), registerMessage);
    }
    public boolean sendMessage(String key, BaseCommand msg) {
        //将从用户接收的信息发送到天宇 commandCache
        boolean cmdResult = false;
        try {
            ChannelFuture future = TyNettyTcpClient.channels.get(key).getFuture();
            Channel channel = future.channel();
            if (future != null && channel.isActive()) {
                InetSocketAddress ipSocket = (InetSocketAddress) channel.remoteAddress();
                String host = ipSocket.getHostString();
                int port = ipSocket.getPort();
                // 创建 ByteBuf 并编码消息
                ByteBuf buffer = Unpooled.buffer();
                msg.encode(buffer); // 确保对消息进行编码
                channel.writeAndFlush(buffer); // 发送编码后的数据
                cmdResult = true;
                log.info("向天宇服务器" + host + ":" + port + "发送数据" + ",命令码:" + msg.getCmdCode());
            } else {
                log.error("与天宇服务器链接断开,无法发送数据!");
            }
        } catch (Exception e) {
            log.info("Failed to publish the message. {}", "转发信息");
            e.printStackTrace();
        } finally {
            return cmdResult;
        }
    }
    @Scheduled(fixedRate = 5000)
    public void sendHeartBeat() {
        long timestamp = System.currentTimeMillis();
        log.debug("定时向天宇发送⼼跳,时间戳{}", timestamp);
        HeartbeatCommand heartbeatMessage = new HeartbeatCommand(timestamp);
        this.sendMessage(tyConfiguration.getIpPort(), heartbeatMessage);
    }
}
ard-work/src/main/java/com/ard/vtdu/client/MediaClient.java
New file
@@ -0,0 +1,123 @@
package com.ard.vtdu.client;
import com.ard.vtdu.domain.*;
import com.dtflys.forest.annotation.*;
/**
 * @Description: mediamtx流媒体客户端
 * @ClassName: client
 * @Author: 刘苏义
 * @Date: 2023年07月06日9:51
 * @Version: 1.0
 **/
@BaseRequest(baseURL = "http://#{mediamtx.host}:9997")
public interface MediaClient {
    /**
     * 增加路径配置
     */
    @Post("/v3/config/paths/add/{name}")
    public String addPath(@Var("name") String name, @JSONBody Conf body);
    /**
     * 修改路径配置
     */
    @Patch("/v3/config/paths/patch/{name}")
    public String editPath(@Var("name") String name, @JSONBody Conf body);
    /**
     * 移除路径配置
     */
    @Delete("/v3/config/paths/delete/{name}")
    public String removePath(@Var("name") String name);
    /**
     * 获取路径配置详情
     */
    @Get(url = "/v3/config/paths/get/{name}")
    public Conf getPathConfig(@Var("name") String name);
    /**
     * 获取路径配置列表
     */
    @Get(url = "/v3/config/paths/list?page={pageNum}&itemsPerPage={pageSize}")
    public PathConfig getPathConfigs(@Var("pageNum") Integer pageNum, @Var("pageSize") Integer pageSize);
    /**
     * 查询所有路径
     */
    @Get("/v3/paths/list?page={pageNum}&itemsPerPage={pageSize}")
    public Paths paths(@Var("pageNum") Integer pageNum, @Var("pageSize") Integer pageSize);
    /**
     * 查询所有rtsp会话
     */
    @Get("/v3/rtspsessions/list?page={pageNum}&itemsPerPage={pageSize}")
    public RtspSessions rtspsessions(@Var("pageNum") Integer pageNum, @Var("pageSize") Integer pageSize);
    /**
     * 查询所有webrt会话
     */
    @Get("/v3/webrtcsessions/list?page={pageNum}&itemsPerPage={pageSize}")
    public WebrtcSessions webrtcsessions(@Var("pageNum") Integer pageNum, @Var("pageSize") Integer pageSize);
    /**
     * 查询所有rtmp会话
     */
    @Get("/v3/rtmpconns/list?page={pageNum}&itemsPerPage={pageSize}")
    public RtmpSessions rtmpsessions(@Var("pageNum") Integer pageNum, @Var("pageSize") Integer pageSize);
    /**
     * 按sessionId查询rtsp会话
     */
    @Get("/v3/rtspsessions/get/{sessionId}")
    public RtspSession getRtspsessionById(@Var("sessionId") String sessionId);
    /**
     * 按sessionId查询webrtc会话
     */
    @Get("/v3/webrtcsessions/get/{sessionId}")
    public WebrtcSession getWebrtcsessionById(@Var("sessionId") String sessionId);
    /**
     * 按sessionId查询rtmp会话
     */
    @Get("/v3/rtmpconns/get/{sessionId}")
    public RtmpSession getRtmpsessionById(@Var("sessionId") String sessionId);
    /**
     * 按sessionId删除rtsp会话
     */
    @Post("/v3/rtspsessions/kick/{sessionId}")
    public String kickRtspSessions(@Var("sessionId") String sessionId);
    /**
     * 按sessionId删除rtmp连接
     */
    @Post("/v3/rtmpconns/kick/{sessionId}")
    public String kickRtmpSessions(@Var("sessionId") String sessionId);
    /**
     * 按sessionId删除webrtc会话
     */
    @Post("/v3/webrtcsessions/kick/{sessionId}")
    public String kickWebrtcSessions(@Var("sessionId") String sessionId);
    /**
     * 录像分段列表
     */
    @Get("/v3/recordings/list/")
    public Paths recordingList(@Query("pageNum") Integer pageNum, @Query("pageSize") Integer pageSize);
    /**
     * 通过路径名获取录像分段列表
     */
    @Get("/v3/recordings/get/{name}")
    public Item recordingListByName(@Var("name") String name);
    /**
     * 通过路径名和分段录像开始时间删除录像分段
     */
    @Delete("/v3/recordings/deletesegment")
    public String deleteSegment(@Query("path") String name, @Query("start") String start);
}
ard-work/src/main/java/com/ard/vtdu/controller/MediaController.java
New file
@@ -0,0 +1,216 @@
package com.ard.vtdu.controller;
import com.ard.common.core.controller.BaseController;
import com.ard.common.core.domain.AjaxResult;
import com.ard.common.core.page.TableDataInfo;
import com.ard.vtdu.domain.ArdVtdu;
import com.ard.vtdu.domain.Conf;
import com.ard.vtdu.service.IMediaService;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Arrays;
/**
 * @Description: mediaMTX流媒体业务
 * @ClassName: controller
 * @Author: 刘苏义
 * @Date: 2023年07月13日9:26
 * @Version: 1.0
 **/
@RestController
@Api(tags = "mediaMTX接口")
@RequestMapping("/mediamtx/media")
public class MediaController extends BaseController {
    @Resource
    private IMediaService mediaService;
    /**
     * 增加通道
     */
    @PostMapping()
    @ApiOperation("增加通道")
    @PreAuthorize("@ss.hasPermi('mediamtx:media:add')")
    @ApiOperationSupport(includeParameters = {"ardVtdu.name", "ardVtdu.rtspSource", "ardVtdu.mode", "ardVtdu.isCode", "ardVtdu.isRecord"}, order = 1)
    public AjaxResult addPath(@RequestBody ArdVtdu ardVtdu) {
        if (StringUtils.isEmpty(ardVtdu.getName())) {
            return AjaxResult.error("通道名称不能为空");
        }
        if (StringUtils.isEmpty(ardVtdu.getRtspSource())) {
            return AjaxResult.error("rtsp地址不能为空");
        }
        return AjaxResult.success(mediaService.addPath(ardVtdu));
    }
    /**
     * 修改通道
     */
    @ApiOperation("修改通道")
    @ApiOperationSupport(includeParameters = {"ardVtdu.name", "ardVtdu.rtspSource", "ardVtdu.mode", "ardVtdu.isCode", "ardVtdu.isRecord"}, order = 2)
    @PreAuthorize("@ss.hasPermi('mediamtx:media:edit')")
    @PutMapping
    public AjaxResult edit(@RequestBody ArdVtdu ardVtdu) {
        return AjaxResult.success(mediaService.editPath(ardVtdu));
    }
    /**
     * 移除通道
     */
    @DeleteMapping("/path/{names}")
    @PreAuthorize("@ss.hasPermi('mediamtx:media:remove')")
    @ApiOperation("移除通道")
    @ApiOperationSupport(order = 3)
    public AjaxResult removePath(@PathVariable String[] names) {
        mediaService.removePath(names);
        return AjaxResult.success();
    }
    /**
     * 移除Webrtc拉流
     */
    @ApiOperation("移除Webrtc拉流")
    @ApiOperationSupport(order = 4)
    @DeleteMapping("/webrtc/{sessionIds}")
    public AjaxResult removeWebrtcPullStreamSession(@PathVariable String[] sessionIds) {
        Arrays.stream(sessionIds).forEach(sessionId -> mediaService.kickWebrtcSession(sessionId));
        return AjaxResult.success();
    }
    /**
     * 移除Rtmp拉流
     */
    @ApiOperation("移除Rtmp拉流")
    @ApiOperationSupport(order = 4)
    @DeleteMapping("/rtmp/{sessionIds}")
    public AjaxResult removeRtmpPullStreamSession(@PathVariable String[] sessionIds) {
        Arrays.stream(sessionIds).forEach(sessionId -> mediaService.kickRtmpSession(sessionId));
        return AjaxResult.success();
    }
    /**
     * 移除Rtsp拉流
     */
    @ApiOperation("移除Rtsp拉流")
    @ApiOperationSupport(order = 4)
    @DeleteMapping("/rtsp/{sessionIds}")
    public AjaxResult removeRtspPullStreamSession(@PathVariable String[] sessionIds) {
        Arrays.stream(sessionIds).forEach(sessionId -> mediaService.kickRtspSession(sessionId));
        return AjaxResult.success();
    }
    /**
     * 获取当前通道列表
     */
    @GetMapping("/path/list")
    @ApiOperation("获取当前通道列表")
    @ApiOperationSupport(order = 5)
    public TableDataInfo getPaths(Integer pageNum, Integer pageSize) {
        startPage();
        return getDataTable(mediaService.paths(pageNum, pageSize));
    }
    /**
     * 按ID查询Webrtc拉流详情
     */
    @GetMapping("/getWebrtcSessionById")
    @ApiOperation("按ID查询Webrtc拉流详情")
    public AjaxResult getWebrtcSessionById(String sessionId) {
        return AjaxResult.success(mediaService.getWebrtcSessionById(sessionId));
    }
    /**
     * 获取推流列表
     */
    @PreAuthorize("@ss.hasPermi('mediamtx:media:list')")
    @GetMapping("/pushList")
    @ApiOperation("获取推流列表")
    @ApiOperationSupport(order = 6)
    public TableDataInfo getPushStreamList(Integer pageNum, Integer pageSize) {
        TableDataInfo tableDataInfo = mediaService.getPushStreamList(pageNum, pageSize);
        return tableDataInfo;
    }
    /**
     * 获取WebRtc拉流列表
     */
    @GetMapping("/getPullWebrtcStreamList")
    @ApiOperation("获取WebRtc拉流列表")
    @ApiOperationSupport(order = 7)
    public TableDataInfo getPullWebrtcStreamList(Integer pageNum, Integer pageSize) {
        return mediaService.getPullWebrtcStreamList(pageNum, pageSize);
    }
    /**
     * 获取rtsp拉流列表
     */
    @GetMapping("/getPullRtspStreamList")
    @ApiOperation("获取rtsp拉流列表")
    @ApiOperationSupport(order = 8)
    public TableDataInfo getPullRtspStreamList(Integer pageNum, Integer pageSize) {
        return mediaService.getPullRtspStreamList(pageNum, pageSize);
    }
    /**
     * 获取rtmp拉流列表
     */
    @GetMapping("/getPullRtmpStreamList")
    @ApiOperation("获取rtmp拉流列表")
    @ApiOperationSupport(order = 9)
    public TableDataInfo getPullRtmpStreamList(Integer pageNum, Integer pageSize) {
        return mediaService.getPullRtmpStreamList(pageNum, pageSize);
    }
    //配置录像
    @PostMapping("/setRecord")
    @ApiOperation("配置录像")
    @ApiOperationSupport(includeParameters = {"conf.name", "conf.sourceOnDemand", "conf.record"})
    public AjaxResult setRecord(@RequestBody Conf conf) {
        return AjaxResult.success(mediaService.setRecord(conf));
    }
    //获取全部录像分段列表
    @GetMapping("/getRecordingList")
    @ApiOperation("获取全部录像分段列表")
    public AjaxResult getRecordingList(Integer pageNum, Integer pageSize) {
        return AjaxResult.success(mediaService.recordingList(pageNum, pageSize));
    }
    //通过路径名获取录像分段列表
    @GetMapping("/getRecordingListByName/{name}")
    @ApiOperation("通过路径名获取录像分段列表")
    public AjaxResult getRecordingListByName(@PathVariable String name) {
        return AjaxResult.success(mediaService.recordingListByName(name));
    }
    //删除录像
    @DeleteMapping("/deleteRecording")
    @ApiOperation("删除录像")
    public AjaxResult deleteRecording(String name, String start) {
        return AjaxResult.success(mediaService.deleteSegment(name, start));
    }
    //获取通道配置
    @GetMapping("/{name}")
    @ApiOperation("获取通道配置")
    public AjaxResult getPathConfig(@PathVariable String name) {
        return AjaxResult.success(mediaService.getPathConfig(name));
    }
    //获取所有通道配置列表
    @GetMapping("/getRecordConfList")
    @ApiOperation("获取所有通道配置列表")
    public AjaxResult getRecordConfList(Integer pageNum, Integer pageSize) {
        return AjaxResult.success(mediaService.getPathConfigs(pageNum, pageSize));
    }
}
ard-work/src/main/java/com/ard/vtdu/domain/ArdVtdu.java
New file
@@ -0,0 +1,74 @@
package com.ard.vtdu.domain;
import com.ard.common.annotation.Excel;
import com.ard.common.core.domain.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * 流媒体管理对象 vtdu
 *
 * @author ard
 * @date 2023-08-29
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ArdVtdu extends BaseEntity {
    private static final long serialVersionUID = 1L;
    /**
     * 主键名称
     */
    @Excel(name = "名称")
    private String name;
    /**
     * rtsp源地址
     */
    @Excel(name = "rtsp源地址")
    private String rtspSource;
    /**
     * 是否转码
     * 0:不转码 1:转码
     */
    @Excel(name = "是否转码")
    @Builder.Default
    private Integer isCode = 0;
    /**
     * 编解码模式
     * 0:cpu 1:gpu
     */
    @Excel(name = "编解码模式")
    @Builder.Default
    private Integer mode = 0;
    /**
     * 是否录像
     * 0:不录像 1:录像
     */
    @Excel(name = "是否录像")
    @Builder.Default
    private Integer isRecord = 0;
    /**
     * rtsp播放地址
     */
    String rtspUrl;
    /**
     * rtmp播放地址
     */
    String rtmpUrl;
    /**
     * webrtc播放地址
     */
    String webrtcUrl;
    /**
     * 相机ID
     */
    String cameraId;
}
ard-work/src/main/java/com/ard/vtdu/domain/Conf.java
New file
@@ -0,0 +1,46 @@
package com.ard.vtdu.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * @Description:
 * @ClassName: MediaInfo
 * @Author: 刘苏义
 * @Date: 2023年07月13日9:32
 * @Version: 1.0
 **/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Conf {
    private String name;
    private String source;
    //是否按需发布
    private Boolean sourceOnDemand;
    private String sourceProtocol;
    private Integer maxReaders;
    private String runOnDemand;
    private Boolean runOnDemandRestart;
    private String runOnDemandStartTimeout;
    private String runOnDemandCloseAfter;
    private String runOnInit;
    private Boolean runOnInitRestart;
    //录像
    @Builder.Default
    private Boolean record = false;
    @Builder.Default
    private String recordPath = "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f";
    @Builder.Default
    private String recordFormat = "fmp4";
    @Builder.Default
    private String recordPartDuration = "1s";
    @Builder.Default
    private String recordSegmentDuration = "0h5m0s";
    @Builder.Default
    private String recordDeleteAfter = "24h0m0s";
}
ard-work/src/main/java/com/ard/vtdu/domain/Item.java
New file
@@ -0,0 +1,29 @@
package com.ard.vtdu.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
 * @Description:
 * @ClassName: Items
 * @Author: 刘苏义
 * @Date: 2023年07月13日10:23
 * @Version: 1.0
 **/
@Data
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class Item {
    private String name;
    private String confName;
    private Source source;
    private List<Readers> readers;
    private List<String> tracks;
    private Long bytesReceived;
    private Long bytesSent;
    private Date readyTime;
    private List<Segment> segments;
}
ard-work/src/main/java/com/ard/vtdu/domain/PathConfig.java
New file
@@ -0,0 +1,20 @@
package com.ard.vtdu.domain;
import lombok.Data;
import java.util.List;
/**
 * ConfigPath
 *
 * @author liusuyi
 * @date 2024年09月26日10:04
 **/
@Data
public class PathConfig {
    private int itemCount;
    private int pageCount;
    private List<Conf> items;
}
ard-work/src/main/java/com/ard/vtdu/domain/Paths.java
New file
@@ -0,0 +1,23 @@
package com.ard.vtdu.domain;
import lombok.Data;
import java.util.List;
/**
 * @Description: returns all paths.
 * @ClassName: Paths
 * @Author: 刘苏义
 * @Date: 2023年07月13日10:06
 * @Version: 1.0
 **/
@Data
public class Paths {
    private int itemCount;
    private int pageCount;
    private List<Item> items;
}
ard-work/src/main/java/com/ard/vtdu/domain/Readers.java
New file
@@ -0,0 +1,16 @@
package com.ard.vtdu.domain;
import lombok.Data;
/**
 * @ClassName Readers
 * @Description:
 * @Author 刘苏义
 * @Date 2023/7/14 21:04
 * @Version 1.0
 */
@Data
public class Readers {
    private String type;
    private String id;
}
ard-work/src/main/java/com/ard/vtdu/domain/RtmpSession.java
New file
@@ -0,0 +1,27 @@
package com.ard.vtdu.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
 * @Description:
 * @ClassName: WebrtcSession
 * @Author: 刘苏义
 * @Date: 2023年08月26日10:16:21
 **/
@Data
public class RtmpSession {
    private String path;
    private String id;
    private Date created;
    private String remoteAddr;
    private String state;
    private long bytesReceived;
    private long bytesSent;
    private String upStream;
    private String downStream;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
    private Date beginTime;
}
ard-work/src/main/java/com/ard/vtdu/domain/RtmpSessions.java
New file
@@ -0,0 +1,21 @@
package com.ard.vtdu.domain;
import lombok.Data;
import java.util.List;
/**
 * @ClassName:Sessions
 * @Description:
 * @Author:ard
 * @Date:2024年07月12日13:34
 * @Version:1.0
 **/
@Data
public class RtmpSessions {
    private int itemCount;
    private int pageCount;
    private List<RtmpSession> items;
}
ard-work/src/main/java/com/ard/vtdu/domain/RtspSession.java
New file
@@ -0,0 +1,29 @@
package com.ard.vtdu.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
 * @ClassName RtspSession
 * @Description:
 * @Author 刘苏义
 * @Date 2023/7/14 21:19
 * @Version 1.0
 */
@Data
public class RtspSession {
    private String id;
    private String path;
    private Date created;
    private String remoteAddr;
    private String state;
    private String transport;
    private long bytesReceived;
    private long bytesSent;
    private String upStream;
    private String downStream;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date beginTime;
}
ard-work/src/main/java/com/ard/vtdu/domain/RtspSessions.java
New file
@@ -0,0 +1,21 @@
package com.ard.vtdu.domain;
import lombok.Data;
import java.util.List;
/**
 * @ClassName:Sessions
 * @Description:
 * @Author:ard
 * @Date:2024年07月12日13:34
 * @Version:1.0
 **/
@Data
public class RtspSessions {
    private int itemCount;
    private int pageCount;
    private List<RtspSession> items;
}
ard-work/src/main/java/com/ard/vtdu/domain/Segment.java
New file
@@ -0,0 +1,22 @@
package com.ard.vtdu.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
 * Segment
 *
 * @author liusuyi
 * @date 2024年09月24日13:26
 **/
@Data
public class Segment {
    private String start;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
    private Date startTime;
}
ard-work/src/main/java/com/ard/vtdu/domain/Source.java
New file
@@ -0,0 +1,16 @@
package com.ard.vtdu.domain;
import lombok.Data;
/**
 * @Description:
 * @ClassName: Source
 * @Author: 刘苏义
 * @Date: 2023年07月13日10:24
 * @Version: 1.0
 **/
@Data
public class Source {
    private String type;
    private String id;
}
ard-work/src/main/java/com/ard/vtdu/domain/StreamInfo.java
New file
@@ -0,0 +1,85 @@
package com.ard.vtdu.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.Date;
/**
 * @Description:流信息
 * @ClassName: PushStreamInfo
 * @Author: 刘苏义
 * @Date: 2023年07月15日8:40:37
 * @Version: 1.0
 **/
@Data
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class StreamInfo {
    /**
     * 会话ID
     */
    String id;
    /**
     * 路径名称
     */
    String name;
    /**
     * 源rtsp地址
     */
    String rtspSource;
    /**
     * rtsp播放地址
     */
    String rtspUrl;
    /**
     * rtmp播放地址
     */
    String rtmpUrl;
    /**
     * webrtc播放地址
     */
    String webrtcUrl;
    /**
     * 传输方式
     */
    String protocol;
    /**
     * 上行流量
     */
    String upTraffic;
    /**
     * 下行流量
     */
    String downTraffic;
    /**
     * 拉流数量
     */
    Integer num;
    /**
     * 开始时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    Date beginTime;
    /**
     * 远程主机
     */
    String remoteAddr;
    /**
     * 解码模式
     * 0-gpu硬解码 1-cpu软解码
     */
    Integer mode;
    /**
     * 是否转码
     * 0-不转码 1-转码
     */
    Integer isCode;
    /**
     * 媒体类型(webRTCSession/rtspSession)
     */
    String sessionType;
}
ard-work/src/main/java/com/ard/vtdu/domain/WebrtcSession.java
New file
@@ -0,0 +1,31 @@
package com.ard.vtdu.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
 * @Description:
 * @ClassName: WebrtcSession
 * @Author: 刘苏义
 * @Date: 2023年08月26日10:16:21
 **/
@Data
public class WebrtcSession {
    private String id;
    private String path;
    private Date created;
    private String remoteAddr;
    private String peerConnectionEstablished;
    private String localCandidate;
    private String remoteCandidate;
    private String state;
    private long bytesReceived;
    private long bytesSent;
    private String upStream;
    private String downStream;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date beginTime;
}
ard-work/src/main/java/com/ard/vtdu/domain/WebrtcSessions.java
New file
@@ -0,0 +1,21 @@
package com.ard.vtdu.domain;
import lombok.Data;
import java.util.List;
/**
 * @ClassName:Sessions
 * @Description:
 * @Author:ard
 * @Date:2024年07月12日13:34
 * @Version:1.0
 **/
@Data
public class WebrtcSessions {
    private int itemCount;
    private int pageCount;
    private List<WebrtcSession> items;
}
ard-work/src/main/java/com/ard/vtdu/service/IMediaService.java
New file
@@ -0,0 +1,69 @@
package com.ard.vtdu.service;
import com.ard.common.core.page.TableDataInfo;
import com.ard.vtdu.domain.*;
import java.util.List;
public interface IMediaService {
    /**
     * 增加路径
     */
    ArdVtdu addPath(ArdVtdu ardVtdu);
    /**
     * 修改路径
     */
    ArdVtdu editPath(ArdVtdu ardVtdu);
    //获取通道配置
    Conf getPathConfig(String name);
    //获取通道配置列表
    PathConfig getPathConfigs(Integer pageNum, Integer pageSize);
    void removePath(String[] names);
    void removePath(String name);
    List<StreamInfo> paths(Integer pageNum, Integer pageSize);
    List<String> getNameList();
    boolean checkNameExist(String name);
    RtspSession getRtspSessionById(String sessionId);
    WebrtcSession getWebrtcSessionById(String sessionId);
    RtmpSession getRtmpSessionById(String sessionId);
    TableDataInfo getPushStreamList(Integer pageNum, Integer pageSize);
    TableDataInfo getPullWebrtcStreamList(Integer pageNum, Integer pageSize);
    TableDataInfo getPullRtspStreamList(Integer pageNum, Integer pageSize);
    TableDataInfo getPullRtmpStreamList(Integer pageNum, Integer pageSize);
    Boolean kickRtspSession(String sessionId);
    Boolean kickRtmpSession(String sessionId);
    Boolean kickWebrtcSession(String sessionId);
    void clearAll();
    //开启录像
    String setRecord(Conf conf);
    //获取全部录像分段列表
    Paths recordingList(Integer pageNum, Integer pageSize);
    //通过路径名获取录像分段列表
    Item recordingListByName(String name);
    //通过路径名和分段录像开始时间删除录像分段
    String deleteSegment(String name, String start);
}
ard-work/src/main/java/com/ard/vtdu/service/impl/MediaServiceImpl.java
New file
@@ -0,0 +1,669 @@
package com.ard.vtdu.service.impl;
import com.ard.common.constant.HttpStatus;
import com.ard.common.core.page.TableDataInfo;
import com.ard.tianyu.model.receiver.BoxReceiver;
import com.ard.tianyu.model.receiver.PullStreamUrlReceiver;
import com.ard.tianyu.service.ITyService;
import com.ard.vtdu.client.MediaClient;
import com.ard.vtdu.domain.*;
import com.ard.vtdu.service.IMediaService;
import com.ard.vtdu.util.ArdTool;
import com.dtflys.forest.exceptions.ForestRuntimeException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
 * @Description: 流媒体业务
 * @ClassName: MediaService
 * @Author: 刘苏义
 * @Date: 2023年07月13日9:28
 * @Version: 1.0
 **/
@Service
@Slf4j
public class MediaServiceImpl implements IMediaService {
    @Resource
    private MediaClient mediaClient;
    @Resource
    private ITyService tyService;
    @Value("${mediamtx.host}")
    private String mediamtxHost;
    @PostConstruct
    public void initMediaMTX() {
        //清空mediamtx流媒体数据
        clearAll();
    }
    /**
     * @return
     * @Author 刘苏义
     * @Description 添加流媒体
     * name 相机ID
     * sourceUrl rtsp推流地址
     * isCode 0-不转码 1-转码
     * mode 1-gpu硬解码 0-cpu软解码
     * @Date 2024/7/3 8:45
     * @Param
     */
    @Override
    public ArdVtdu addPath(ArdVtdu ardVtdu) {
        try {
            String rtspUrl = "rtsp://" + mediamtxHost + ":8554/" + ardVtdu.getName();
            String rtmpUrl = "rtmp://" + mediamtxHost + ":1935/" + ardVtdu.getName();
            String webrtcUrl = "http://" + mediamtxHost + ":8889/" + ardVtdu.getName();
            Conf conf = new Conf();
            boolean isCodeEnabled = ardVtdu.getIsCode().equals(1);
            boolean isModeEabled = ardVtdu.getMode().equals(1);
            boolean isRecordEnabled = ardVtdu.getIsRecord().equals(1);
            if (isCodeEnabled) {
                conf.setSource("publisher");
                //默认软解码
                String cmd = "ffmpeg -rtsp_transport tcp -i " + ardVtdu.getRtspSource() + " -vcodec libx264 -preset ultrafast -r 25 -keyint_min 25 -g 60 -sc_threshold 0 -threads 6  -b:v 2048k -acodec opus  -strict -2 -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH";
                if (isModeEabled) {//硬解码
                    cmd = "ffmpeg -hwaccel cuvid -c:v hevc_cuvid  -rtsp_transport tcp  -i " + ardVtdu.getRtspSource() + " -c:v h264_nvenc  -r 25 -g 60 -sc_threshold 0 -threads 6  -b:v 2048k -bf 0 -acodec opus  -strict -2 -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH";
                }
                if (isRecordEnabled) {
                    //录像
                    conf.setRunOnInit(cmd);
                    conf.setRunOnInitRestart(true);
                    conf.setRunOnDemand("");
                    conf.setRecord(true);
                } else {
                    conf.setRunOnDemand(cmd);
                    conf.setRunOnDemandRestart(true);
                    conf.setRunOnDemandCloseAfter("5s");
                    conf.setRecord(false);
                    conf.setRunOnInit("");
                }
            } else {
                conf.setSource(ardVtdu.getRtspSource());
                conf.setRunOnDemandRestart(false);
                conf.setRunOnDemandCloseAfter("5s");
                conf.setSourceOnDemand(!isRecordEnabled);
                conf.setRecord(isRecordEnabled);
            }
            conf.setMaxReaders(100);
            conf.setSourceProtocol("tcp");
            if (!checkNameExist(ardVtdu.getName())) {
                mediaClient.addPath(ardVtdu.getName(), conf);
            } else {
                mediaClient.editPath(ardVtdu.getName(), conf);
            }
            ardVtdu.setCameraId(ardVtdu.getName().split("_")[0]);
            ardVtdu.setRtspUrl(rtspUrl);
            ardVtdu.setRtmpUrl(rtmpUrl);
            ardVtdu.setWebrtcUrl(webrtcUrl);
            return ardVtdu;
        } catch (ForestRuntimeException ex) {
            log.error("添加流媒体异常:" + ex.getMessage());
        }
        return null;
    }
    @Override
    public ArdVtdu editPath(ArdVtdu ardVtdu) {
        try {
            String rtspUrl = "rtsp://" + mediamtxHost + ":8554/" + ardVtdu.getName();
            String rtmpUrl = "rtmp://" + mediamtxHost + ":1935/" + ardVtdu.getName();
            String webrtcUrl = "http://" + mediamtxHost + ":8889/" + ardVtdu.getName();
            Conf conf = new Conf();
            boolean isCodeEnabled = ardVtdu.getIsCode().equals(1);
            boolean isModeEabled = ardVtdu.getMode().equals(1);
            boolean isRecordEnabled = ardVtdu.getIsRecord().equals(1);
            if (isCodeEnabled) {
                conf.setSource("publisher");
                //默认软解码
                String cmd = "ffmpeg -rtsp_transport tcp -i " + ardVtdu.getRtspSource() + " -vcodec libx264 -preset ultrafast -r 25 -keyint_min 25 -g 60 -sc_threshold 0 -threads 6  -b:v 2048k -acodec opus  -strict -2 -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH";
                if (isModeEabled) {//硬解码
                    cmd = "ffmpeg -hwaccel cuvid -c:v hevc_cuvid  -rtsp_transport tcp  -i " + ardVtdu.getRtspSource() + " -c:v h264_nvenc  -r 25 -g 60 -sc_threshold 0 -threads 6  -b:v 2048k -bf 0 -acodec opus  -strict -2 -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH";
                }
                if (isRecordEnabled) {
                    //录像
                    conf.setRunOnInit(cmd);
                    conf.setRunOnInitRestart(true);
                    conf.setRunOnDemand("");
                    conf.setRecord(true);
                } else {
                    conf.setRunOnDemand(cmd);
                    conf.setRunOnDemandRestart(true);
                    conf.setRunOnDemandCloseAfter("5s");
                    conf.setRecord(false);
                    conf.setRunOnInit("");
                }
            } else {
                conf.setSource(ardVtdu.getRtspSource());
                conf.setRunOnDemand("");
                conf.setRunOnDemandRestart(false);
                conf.setRunOnDemandCloseAfter("5s");
                conf.setSourceOnDemand(!isRecordEnabled);
                conf.setRecord(isRecordEnabled);
            }
            conf.setMaxReaders(100);
            conf.setSourceProtocol("tcp");
            if (checkNameExist(ardVtdu.getName())) {
                mediaClient.editPath(ardVtdu.getName(), conf);
            }
            ardVtdu.setRtspUrl(rtspUrl);
            ardVtdu.setRtmpUrl(rtmpUrl);
            ardVtdu.setWebrtcUrl(webrtcUrl);
            return ardVtdu;
        } catch (ForestRuntimeException ex) {
            log.error("修改流媒体异常:" + ex.getMessage());
        }
        return null;
    }
    @Override
    public void removePath(String[] names) {
        try {
            for (String name : names) {
                if (checkNameExist(name)) {
                    mediaClient.removePath(name);
                }
            }
        } catch (Exception ex) {
            log.error("批量移除流媒体异常:" + ex.getMessage());
        }
    }
    @Override
    public void removePath(String name) {
        try {
            if (checkNameExist(name)) {
                mediaClient.removePath(name);
            }
        } catch (ForestRuntimeException ex) {
            log.error("移除流媒体异常:" + ex.getMessage());
        }
    }
    @Override
    public List<StreamInfo> paths(Integer pageNum, Integer pageSize) {
        Paths paths = mediaClient.paths(pageNum - 1, pageSize);
        List<Item> items = paths.getItems();
        List<StreamInfo> pathInfoList = new ArrayList<>();
        for (Item item : items) {
            StreamInfo info = new StreamInfo();
            //ID
            String name = item.getName();
            info.setName(name);
            Conf conf = mediaClient.getPathConfig(name);
            String runOn = "";
            if (StringUtils.isNotEmpty(conf.getRunOnDemand())) {
                runOn = conf.getRunOnDemand();
            }
            //RTSP源地址
            Matcher matcher = Pattern.compile("rtsp://[^\\s\"]+").matcher(runOn);
            if (matcher.find()) {
                info.setRtspSource(matcher.group());
                info.setIsCode(1);
            } else {
                info.setRtspSource(conf.getSource());
                info.setIsCode(0);
            }
            //传输协议
            matcher = Pattern.compile("-rtsp_transport\\s+(\\w+)").matcher(runOn);
            if (matcher.find()) {
                info.setProtocol(matcher.group(1));
            }
            pathInfoList.add(info);
        }
        return pathInfoList;
    }
    @Override
    public RtspSession getRtspSessionById(String sessionId) {
        RtspSession rtspSession = mediaClient.getRtspsessionById(sessionId);
        return rtspSession;
    }
    @Override
    public WebrtcSession getWebrtcSessionById(String sessionId) {
        WebrtcSession webrtcSession = mediaClient.getWebrtcsessionById(sessionId);
        return webrtcSession;
    }
    @Override
    public RtmpSession getRtmpSessionById(String sessionId) {
        RtmpSession rtmpSession = mediaClient.getRtmpsessionById(sessionId);
        return rtmpSession;
    }
    /**
     * 获取推流列表
     * 刘苏义
     * 2023/8/29 9:37:05
     */
    @Override
    public TableDataInfo getPushStreamList(Integer pageNum, Integer pageSize) {
        TableDataInfo tableDataInfo = new TableDataInfo();
        List<StreamInfo> PushStreamInfoList = new ArrayList<>();
        Paths paths = mediaClient.paths(pageNum - 1, pageSize);
        int itemCount = paths.getItemCount();
        tableDataInfo.setTotal(itemCount);
        List<Item> items = paths.getItems();
        for (Item item : items) {
            StreamInfo info = new StreamInfo();
            //ID
            String name = item.getName();
            info.setName(name);
            Conf conf = mediaClient.getPathConfig(name);
            //RTMP播放地址
            String rtmpUrl = "rtmp://" + mediamtxHost + ":1935/" + name;
            info.setRtmpUrl(rtmpUrl);
            //RTSP播放地址
            String rtspUrl = "rtsp://" + mediamtxHost + ":8554/" + name;
            info.setRtspUrl(rtspUrl);
            //WEBRTC播放地址
            String webrtcUrl = "http://" + mediamtxHost + ":8889/" + name;
            info.setWebrtcUrl(webrtcUrl);
            Source source = item.getSource();
            if (source == null || source.getId().equals("")) {
                //会话ID
                info.setId("0");
                //下行流量
                long bytesReceived = item.getBytesReceived();
                String formatReceivedSize = ArdTool.formatFileSize(bytesReceived);
                info.setDownTraffic(formatReceivedSize);
                //上行流量
                long bytesSent = item.getBytesSent();
                String formatSentdSize = ArdTool.formatFileSize(bytesSent);
                info.setUpTraffic(formatSentdSize);
                info.setBeginTime(item.getReadyTime());
            } else {
                RtspSession rtspSession = getRtspSessionById(source.getId());
                //会话ID
                info.setId(rtspSession.getId());
                //开始推流时间
                info.setBeginTime(rtspSession.getCreated());
                //上行流量
                long bytesReceived = rtspSession.getBytesReceived();
                String formatReceivedSize = ArdTool.formatFileSize(bytesReceived);
                info.setUpTraffic(formatReceivedSize);
                //下行流量
                long bytesSent = rtspSession.getBytesSent();
                String formatSentSize = ArdTool.formatFileSize(bytesSent);
                info.setDownTraffic(formatSentSize);
                //推流服务器
                info.setRemoteAddr(rtspSession.getRemoteAddr());
            }
            //RTSP源地址
            String runOn = "";
            if (StringUtils.isNotEmpty(conf.getRunOnDemand())) {
                runOn = conf.getRunOnDemand();
            }
            Matcher matcher = Pattern.compile("rtsp://[^\\s\"]+").matcher(runOn);
            if (matcher.find()) {
                info.setRtspSource(matcher.group());
            } else {
                info.setRtspSource(conf.getSource());
            }
            //传输协议
            info.setProtocol(conf.getSourceProtocol());
            //拉流数量
            List<Readers> readers = item.getReaders();
            info.setNum(readers.size());
            PushStreamInfoList.add(info);
        }
        tableDataInfo.setRows(PushStreamInfoList);
        tableDataInfo.setCode(HttpStatus.SUCCESS);
        tableDataInfo.setMsg("查询成功");
        return tableDataInfo;
    }
    /**
     * 获取webrtc拉流列表
     * 刘苏义
     * 2023/8/29 9:37:05
     */
    @Override
    public TableDataInfo getPullWebrtcStreamList(Integer pageNum, Integer pageSize) {
        TableDataInfo tableDataInfo = new TableDataInfo();
        WebrtcSessions WebrtcSessions = mediaClient.webrtcsessions(pageNum - 1, pageSize);
        List<WebrtcSession> items = WebrtcSessions.getItems();
        items.forEach(
                webrtcSession -> {
                    webrtcSession.setUpStream(ArdTool.formatFileSize(webrtcSession.getBytesReceived()));
                    webrtcSession.setDownStream(ArdTool.formatFileSize(webrtcSession.getBytesSent()));
                    webrtcSession.setBeginTime(webrtcSession.getCreated());
                }
        );
        tableDataInfo.setTotal(WebrtcSessions.getItemCount());
        tableDataInfo.setRows(items);
        tableDataInfo.setCode(HttpStatus.SUCCESS);
        tableDataInfo.setMsg("查询成功");
        return tableDataInfo;
    }
    /**
     * 获取rtmp拉流列表
     * 刘苏义
     * 2023/8/29 9:37:05
     */
    @Override
    public TableDataInfo getPullRtmpStreamList(Integer pageNum, Integer pageSize) {
        TableDataInfo tableDataInfo = new TableDataInfo();
        RtmpSessions rtmpSessions = mediaClient.rtmpsessions(pageNum - 1, pageSize);
        List<RtmpSession> items = rtmpSessions.getItems();
        items.stream().forEach(
                webrtcSession -> {
                    webrtcSession.setUpStream(ArdTool.formatFileSize(webrtcSession.getBytesReceived()));
                    webrtcSession.setDownStream(ArdTool.formatFileSize(webrtcSession.getBytesSent()));
                    webrtcSession.setBeginTime(webrtcSession.getCreated());
                }
        );
        tableDataInfo.setTotal(rtmpSessions.getItemCount());
        tableDataInfo.setRows(items);
        tableDataInfo.setCode(HttpStatus.SUCCESS);
        tableDataInfo.setMsg("查询成功");
        return tableDataInfo;
    }
    /**
     * 获取rtsp拉流列表
     * 刘苏义
     * 2023/8/29 9:37:05
     */
    @Override
    public TableDataInfo getPullRtspStreamList(Integer pageNum, Integer pageSize) {
        TableDataInfo tableDataInfo = new TableDataInfo();
        RtspSessions rtspSessions = mediaClient.rtspsessions(pageNum - 1, pageSize);
        List<RtspSession> items = rtspSessions.getItems();
        items.stream().forEach(
                webrtcSession -> {
                    webrtcSession.setUpStream(ArdTool.formatFileSize(webrtcSession.getBytesReceived()));
                    webrtcSession.setDownStream(ArdTool.formatFileSize(webrtcSession.getBytesSent()));
                    webrtcSession.setBeginTime(webrtcSession.getCreated());
                }
        );
        tableDataInfo.setTotal(rtspSessions.getItemCount());
        tableDataInfo.setRows(items);
        tableDataInfo.setCode(HttpStatus.SUCCESS);
        tableDataInfo.setMsg("查询成功");
        return tableDataInfo;
    }
    /**
     * 踢出rtsp会话
     * 刘苏义
     * 2023/8/29 9:37:05
     */
    @Override
    public Boolean kickRtspSession(String sessionId) {
        try {
            mediaClient.kickRtspSessions(sessionId);
            return true;
        } catch (Exception ex) {
            return false;
        }
    }
    /**
     * 踢出rtmp会话
     * 刘苏义
     * 2023/8/29 9:37:05
     */
    @Override
    public Boolean kickRtmpSession(String sessionId) {
        try {
            mediaClient.kickRtmpSessions(sessionId);
            return true;
        } catch (Exception ex) {
            return false;
        }
    }
    /**
     * 踢出webrtc会话
     * 刘苏义
     * 2023/8/29 9:37:05
     */
    @Override
    public Boolean kickWebrtcSession(String sessionId) {
        try {
            mediaClient.kickWebrtcSessions(sessionId);
            return true;
        } catch (Exception ex) {
            return false;
        }
    }
    /**
     * 获取流媒体name列表
     * 刘苏义
     * 2023/10/13 14:19:07
     */
    @Override
    public List<String> getNameList() {
        List<String> nameList = new ArrayList<>();
        try {
            Paths paths = mediaClient.paths(0, 1000);
            List<Item> items = paths.getItems();
            for (Item item : items) {
                nameList.add(item.getName());
            }
        } catch (Exception ex) {
            log.error("获取流媒体name列表异常:" + ex.getMessage());
        }
        return nameList;
    }
    /**
     * 检查name是否存在
     * 刘苏义
     * 2023/10/19 15:18:45
     */
    @Override
    public boolean checkNameExist(String name) {
        boolean result = false;
        List<String> nameList = getNameList();
        if (nameList.contains(name)) {
            result = true;
        }
        return result;
    }
    /**
     * 清空所有流媒体信息
     *
     * @author 刘苏义
     * @date 2024/8/22 10:18
     */
    @Override
    public void clearAll() {
        List<String> nameList = getNameList();
        nameList.stream().forEach(name -> {
            removePath(name);
        });
    }
    /**
     * 开启录像
     */
    @Override
    public String setRecord(Conf conf) {
        return mediaClient.editPath(conf.getName(), conf);
    }
    /**
     * 获取全部录像分段列表
     */
    @Override
    public Paths recordingList(Integer pageNum, Integer pageSize) {
        Paths paths = mediaClient.recordingList(pageNum - 1, pageSize);
        paths.getItems().forEach(item -> {
            item.getSegments().forEach(segment -> {
                // 使用 OffsetDateTime 进行解析
                OffsetDateTime offsetDateTime = OffsetDateTime.parse(segment.getStart());
                // 转换为 java.util.Date
                Date date = Date.from(offsetDateTime.toInstant());
                segment.setStartTime(date);
            });
        });
        return paths;
    }
    /**
     * 指定路径名获取录像分段列表
     */
    @Override
    public Item recordingListByName(String name) {
        Item item = mediaClient.recordingListByName(name);
        item.getSegments().forEach(segment -> {
            // 使用 OffsetDateTime 进行解析
            OffsetDateTime offsetDateTime = OffsetDateTime.parse(segment.getStart());
            // 转换为 java.util.Date
            Date date = Date.from(offsetDateTime.toInstant());
            segment.setStartTime(date);
        });
        return item;
    }
    /**
     * 通过路径名和分段录像开始时间删除录像分段
     */
    @Override
    public String deleteSegment(String name, String start) {
        return mediaClient.deleteSegment(name, start);
    }
    //获取通道配置
    @Override
    public Conf getPathConfig(String name) {
        return mediaClient.getPathConfig(name);
    }
    //获取所有通道配置列表
    @Override
    public PathConfig getPathConfigs(Integer pageNum, Integer pageSize) {
        return mediaClient.getPathConfigs(pageNum - 1, pageSize);
    }
    //同步无人机推流到流媒体
    @Scheduled(cron = "0/20 * * * * ?")
    public void syncVtdu() {
        log.info("定时同步MediaMTX任务开始");
        List<String> nameList = getNameList();
        List<BoxReceiver> boxList = tyService.getBoxList();
        List<ArdVtdu> ardVtduList = boxList.stream()
                .filter(boxReceiver -> boxReceiver.getOnLine() != 0)  // 过滤掉不在线的
                .map(boxReceiver -> {
                    PullStreamUrlReceiver boxLiveUrl = tyService.getBoxLiveUrl(boxReceiver.getBoxSn());
                    return ArdVtdu.builder().name(boxReceiver.getBoxSn()).rtspSource(boxLiveUrl.getRtmp()).build();
                })
                .collect(Collectors.toList());
        asyncMediaMTX(ardVtduList, nameList);
    }
    /**
     * @return
     * @Author 刘苏义
     * @Description 同步本地vtdu库和流媒体中的数据
     * @Date 2024/7/10 15:26
     * @Param vtdus vtdu库的集合
     * @Param names 流媒体的name集合
     */
    public void asyncMediaMTX(List<ArdVtdu> vtdus, List<String> names) {
        //需要更新的数据,参数顺序注意
        sameList(vtdus, names).forEach(vtdu -> {
            Conf conf = getPathConfig(vtdu.getName());
            if (conf == null || conf.getSource() == null) {
                return;
            }
            String currentRtspSource = vtdu.getRtspSource();  // 提前获取需要比对的RTSP源
            String confRtspSource = null;
            // 未转码
            if (vtdu.getIsCode().equals(0)) {
                //获取source中的rtsp地址
                confRtspSource = conf.getSource();
            } else {
                // 转码
                if (vtdu.getIsRecord().equals(1)) {
                    //开启录像获取RunOnInit中的rtsp地址
                    confRtspSource = conf.getRunOnInit() != null ? matchRtsp(conf.getRunOnInit()) : null;
                } else {
                    //关闭录像获取RunOnDemand中的rtsp地址
                    confRtspSource = conf.getRunOnDemand() != null ? matchRtsp(conf.getRunOnDemand()) : null;
                }
            }
            // 比较 录像状态 是否一致
            boolean isRecordChanged = !Objects.equals(conf.getRecord() != null && conf.getRecord() ? 1 : 0, vtdu.getIsRecord());
            // 比较 RTSP 源是否一致
            boolean isRtspSourceChanged = !Objects.equals(confRtspSource, currentRtspSource);
            // 如果 RTSP 源或录制状态发生变化,则更新路径
            if (isRecordChanged || isRtspSourceChanged) {
                editPath(vtdu);
            }
        });
        //需要删除的数据
        diffListToDel(vtdus, names).forEach(name -> {
            removePath(name);
        });
        //需要新增的数据
        diffListToAdd(vtdus, names).forEach(vtdu -> {
            addPath(vtdu);
        });
    }
    //需要更新的(流媒体和vtdu相同)
    public List<ArdVtdu> sameList(List<ArdVtdu> vtduses, List<String> names) {
        return vtduses.stream().filter(vtdu -> names.contains(vtdu.getName())).collect(Collectors.toList());
    }
    //需要删除的(流媒体中多的)
    public List<String> diffListToDel(List<ArdVtdu> vtduses, List<String> names) {
        if (vtduses.size() >= names.size()) {
            List<String> nameList = vtduses.stream().map(ArdVtdu::getName)
                    .filter(item -> !names.contains(item))
                    .collect(Collectors.toList());
            return nameList;
        } else {
            List<String> nameList = names.stream().filter(item -> !vtduses.stream().map(ArdVtdu::getName).collect(Collectors.toList()).contains(item))
                    .collect(Collectors.toList());
            return nameList;
        }
    }
    //需要新增的(流媒体中少的)
    public List<ArdVtdu> diffListToAdd(List<ArdVtdu> vtduses, List<String> names) {
        return vtduses.stream().filter(vtdu -> !names.contains(vtdu.getName())).collect(Collectors.toList());
    }
    //匹配rtsp地址
    private String matchRtsp(String value) {
        Matcher matcher = Pattern.compile("rtsp://[^\\s\"]+").matcher(value);
        if (matcher.find()) {
            return matcher.group();
        } else {
            return "";
        }
    }
}
ard-work/src/main/java/com/ard/vtdu/util/ArdTool.java
New file
@@ -0,0 +1,214 @@
package com.ard.vtdu.util;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.lang.reflect.Field;
import java.text.DecimalFormat;
import java.time.LocalTime;
import java.util.*;
/**
 * @Description: 工具方法
 * @ClassName: ArdTool
 * @Author: 刘苏义
 * @Date: 2023年06月02日14:37
 * @Version: 1.0
 **/
@Slf4j
public class ArdTool {
    /**
     * @描述 实体对象转Map
     * @参数 [entity]
     * @返回值 java.util.Map<java.lang.String, java.lang.Object>
     * @创建人 刘苏义
     * @创建时间 2023/6/2 14:35
     * @修改人和其它信息
     */
    public static Map<String, Object> convertEntityToMap(Object entity) {
        Map<String, Object> map = new HashMap<>();
        Field[] fields = entity.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            try {
                Object value = field.get(entity);
                map.put(field.getName(), value);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return map;
    }
    /**
     * 求Map<K,V>中最小 Value 对应的Key值
     *
     * @param map
     * @return
     */
    public static String getKeyByMinValue(Map<String, Double> map) {
        if (map == null || map.size() == 0) return null;
        List<Map.Entry<String, Double>> list = new ArrayList(map.entrySet());
        Collections.sort(list, (o1, o2) -> (o1.getValue().intValue() - o2.getValue().intValue()));
        String min = list.get(0).getKey();
//        String max = list.get(list.size() - 1).getKey();
        return min;
    }
    /**
     * @描述 通过日夜切换时间判断当前使用通道号
     * @参数 []
     * @返回值 java.lang.Integer
     * @创建人 刘苏义
     * @创建时间 2023/6/17 13:50
     * @修改人和其它信息
     */
    public static Integer getChannelBydayNightTime(String dayNightTime) {
        try {
            // 获取当前时间
            LocalTime currentTime = LocalTime.now();
            String[] dayNight = dayNightTime.split("-");
            // 解析时间字符串
            LocalTime startTime = LocalTime.parse(dayNight[0]);
            LocalTime endTime = LocalTime.parse(dayNight[1]);
            // 检查当前时间是否在给定的时间范围内
            boolean isWithinRange = !currentTime.isBefore(startTime) && !currentTime.isAfter(endTime);
            // 输出结果
            if (isWithinRange) {
                return 1;
            } else {
                return 2;
            }
        } catch (Exception ex) {
            log.error("光电切换获取通道异常:" + ex.getMessage());
            return 1;
        }
    }
    /**
     * 文件大小智能转换
     * 会将文件大小转换为最大满足单位
     *
     * @param size(文件大小,单位为B)
     * @return 文件大小
     */
    public static String formatFileSize(Long size) {
        String sizeName = null;
        if (1024 * 1024 > size && size >= 1024) {
            sizeName = String.format("%.2f", size.doubleValue() / 1024) + "KB";
        } else if (1024 * 1024 * 1024 > size && size >= 1024 * 1024) {
            sizeName = String.format("%.2f", size.doubleValue() / (1024 * 1024)) + "MB";
        } else if (size >= 1024 * 1024 * 1024) {
            sizeName = String.format("%.2f", size.doubleValue() / (1024 * 1024 * 1024)) + "GB";
        } else {
            sizeName = size.toString() + "B";
        }
        return sizeName;
    }
    /**
     * 文件大小智能转换
     * 会将文件大小转换为最大满足单位
     *
     * @param size(文件大小,单位为B)
     * @return 文件大小
     */
    public static String readableFileSize(long size) {
        if (size <= 0) {
            return "0";
        }
        final String[] units = new String[]{"B", "KB", "MB", "GB", "TB"};
        int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
        return new DecimalFormat("#,###.##").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
    }
    /**
     * byte数组转字符串
     * 刘苏义
     * 2023/10/18 8:42:59
     *
     * @param byteArray
     */
    public static String byteArrayToString(byte[] byteArray) {
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b : byteArray) {
            stringBuilder.append(String.valueOf(b));
        }
        return stringBuilder.toString();
    }
    /**
     * 将数据写入txt文件
     * 刘苏义
     * 2024/3/8 12:58:39
     */
    public static void writeStringToFile(String data, String fileName) {
        try {
            File file = new File(fileName);
            File parentDir = file.getParentFile();
            if (!parentDir.exists()) {
                parentDir.mkdirs(); // 创建文件所在的目录,包括父目录
            }
            boolean fileExists = file.exists();
            boolean dataExists = false;
            if(fileExists) {
                // 读取文件内容
                BufferedReader reader = new BufferedReader(new FileReader(fileName));
                String line;
                while ((line = reader.readLine()) != null) {
                    if (line.equals(data)) {
                        dataExists = true;
                        break;
                    }
                }
                reader.close();
            }
            // 如果文件不存在或数据不存在,则追加到文件
            if (!fileExists || !dataExists) {
                FileWriter writer = new FileWriter(fileName, true);
                writer.write(data + System.lineSeparator());
                writer.close();
                if (!fileExists) {
                    log.debug("File created and data has been appended.");
                } else {
                    log.debug("Data has been appended to the file.");
                }
            } else {
                log.debug("Data already exists in the file. Not appending.");
            }
        } catch (IOException e) {
            log.error("An error occurred.");
            e.printStackTrace();
        }
    }
    /**
     *  通过反射对象填充
     * 刘苏义
     * 2024/4/2 11:50:57
     */
    public static void fillNullFields(Object source, Object target) {
        if (source == null || target == null) {
            return;
        }
        Field[] fields = source.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            try {
                Object sourceValue = field.get(source);
                Object targetValue = field.get(target);
                if (targetValue == null && sourceValue != null) {
                    field.set(target, sourceValue);
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
}