liusuyi
2024-07-11 b1084891961232e3c697ea9fc52f127cdccffb6b
优化:流媒体
已添加1个文件
已修改18个文件
已删除2个文件
2759 ■■■■■ 文件已修改
ard-work/src/main/java/com/ruoyi/device/camera/domain/ArdCameras.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/device/camera/factory/CameraSDK.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/device/channel/service/IArdChannelService.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/device/channel/service/impl/ArdChannelServiceImpl.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/media/controller/MediaController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/media/service/IMediaService.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/media/service/IMediaV2Service.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/media/service/IVtduService.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/media/service/impl/MediaServiceImpl.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/media/service/impl/MediaV2ServiceImpl.java 484 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/media/service/impl/VtduServiceImpl.java 82 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/utils/forest/MediaClient.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/utils/sdk/dhsdk/module/ConfigModule.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/utils/sdk/dhsdk/service/impl/DahuaSDK.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/utils/sdk/hiksdk/lib/LoginResultCallBack.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/utils/sdk/hiksdk/service/impl/HikvisionSDK.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/resources/mapper/device/ArdCamerasMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/resources/templates/test.html 548 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/resources/templates/test1.html 1161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/SyncTask.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/mediamtx/mediamtx.yml 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ard-work/src/main/java/com/ruoyi/device/camera/domain/ArdCameras.java
@@ -178,7 +178,7 @@
    /**
     * ç™»å½•ID
     */
    private Integer loginId;
    private Long loginId;
    /**
     * åœ¨çº¿çŠ¶æ€ 0-离线 1-在线
     */
@@ -192,7 +192,7 @@
    /**
     * èµ·å§‹é€šé“号
     */
    private Integer startDChan;
    private Integer startChan;
    /**
     * é€šé“æ•°
     */
ard-work/src/main/java/com/ruoyi/device/camera/factory/CameraSDK.java
@@ -4,6 +4,7 @@
import com.ruoyi.device.camera.domain.ArdCameras;
import com.ruoyi.device.camera.domain.CameraCmd;
import com.ruoyi.device.channel.domain.ArdChannel;
import com.ruoyi.media.domain.Vtdu;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@@ -116,4 +117,5 @@
    //本地录像停止
    AjaxResult localRecordStop(CameraCmd cmd);
}
ard-work/src/main/java/com/ruoyi/device/channel/service/IArdChannelService.java
@@ -1,6 +1,8 @@
package com.ruoyi.device.channel.service;
import java.util.List;
import com.ruoyi.device.camera.domain.ArdCameras;
import com.ruoyi.device.channel.domain.ArdChannel;
/**
@@ -65,22 +67,6 @@
     * @return ç»“æžœ
     */
     public int deleteArdChannelByDeviceId(String deviceId);
     /**
      * @Author åˆ˜è‹ä¹‰
      * @Description èŽ·å–2个通道列表的交集
      * @Date   2024/7/10 9:38
      * @Param
      * @return
      */
    public List<ArdChannel> sameList(List<ArdChannel> oldArrayList, List<ArdChannel> newArrayList);
    /**
     * @Author åˆ˜è‹ä¹‰
     * @Description å–2个通道列表的差集
     * @Date   2024/7/10 9:39
     * @Param
     * @return
     */
    public List<ArdChannel> diffList(List<ArdChannel> firstArrayList, List<ArdChannel> secondArrayList);
    /**
     * @Author åˆ˜è‹ä¹‰
@@ -89,5 +75,5 @@
     * @Param
     * @return
     */
    public void asyncChannel(List<ArdChannel> oldArrayList, List<ArdChannel> newArrayList);
    public void asyncChannel(ArdCameras ardCameras, List<ArdChannel> oldArrayList, List<ArdChannel> newArrayList);
}
ard-work/src/main/java/com/ruoyi/device/channel/service/impl/ArdChannelServiceImpl.java
@@ -1,9 +1,16 @@
package com.ruoyi.device.channel.service.impl;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.device.camera.domain.ArdCameras;
import com.ruoyi.device.camera.factory.CameraSDK;
import com.ruoyi.device.camera.factory.CameraSDKFactory;
import com.ruoyi.device.camera.mapper.ArdCamerasMapper;
import com.ruoyi.media.mapper.VtduMapper;
import com.ruoyi.media.service.IVtduService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.device.channel.mapper.ArdChannelMapper;
@@ -22,7 +29,10 @@
public class ArdChannelServiceImpl implements IArdChannelService {
    @Resource
    private ArdChannelMapper ardChannelMapper;
    @Resource
    private CameraSDKFactory cameraSDKFactory;
    @Resource
    private IVtduService vtduService;
    /**
     * æŸ¥è¯¢é€šé“管理
     *
@@ -101,38 +111,40 @@
        return ardChannelMapper.deleteArdChannelByDeviceId(deviceId);
    }
    //求两个对象List的交集
    @Override
    public List<ArdChannel> sameList(List<ArdChannel> oldArrayList, List<ArdChannel> newArrayList) {
        List<ArdChannel> resultList = newArrayList.stream()
                .filter(item -> oldArrayList.stream().map(e -> e.getChanNo())
                        .collect(Collectors.toList()).contains(item.getChanNo()))
                .collect(Collectors.toList());
        return resultList;
    }
    public void asyncChannel(ArdCameras ardCameras,List<ArdChannel> oldArrayList, List<ArdChannel> newArrayList) {
        // å°†åˆ—表转换为映射以提高性能
        Map<Integer, ArdChannel> oldMap = oldArrayList.stream()
                .collect(Collectors.toMap(ArdChannel::getChanNo, channel -> channel));
        Map<Integer, ArdChannel> newMap = newArrayList.stream()
                .collect(Collectors.toMap(ArdChannel::getChanNo, channel -> channel));
    //求两个对象List的差集
    @Override
    public List<ArdChannel> diffList(List<ArdChannel> firstArrayList, List<ArdChannel> secondArrayList) {
        List<ArdChannel> resultList = firstArrayList.stream()
                .filter(item -> !secondArrayList.stream().map(e -> e.getChanNo()).collect(Collectors.toList()).contains(item.getChanNo()))
                .collect(Collectors.toList());
        return resultList;
    }
        // éœ€è¦æ›´æ–°çš„æ•°æ®
        newArrayList.stream()
                .filter(channel -> {
                    ArdChannel oldChannel = oldMap.get(channel.getChanNo());
                    return oldChannel != null && !oldChannel.getName().equals(channel.getName());
                })
                .forEach(channel -> {
                    ArdChannel oldChannel = oldMap.get(channel.getChanNo());
                    channel.setId(oldChannel.getId());
                    updateArdChannel(channel);
                });
    @Override
    public void asyncChannel(List<ArdChannel> oldArrayList, List<ArdChannel> newArrayList) {
        //需要更新的数据,参数顺序注意
        sameList(oldArrayList, newArrayList).stream().forEach(ardChannel -> {
            updateArdChannel(ardChannel);
        });
        //需要删除的数据
        diffList(oldArrayList, newArrayList).stream().forEach(ardChannel -> {
            deleteArdChannelById(ardChannel.getId());
        });
        //需要新增的数据
        diffList(newArrayList, oldArrayList).stream().forEach(ardChannel -> {
            insertArdChannel(ardChannel);
        });
        // éœ€è¦åˆ é™¤çš„æ•°æ®
        oldArrayList.stream()
                .filter(channel -> !newMap.containsKey(channel.getChanNo()))
                .forEach(channel -> {
                    deleteArdChannelById(channel.getId());
                    vtduService.deleteVtduByName(channel.getDeviceId() + "_" + channel.getChanNo());
                });
        // éœ€è¦æ–°å¢žçš„æ•°æ®
        newArrayList.stream()
                .filter(channel -> !oldMap.containsKey(channel.getChanNo()))
                .forEach(channel -> {
                    insertArdChannel(channel);
                    vtduService.addChanToVtdu(ardCameras, channel);
                });
    }
}
ard-work/src/main/java/com/ruoyi/media/controller/MediaController.java
@@ -4,10 +4,12 @@
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.media.domain.Config;
import com.ruoyi.media.domain.Paths;
import com.ruoyi.media.domain.StreamInfo;
import com.ruoyi.media.domain.Vtdu;
import com.ruoyi.media.service.IMediaService;
@@ -112,7 +114,7 @@
    @PreAuthorize("@ss.hasPermi('media:stream:remove')")
    @DeleteMapping("/{sessionId}")
    public AjaxResult removePullStreamSession(@PathVariable String sessionId) {
        List<StreamInfo> pullStreamList = mediaService.getPullStreamList();
        List<StreamInfo> pullStreamList = mediaService.getPullStreamList(1,1000);
        StreamInfo streamInfo = pullStreamList.stream()
                .filter(object -> object.getId().equals(sessionId))
                .collect(Collectors.toList()).get(0);
@@ -143,9 +145,9 @@
    @GetMapping("/path/list")
    @ApiOperation("获取当前通道列表")
    @ApiOperationSupport(order = 5)
    public TableDataInfo getPaths() {
    public TableDataInfo getPaths(Integer pageNum,Integer pageSize) {
        startPage();
        return getDataTable(mediaService.paths());
        return getDataTable(mediaService.paths(pageNum,pageSize));
    }
    /**
@@ -154,7 +156,7 @@
    @GetMapping("/getRtspSessionById")
    @ApiOperation("按ID查询拉流详情")
    public AjaxResult getRtspSessionById(String sessionId) {
        List<StreamInfo> pullStreamList = mediaService.getPullStreamList();
        List<StreamInfo> pullStreamList = mediaService.getPullStreamList(1,1000);
        StreamInfo streamInfo = pullStreamList.stream()
                .filter(object -> object.getId().equals(sessionId))
                .collect(Collectors.toList()).get(0);
@@ -176,9 +178,9 @@
    @GetMapping("/pushList")
    @ApiOperation("获取推流列表")
    @ApiOperationSupport(order = 6)
    public TableDataInfo getPushStreamList() {
    public TableDataInfo getPushStreamList(Integer pageNum,Integer pageSize) {
        startPage();
        return getDataTable(mediaService.getPushStreamList());
        return getDataTable(mediaService.getPushStreamList(pageNum,pageSize));
    }
    /**
@@ -188,9 +190,9 @@
    @GetMapping("/pullList")
    @ApiOperation("获取拉流列表")
    @ApiOperationSupport(order = 7)
    public TableDataInfo getPullStreamList() {
    public TableDataInfo getPullStreamList(Integer pageNum,Integer pageSize) {
        startPage();
        return getDataTable(mediaService.getPullStreamList());
        return getDataTable(mediaService.getPullStreamList(pageNum,pageSize));
    }
}
ard-work/src/main/java/com/ruoyi/media/service/IMediaService.java
@@ -33,7 +33,7 @@
    void removePath(String name);
    List<StreamInfo> paths();
    List<StreamInfo> paths(Integer pageNum,Integer pageSize);
    List<String> getNameList();
@@ -45,9 +45,9 @@
    RtmpSession getRtmpSessionById(String sessionId);
    List<StreamInfo> getPushStreamList();
    List<StreamInfo> getPushStreamList(Integer pageNum,Integer pageSize);
    List<StreamInfo> getPullStreamList();
    List<StreamInfo> getPullStreamList(Integer pageNum,Integer pageSize);
    Boolean kickRtspSession(String sessionId);
ard-work/src/main/java/com/ruoyi/media/service/IMediaV2Service.java
ÎļþÒÑɾ³ý
ard-work/src/main/java/com/ruoyi/media/service/IVtduService.java
@@ -2,6 +2,7 @@
import java.util.List;
import com.ruoyi.device.camera.domain.ArdCameras;
import com.ruoyi.device.channel.domain.ArdChannel;
import com.ruoyi.media.domain.Vtdu;
@@ -103,4 +104,7 @@
     * @Param
     */
    public void asyncVtdu(List<Vtdu> vtdus, List<String> names);
    //添加通道至流媒体
    void addChanToVtdu(ArdCameras camera, ArdChannel channel);
}
ard-work/src/main/java/com/ruoyi/media/service/impl/MediaServiceImpl.java
@@ -8,6 +8,7 @@
import com.ruoyi.media.service.IMediaService;
import com.ruoyi.utils.forest.MediaClient;
import com.ruoyi.utils.tools.ArdTool;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
@@ -146,6 +147,8 @@
        if (StringUtils.isNotEmpty(conf.getRunOnDemand())) {
            runOn = conf.getRunOnDemand();
            info.setMode("0");
        } else {
            info.setMode("1");
        }
        //RTSP源地址
        Matcher matcher = Pattern.compile("rtsp://[^\\s\"]+").matcher(runOn);
@@ -184,8 +187,8 @@
    }
    @Override
    public List<StreamInfo> paths() {
        Paths paths = mediaClient.paths();
    public List<StreamInfo> paths(Integer pageNum, Integer pageSize) {
        Paths paths = mediaClient.paths(pageNum - 1, pageSize);
        List<Items> items = paths.getItems();
        List<StreamInfo> pathInfoList = new ArrayList<>();
        for (Items item : items) {
@@ -221,23 +224,17 @@
    @Override
    public RtspSession getRtspSessionById(String sessionId) {
        String list = mediaClient.getRtspsessionById(sessionId);
        RtspSession rtspSession = JSONObject.parseObject(list, RtspSession.class);
        return rtspSession;
        return mediaClient.getRtspsessionById(sessionId);
    }
    @Override
    public WebrtcSession getWebrtcSessionById(String sessionId) {
        String list = mediaClient.getWebrtcsessionById(sessionId);
        WebrtcSession webrtcSession = JSONObject.parseObject(list, WebrtcSession.class);
        return webrtcSession;
        return mediaClient.getWebrtcsessionById(sessionId);
    }
    @Override
    public RtmpSession getRtmpSessionById(String sessionId) {
        String list = mediaClient.getRtmpsessionById(sessionId);
        RtmpSession rtmpSession = JSONObject.parseObject(list, RtmpSession.class);
        return rtmpSession;
        return mediaClient.getRtmpsessionById(sessionId);
    }
    /**
@@ -246,9 +243,9 @@
     * 2023/8/29 9:37:05
     */
    @Override
    public List<StreamInfo> getPushStreamList() {
    public List<StreamInfo> getPushStreamList(Integer pageNum, Integer pageSize) {
        List<StreamInfo> PushStreamInfoList = new ArrayList<>();
        Paths paths = mediaClient.paths();
        Paths paths = mediaClient.paths(pageNum - 1, pageSize);
        List<Items> items = paths.getItems();
        for (Items item : items) {
            StreamInfo info = new StreamInfo();
@@ -321,9 +318,9 @@
     * 2023/8/29 9:37:05
     */
    @Override
    public List<StreamInfo> getPullStreamList() {
    public List<StreamInfo> getPullStreamList(Integer pageNum, Integer pageSize) {
        List<StreamInfo> PullStreamInfoList = new ArrayList<>();
        Paths paths = mediaClient.paths();
        Paths paths = mediaClient.paths(pageNum - 1, pageSize);
        List<Items> items = paths.getItems();
        for (Items item : items) {
            List<Readers> readers = item.getReaders();
@@ -467,7 +464,7 @@
    public List<String> getNameList() {
        List<String> nameList = new ArrayList<>();
        try {
            Paths paths = mediaClient.paths();
            Paths paths = mediaClient.paths(0, 1000);
            List<Items> items = paths.getItems();
            for (Items item : items) {
                nameList.add(item.getName());
ard-work/src/main/java/com/ruoyi/media/service/impl/MediaV2ServiceImpl.java
ÎļþÒÑɾ³ý
ard-work/src/main/java/com/ruoyi/media/service/impl/VtduServiceImpl.java
@@ -6,9 +6,15 @@
import java.util.stream.Collectors;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.device.camera.domain.ArdCameras;
import com.ruoyi.device.camera.domain.CameraCmd;
import com.ruoyi.device.camera.factory.CameraSDK;
import com.ruoyi.device.camera.factory.CameraSDKFactory;
import com.ruoyi.device.channel.domain.ArdChannel;
import com.ruoyi.media.domain.StreamInfo;
import com.ruoyi.media.service.IMediaService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.ruoyi.media.mapper.VtduMapper;
import com.ruoyi.media.domain.Vtdu;
@@ -30,7 +36,10 @@
    private VtduMapper vtduMapper;
    @Resource
    private IMediaService mediaService;
    @Resource
    private CameraSDKFactory cameraSDKFactory;
    @Value("${mediamtx.host}")
    String mediamtxHost;
    /**
     * æŸ¥è¯¢æµåª’体管理
     *
@@ -39,7 +48,6 @@
     */
    @Override
    public Vtdu selectVtduByName(String name) {
        return vtduMapper.selectVtduByName(name);
    }
@@ -62,17 +70,6 @@
     */
    @Override
    public int insertVtdu(Vtdu vtdu) {
        log.debug("流媒体【" + vtdu.getName() + "】通道添加");
        Map<String, String> map = mediaService.addPath(vtdu.getName(), vtdu.getRtspSource(), vtdu.getMode(), vtdu.getIsCode());
        vtdu.setRtspUrl(map.get("rtspUrl"));
        vtdu.setRtmpUrl(map.get("rtmpUrl"));
        vtdu.setWebrtcUrl(map.get("webrtcUrl"));
        vtdu.setName(vtdu.getName());
        vtdu.setRtspSource(vtdu.getRtspSource());
        vtdu.setIsCode(vtdu.getIsCode());
        vtdu.setMode(vtdu.getMode());
        vtdu.setCreateTime(DateUtils.getNowDate());
        return vtduMapper.insertVtdu(vtdu);
    }
@@ -84,7 +81,6 @@
     */
    @Override
    public int updateVtdu(Vtdu vtdu) {
        log.debug("流媒体【" + vtdu.getName() + "】通道更新");
        Map<String, String> map = mediaService.editPath(vtdu.getName(), vtdu.getRtspSource(), vtdu.getMode(), vtdu.getIsCode());
        vtdu.setName(vtdu.getName());
        vtdu.setRtspSource(vtdu.getRtspSource());
@@ -171,12 +167,10 @@
                    .collect(Collectors.toList());
            return nameList;
        } else {
            List<String> nameList = names.stream().filter(item -> !vtdus.stream().map(Vtdu::getName).collect(Collectors.toList()).contains(item))
                    .collect(Collectors.toList());
            return nameList;
        }
    }
    //需要新增的(流媒体中少的)
@@ -186,18 +180,21 @@
    }
    /**
     * @Author åˆ˜è‹ä¹‰
     * @Description  åŒæ­¥æœ¬åœ°vtdu库和流媒体中的数据
     * @Date   2024/7/10 15:26
     * @Param  vtdus vtdu库的集合
     * @Param  names æµåª’体的name集合
     * @return
     * @Author åˆ˜è‹ä¹‰
     * @Description åŒæ­¥æœ¬åœ°vtdu库和流媒体中的数据
     * @Date 2024/7/10 15:26
     * @Param vtdus vtdu库的集合
     * @Param names æµåª’体的name集合
     */
    @Override
    public void asyncVtdu(List<Vtdu> vtdus, List<String> names) {
        //需要更新的数据,参数顺序注意
        sameList(vtdus, names).stream().forEach(vtdu -> {
            mediaService.editPath(vtdu.getName(), vtdu.getRtspSource(), vtdu.getMode(), vtdu.getIsCode());
            StreamInfo streamInfo = mediaService.getPathInfo(vtdu.getName());
            if (!streamInfo.getRtspSource().equals(vtdu.getRtspSource())) {
                mediaService.editPath(vtdu.getName(), vtdu.getRtspSource(), vtdu.getMode(), vtdu.getIsCode());
            }
        });
        //需要删除的数据
        diffListToDel(vtdus, names).stream().forEach(name -> {
@@ -208,4 +205,45 @@
            mediaService.addPath(vtdu.getName(), vtdu.getRtspSource(), vtdu.getMode(), vtdu.getIsCode());
        });
    }
    @Override
    public void addChanToVtdu(ArdCameras camera, ArdChannel channel) {
        String name = camera.getId() + "_" + channel.getChanNo();
        String rtspSource="";
        switch (camera.getFactory())
        {
            case "1": rtspSource = "rtsp://" + camera.getUsername() + ":" + camera.getPassword() + "@" + camera.getIp() + ":" + camera.getRtspPort() + "/h264/ch" + channel.getChanNo() + "/main/av_stream";break;
            case "2": rtspSource = "rtsp://" + camera.getUsername() + ":" + camera.getPassword() + "@" + camera.getIp() + ":" + camera.getRtspPort() + "/cam/realmonitor?channel=" + channel.getChanNo() + "&subtype=0";break;
        }
        //删除流媒体
        if (selectVtduByName(name) != null) {
            deleteVtduByName(name);
        }
        //添加到流媒体
        CameraCmd cmd = new CameraCmd(camera.getId(), channel.getChanNo());
        CameraSDK cameraSDK = cameraSDKFactory.createCameraSDK(camera.getFactory());
        Map<String, Object> videoCompressionCfg = cameraSDK.getVideoCompressionCfg(cmd);
        Vtdu vtdu = new Vtdu();
        if (videoCompressionCfg.get("videoEncType") != null) {
            if (videoCompressionCfg.get("videoEncType").equals("标准h264")) {
                vtdu.setIsCode("0");//默认不转码
            } else {
                vtdu.setIsCode("1");//默认转码
            }
        } else {
            vtdu.setIsCode("0");//默认不转码
        }
        vtdu.setRtspSource(rtspSource);
        vtdu.setName(name);
        vtdu.setMode("1");//默认CPU软解码
        vtdu.setCameraId(camera.getId());
        String rtspUrl = "rtsp://" + mediamtxHost + ":8554/" + name;
        String rtmpUrl = "rtmp://" + mediamtxHost + ":1935/" + name;
        String webrtcUrl = "http://" + mediamtxHost + ":8889/" + name;
        vtdu.setRtmpUrl(rtmpUrl);
        vtdu.setWebrtcUrl(webrtcUrl);
        vtdu.setRtspUrl(rtspUrl);
        insertVtdu(vtdu);
    }
}
ard-work/src/main/java/com/ruoyi/utils/forest/MediaClient.java
@@ -1,10 +1,7 @@
package com.ruoyi.utils.forest;
import com.dtflys.forest.annotation.*;
import com.ruoyi.media.domain.Conf;
import com.ruoyi.media.domain.Config;
import com.ruoyi.media.domain.Items;
import com.ruoyi.media.domain.Paths;
import com.ruoyi.media.domain.*;
import java.nio.file.Path;
@@ -38,14 +35,14 @@
    /**
     * èŽ·å–è·¯å¾„è¯¦æƒ…
     */
    @Get(url ="/config/paths/get/{name}")
    @Get(url = "/config/paths/get/{name}")
    public Conf getPathInfo(@Var("name") String name);
    /**
     * æŸ¥è¯¢æ‰€æœ‰è·¯å¾„
     */
    @Get("/paths/list")
    public Paths paths();
    @Get("/paths/list?page={pageNum}&itemsPerPage={pageSize}")
    public Paths paths(@Var("pageNum") Integer pageNum, @Var("pageSize") Integer pageSize);
    /**
     * æŸ¥è¯¢æ‰€æœ‰rtsp会话
@@ -57,19 +54,19 @@
     * æŒ‰sessionId查询rtsp会话
     */
    @Get("/rtspsessions/get/{sessionId}")
    public String getRtspsessionById(@Var("sessionId") String sessionId);
    public RtspSession getRtspsessionById(@Var("sessionId") String sessionId);
    /**
     * æŒ‰sessionId查询webrtc会话
     */
    @Get("/webrtcsessions/get/{sessionId}")
    public String getWebrtcsessionById(@Var("sessionId") String sessionId);
    public WebrtcSession getWebrtcsessionById(@Var("sessionId") String sessionId);
    /**
     * æŒ‰sessionId查询rtmp会话
     */
    @Get("/rtmpconns/get/{sessionId}")
    public String getRtmpsessionById(@Var("sessionId") String sessionId);
    public RtmpSession getRtmpsessionById(@Var("sessionId") String sessionId);
    /**
     * æŒ‰sessionId删除rtsp会话
@@ -89,9 +86,4 @@
    @Post("/webrtcsessions/kick/{sessionId}")
    public String kickWebrtcSessions(@Var("sessionId") String sessionId);
    /**
     * é…ç½®æµåª’体参数
     */
    @Post("/config/set")
    public String setConfig(@JSONBody Config config);
}
ard-work/src/main/java/com/ruoyi/utils/sdk/dhsdk/module/ConfigModule.java
@@ -81,10 +81,12 @@
        return result;
    }
    public static String getChannelName(NetSDKLib.LLong hLoginHandle,Integer channel) {
    //查询通道名称
    public static String getChannelName(NetSDKLib.LLong hLoginHandle, Integer channel) {
        String channelName = "";
        NetSDKLib.AV_CFG_ChannelName channelTitleName = new NetSDKLib.AV_CFG_ChannelName();
        if (ToolKits.GetDevConfig(hLoginHandle, channel, NetSDKLib.CFG_CMD_CHANNELTITLE, channelTitleName)) {
        if (ToolKits.GetDevConfig(hLoginHandle, channel - 1, NetSDKLib.CFG_CMD_CHANNELTITLE, channelTitleName)) {
            try {
                channelName = new String(channelTitleName.szName, "GBK");
            } catch (Exception e) {
@@ -95,6 +97,7 @@
        }
        return channelName;
    }
    public static boolean GetDevConfig(NetSDKLib.LLong hLoginHandle, int nChn, String strCmd, Structure cmdObject) {
        boolean result = true;
        IntByReference error = new IntByReference(0);
@@ -111,6 +114,7 @@
        return result;
    }
    //查询相机状态
    public static boolean queryCameraState(NetSDKLib.LLong hLoginHandle, Integer chanNum, Integer chanNo) {
        boolean bRet = false;
        try {
@@ -141,9 +145,9 @@
                stOut.read();
                ToolKits.GetPointerDataToStructArr(stOut.pCameraStateInfo, arrCameraStatus);  // å°†Pointer拷贝到数组内存
                final String[] connectionState = {"未知", "正在连接", "已连接", "未连接", "通道未配置,无信息", "通道有配置,但被禁用"};
               log.debug(connectionState[arrCameraStatus[chanNo - 1].emConnectionState]);
                if (connectionState[arrCameraStatus[chanNo - 1].emConnectionState].equals("已连接")) {
                    log.debug("通道" + arrCameraStatus[chanNo - 1].nChannel + connectionState[arrCameraStatus[chanNo - 1].emConnectionState]);
                    //log.debug("通道:" + arrCameraStatus[chanNo - 1].nChannel + "状态:" + connectionState[arrCameraStatus[chanNo - 1].emConnectionState]);
                    bRet = true;
                } else {
                    bRet = false;
@@ -205,6 +209,5 @@
        return result;
    }
}
ard-work/src/main/java/com/ruoyi/utils/sdk/dhsdk/service/impl/DahuaSDK.java
@@ -104,7 +104,7 @@
            NetSDKLib.LLong loginId = LoginModule.login(camera.getIp(), camera.getPort(), camera.getUsername(), camera.getPassword(), m_stDeviceInfo);
            if (loginId.longValue() <= 0) {
                camera.setChanNum(0);
                camera.setLoginId(-1);
                camera.setLoginId(-1l);
                camera.setState("0");
                ardCamerasService.updateArdCameras(camera);
                //删除管理通道
@@ -112,11 +112,11 @@
                log.error("设备[" + camera.getIp() + ":" + camera.getPort() + "]登录失败:" + getErrorCodePrint());
                return AjaxResult.warn(ErrorCode.getErrorCode(LoginModule.netsdk.CLIENT_GetLastError()));
            }
            log.debug("设备[" + camera.getIp() + ":" + camera.getPort() + "]登录成功:" + (int) loginId.longValue());
            camera.setState("1");
            camera.setChanNum(m_stDeviceInfo.byChanNum);
            camera.setStartDChan(1);
            camera.setLoginId((int) loginId.longValue());
            camera.setStartChan(1);
            camera.setLoginId(loginId.longValue());
            GlobalVariable.loginMap.put(camera.getId(), loginId);
            //获取最新通道
            List<ArdChannel> ardChannelList = getChannels(camera);
@@ -125,12 +125,11 @@
                ardChannelList.stream().forEach(channel -> {
                    ardChannelService.insertArdChannel(channel);
                });
                camera.setChanNum(ardChannelList.size());
                camera.setChannelList(ardChannelList);
                ardCamerasService.updateArdCameras(camera);
                //配置到流媒体
                addVtdu(camera);
                //通道批量添加到流媒体
                batchAddVtdu(camera);
            }
            ardCamerasService.updateArdCameras(camera);
            //创建引导队列
            createGuideQueue(camera);
            return AjaxResult.success("设备登录成功");
@@ -149,7 +148,7 @@
            NetSDKLib.LLong loginId = LoginModule.login(camera.getIp(), camera.getPort(), camera.getUsername(), camera.getPassword(), m_stDeviceInfo);
            if (loginId.longValue() <= 0) {
                camera.setChanNum(0);
                camera.setLoginId(-1);
                camera.setLoginId(-1l);
                camera.setState("0");
                ardCamerasService.updateArdCameras(camera);
                //删除管理通道
@@ -157,10 +156,11 @@
                log.error("设备[" + camera.getIp() + ":" + camera.getPort() + "]登录失败:" + getErrorCodePrint());
                return AjaxResult.warn(getErrorCodePrint());
            }
            log.debug("设备[" + camera.getIp() + ":" + camera.getPort() + "]登录成功:" + loginId);
            camera.setState("1");
            camera.setChanNum(m_stDeviceInfo.byChanNum);
            camera.setStartDChan(1);
            camera.setLoginId((int) loginId.longValue());
            camera.setStartChan(1);
            camera.setLoginId((Long) loginId.longValue());
            ardCamerasService.updateArdCameras(camera);
            GlobalVariable.loginMap.put(camera.getId(), loginId);
@@ -171,12 +171,11 @@
                ardChannelList.stream().forEach(channel -> {
                    ardChannelService.insertArdChannel(channel);
                });
                camera.setChanNum(ardChannelList.size());
                camera.setChannelList(ardChannelList);
                ardCamerasService.updateArdCameras(camera);
                //配置到流媒体
                addVtdu(camera);
                //通道批量添加到流媒体
                batchAddVtdu(camera);
            }
            ardCamerasService.updateArdCameras(camera);
            //创建引导队列
            createGuideQueue(camera);
            return AjaxResult.success("登录成功");
@@ -186,35 +185,13 @@
        }
    }
    //添加到流媒体
    private void addVtdu(ArdCameras camera) {
    //通道批量添加到流媒体
    public void batchAddVtdu(ArdCameras camera) {
        camera.getChannelList().stream().forEach(channel -> {
            String name = camera.getId() + "_" + channel.getChanNo();
            String rtspSource = "rtsp://" + camera.getUsername() + ":" + camera.getPassword() + "@" + camera.getIp() + ":" + camera.getRtspPort() + "/cam/realmonitor?channel=" + channel.getChanNo() + "&subtype=0";
             //删除流媒体
            if (vtduService.selectVtduByName(name) != null) {
                vtduService.deleteVtduByName(name);
            }
            //添加到流媒体
            CameraCmd cmd = new CameraCmd(camera.getId(), channel.getChanNo());
            Map<String, Object> videoCompressionCfg = getVideoCompressionCfg(cmd);
            Vtdu vtdu = new Vtdu();
            if (videoCompressionCfg.get("videoEncType") != null) {
                if (videoCompressionCfg.get("videoEncType").equals("标准h264")) {
                    vtdu.setIsCode("0");//默认不转码
                } else {
                    vtdu.setIsCode("1");//默认转码
                }
            } else {
                vtdu.setIsCode("0");//默认不转码
            }
            vtdu.setRtspSource(rtspSource);
            vtdu.setName(name);
            vtdu.setMode("1");//默认CPU软解码
            vtdu.setCameraId(camera.getId());
            vtduService.insertVtdu(vtdu);
            vtduService.addChanToVtdu(camera, channel);
        });
    }
    //创建引导队列
    private void createGuideQueue(ArdCameras camera) {
@@ -231,12 +208,14 @@
    //获取通道
    public List<ArdChannel> getChannels(ArdCameras camera) {
        if (camera.getLoginId().equals(-1)) {
            return new ArrayList<>();
        }
        LLong loginId = new LLong(camera.getLoginId());
        List<ArdChannel> ardChannelList = new ArrayList<>();
        for (int i = 1; i < camera.getChanNum() + 1; i++) {
            ArdChannel channel = new ArdChannel();
            String chanName = ConfigModule.getChannelName(loginId, i - 1).trim();
            log.debug("获取通道名称:" + chanName);
            String chanName = ConfigModule.getChannelName(loginId, i).trim();
            channel.setName(chanName.equals("") ? "通道" + i : chanName);
            channel.setDeviceId(camera.getId());
            channel.setChanNo(i);
ard-work/src/main/java/com/ruoyi/utils/sdk/hiksdk/lib/LoginResultCallBack.java
@@ -48,10 +48,10 @@
        if (dwResult == 1) {
            GlobalVariable.loginMap.put(camera.getId(), lUserID);
            log.debug(camera.getIp() + ":" + camera.getPort() + "登录成功");
            camera.setLoginId(lUserID);
            camera.setLoginId((long)lUserID);
            camera.setState("1");
            camera.setChanNum((int) lpDeviceinfo.byChanNum);
            camera.setStartDChan((int) lpDeviceinfo.byStartDChan);
            camera.setStartChan((int) lpDeviceinfo.byStartDChan);
            //获取最新通道
            List<ArdChannel> cameraChannelList = hikClientService.getChannels(camera);
            if (cameraChannelList.size() > 0) {
@@ -89,7 +89,7 @@
        } else {
            log.debug(camera.getIp() + ":" + camera.getPort() + "登录失败");
            camera.setChanNum(0);
            camera.setLoginId(-1);
            camera.setLoginId(-1l);
            camera.setState("0");
        }
        ardCamerasService.updateArdCameras(camera);
ard-work/src/main/java/com/ruoyi/utils/sdk/hiksdk/service/impl/HikvisionSDK.java
@@ -94,7 +94,7 @@
                String WIN_PATH = System.getProperty("user.dir") + File.separator + "ardLog" + File.separator + "logs" + File.separator;
                hCNetSDK.NET_DVR_SetLogToFile(3, WIN_PATH, true);
            } else {
                hCNetSDK.NET_DVR_SetLogToFile(3, "/home/ardLog/hiklog" , true);
                hCNetSDK.NET_DVR_SetLogToFile(3, "/home/ardLog/hiklog", true);
            }
            String m_sDeviceIP = camera.getIp();
            String m_sUsername = camera.getUsername();
@@ -125,7 +125,7 @@
            if (lUserID < 0) {
                int errorCode = hCNetSDK.NET_DVR_GetLastError();
                camera.setChanNum(0);
                camera.setLoginId(-1);
                camera.setLoginId(-1l);
                camera.setState("0");
                //删除管理通道
                ardChannelService.deleteArdChannelByDeviceId(camera.getId());
@@ -146,7 +146,7 @@
            }
            GlobalVariable.loginMap.put(camera.getId(), lUserID);
            GlobalVariable.loginCameraMap.put(lUserID, camera);
            camera.setLoginId(lUserID);
            camera.setLoginId((long)lUserID);
            camera.setState("1");
            int chanNum = m_strDeviceInfo.struDeviceV30.byChanNum;
            int startDchan = m_strDeviceInfo.struDeviceV30.byStartDChan + 1;
@@ -155,7 +155,7 @@
                startDchan = m_strDeviceInfo.struDeviceV30.byStartDChan;
            }
            camera.setChanNum(chanNum);
            camera.setStartDChan(startDchan);
            camera.setStartChan(startDchan);
            //获取最新通道
            List<ArdChannel> cameraChannelList = getChannels(camera);
            if (cameraChannelList.size() > 0) {
@@ -164,16 +164,16 @@
                    ardChannelService.insertArdChannel(channel);
                });
                camera.setChannelList(cameraChannelList);
                camera.setChanNum(cameraChannelList.size());
                ardCamerasService.updateArdCameras(camera);
                //添加到流媒体
                addVtdu(camera);
                //camera.setChanNum(cameraChannelList.size());
                //通道批量添加到流媒体
                batchAddVtdu(camera);
            }
            ardCamerasService.updateArdCameras(camera);
            //创建引导队列
            createGuideQueue(camera);
            return AjaxResult.success("设备登录成功");
        } catch (Exception ex) {
            log.error("设备登录异常" , ex);
            log.error("设备登录异常", ex);
            return AjaxResult.error("设备登录异常" + ex.getMessage());
        }
    }
@@ -200,7 +200,7 @@
                String WIN_PATH = System.getProperty("user.dir") + File.separator + "ardLog" + File.separator + "logs" + File.separator;
                hCNetSDK.NET_DVR_SetLogToFile(3, WIN_PATH, true);
            } else {
                hCNetSDK.NET_DVR_SetLogToFile(3, "/home/ardLog/hiklog" , true);
                hCNetSDK.NET_DVR_SetLogToFile(3, "/home/ardLog/hiklog", true);
            }
            String m_sDeviceIP = camera.getIp();
            String m_sUsername = camera.getUsername();
@@ -231,7 +231,7 @@
            if (lUserID < 0) {
                int errorCode = hCNetSDK.NET_DVR_GetLastError();
                camera.setChanNum(0);
                camera.setLoginId(-1);
                camera.setLoginId(-1l);
                camera.setState("0");
                //删除管理通道
                ardChannelService.deleteArdChannelByDeviceId(camera.getId());
@@ -254,7 +254,7 @@
            GlobalVariable.loginMap.put(camera.getId(), lUserID);
            GlobalVariable.loginCameraMap.put(lUserID, camera);
            camera.setLoginId(lUserID);
            camera.setLoginId((long)lUserID);
            camera.setState("1");
            int chanNum = m_strDeviceInfo.struDeviceV30.byChanNum;
            int startDchan = m_strDeviceInfo.struDeviceV30.byStartDChan + 1;
@@ -263,7 +263,7 @@
                startDchan = m_strDeviceInfo.struDeviceV30.byStartDChan;
            }
            camera.setChanNum(chanNum);
            camera.setStartDChan(startDchan);
            camera.setStartChan(startDchan);
            //获取最新通道
            List<ArdChannel> cameraChannelList = getChannels(camera);
            if (cameraChannelList.size() > 0) {
@@ -272,16 +272,16 @@
                    ardChannelService.insertArdChannel(channel);
                });
                camera.setChannelList(cameraChannelList);
                camera.setChanNum(cameraChannelList.size());
                ardCamerasService.updateArdCameras(camera);
                //添加到流媒体
                addVtdu(camera);
                //camera.setChanNum(cameraChannelList.size());
                //通道批量添加到流媒体
                batchAddVtdu(camera);
            }
            ardCamerasService.updateArdCameras(camera);
            //创建引导队列
            createGuideQueue(camera);
            return AjaxResult.success("设备登录成功");
        } catch (Exception ex) {
            log.error("注册设备异常" , ex);
            log.error("注册设备异常", ex);
            return AjaxResult.error("注册设备异常" + ex.getMessage());
        }
    }
@@ -299,36 +299,19 @@
        }
    }
    //添加到流媒体
    private void addVtdu(ArdCameras camera) {
    //通道批量添加到流媒体
    public void batchAddVtdu(ArdCameras camera) {
        try {
            camera.getChannelList().stream().forEach(channel->{
                String name = camera.getId() + "_" + channel.getChanNo();
                String rtspSource = "rtsp://" + camera.getUsername() + ":" + camera.getPassword() + "@" + camera.getIp() + ":" + camera.getRtspPort() + "/h264/ch" + channel.getChanNo() + "/main/av_stream";
                //删除流媒体
                if (vtduService.selectVtduByName(name) != null) {
                    vtduService.deleteVtduByName(name);
                }
                //添加到流媒体
                Vtdu vtdu = new Vtdu();
                vtdu.setRtspSource(rtspSource);
                vtdu.setName(name);
                CameraCmd cmd = new CameraCmd(camera.getId(), channel.getChanNo());
                Map<String, Object> videoCompressionCfg = getVideoCompressionCfg(cmd);
                if (videoCompressionCfg.get("videoEncType").equals("标准h264")) {
                    vtdu.setIsCode("0");//默认不转码
                } else {
                    vtdu.setIsCode("1");//默认转码
                }
                vtdu.setMode("1");//默认CPU软解码
                vtdu.setCameraId(camera.getId());
                vtduService.insertVtdu(vtdu);
            camera.getChannelList().stream().forEach(channel -> {
                vtduService.addChanToVtdu(camera, channel);
            });
        } catch (Exception ex) {
            log.error("通道添加到流媒体异常:" + ex.getMessage());
        }
    }
    /**
     * @描述 æ³¨é”€ç™»å½•
@@ -875,10 +858,10 @@
                        nFrameRate = "未知";
                        break;
                }
                map.put("resolution" , resolution);//分辨率
                map.put("videoBitrate" , videoBitrate);//比特率
                map.put("videoEncType" , videoEncType);//编码
                map.put("nFrameRate" , nFrameRate);//帧率
                map.put("resolution", resolution);//分辨率
                map.put("videoBitrate", videoBitrate);//比特率
                map.put("videoEncType", videoEncType);//编码
                map.put("nFrameRate", nFrameRate);//帧率
            } else {
                int code = hCNetSDK.NET_DVR_GetLastError();
@@ -931,9 +914,9 @@
        double z = d.setScale(1, BigDecimal.ROUND_HALF_UP).doubleValue();
        //log.debug("T垂直参数为: " + p + "P水平参数为: " + t + "Z变倍参数为: " + z);
        Map<String, Object> ptzMap = new HashMap<>();
        ptzMap.put("p" , p);
        ptzMap.put("t" , t);
        ptzMap.put("z" , z);
        ptzMap.put("p", p);
        ptzMap.put("t", t);
        ptzMap.put("z", z);
        return AjaxResult.success(ptzMap);
    }
@@ -980,9 +963,9 @@
            float fTilt = lpPTZAbsoluteEX_cfg.struPTZCtrl.fTilt;
            float t = fTilt < 0 ? fTilt + 360 : fTilt;
            float z = lpPTZAbsoluteEX_cfg.struPTZCtrl.fZoom;
            ptzMap.put("p" , p);
            ptzMap.put("t" , t);
            ptzMap.put("z" , z);
            ptzMap.put("p", p);
            ptzMap.put("t", t);
            ptzMap.put("z", z);
            return AjaxResult.success(ptzMap);
        } catch (Exception ex) {
            log.error("获取高精度PTZ绝对位置异常:" + ex.getMessage());
@@ -1087,7 +1070,7 @@
            return AjaxResult.success("设置高精度PTZ参数成功");
        } catch (Exception ex) {
            log.error("设置高精度PTZ参数异常" , ex);
            log.error("设置高精度PTZ参数异常", ex);
            return AjaxResult.error("设置高精度PTZ参数异常:" + ex);
        }
    }
@@ -1125,7 +1108,7 @@
                log.error("设置ptz失败,请稍后重试" + code);
                return AjaxResult.warn("设置ptz失败:" + SdkErrorCodeEnum.getDescByCode(code) + "(" + code + ")");
            }
            return AjaxResult.success("引导坐标成功",correctPitch);
            return AjaxResult.success("引导坐标成功", correctPitch);
        } catch (Exception ex) {
            log.error("引导坐标异常:" + ex.getMessage());
            return AjaxResult.error("引导坐标异常:" + ex.getMessage());
@@ -1234,12 +1217,12 @@
            String wZoomPosMax = df.format((float) Integer.parseInt(Integer.toHexString(m_ptzPosCurrent.wZoomPosMax)) / 10);
            String wZoomPosMin = df.format((float) Integer.parseInt(Integer.toHexString(m_ptzPosCurrent.wZoomPosMin)) / 10);
            Map<String, Object> ptzScopeMap = new HashMap<>();
            ptzScopeMap.put("pMax" , wPanPosMax);
            ptzScopeMap.put("pMin" , wPanPosMin);
            ptzScopeMap.put("tMax" , wTiltPosMax);
            ptzScopeMap.put("tMin" , wTiltPosMin);
            ptzScopeMap.put("zMax" , wZoomPosMax);
            ptzScopeMap.put("zMin" , wZoomPosMin);
            ptzScopeMap.put("pMax", wPanPosMax);
            ptzScopeMap.put("pMin", wPanPosMin);
            ptzScopeMap.put("tMax", wTiltPosMax);
            ptzScopeMap.put("tMin", wTiltPosMin);
            ptzScopeMap.put("zMax", wZoomPosMax);
            ptzScopeMap.put("zMin", wZoomPosMin);
            return AjaxResult.success(ptzScopeMap);
        }
    }
@@ -1335,7 +1318,7 @@
        boolean bool = hCNetSDK.NET_DVR_SetDVRConfig(userId, NET_DVR_SET_CCDPARAMCFG, chanNo, point, struDayNigh.size());
        if (!bool) {
            int code = hCNetSDK.NET_DVR_GetLastError();
            log.error("切换红外失败 ErrorCode:{},ErrorInfo:{}" , code, SdkErrorCodeEnum.getDescByCode(code));
            log.error("切换红外失败 ErrorCode:{},ErrorInfo:{}", code, SdkErrorCodeEnum.getDescByCode(code));
            return AjaxResult.warn("切换红外失败:" + SdkErrorCodeEnum.getDescByCode(code) + "(" + code + ")");
        }
        log.debug("切换红外成功");
@@ -1848,6 +1831,9 @@
    //获取IP通道
    public List<ArdChannel> getChannels(ArdCameras camera) {
        if (camera.getLoginId().equals(-1)) {
            return new ArrayList<>();
        }
        //获取通道
        List<ArdChannel> channelList = new ArrayList<>();
        try {
@@ -1856,19 +1842,19 @@
            m_strIpparaCfg.write();
            //lpIpParaConfig æŽ¥æ”¶æ•°æ®çš„缓冲指针
            Pointer lpIpParaConfig = m_strIpparaCfg.getPointer();
            boolean bRet = hCNetSDK.NET_DVR_GetDVRConfig(camera.getLoginId(), HCNetSDK.NET_DVR_GET_IPPARACFG_V40, 0, lpIpParaConfig, m_strIpparaCfg.size(), ibrBytesReturned);
            boolean bRet = hCNetSDK.NET_DVR_GetDVRConfig(camera.getLoginId().intValue(), HCNetSDK.NET_DVR_GET_IPPARACFG_V40, 0, lpIpParaConfig, m_strIpparaCfg.size(), ibrBytesReturned);
            m_strIpparaCfg.read();
            //log.debug("起始数字通道号:" + m_strIpparaCfg.dwStartDChan);//m_strIpparaCfg.dwDChanNum
            for (int iChannum = 0; iChannum < camera.getChanNum(); iChannum++) {
                ArdChannel channel = new ArdChannel();
                int chanNo = iChannum + camera.getStartDChan();
                int chanNo = iChannum + camera.getStartChan();
                HCNetSDK.NET_DVR_PICCFG_V40 strPicCfg = new HCNetSDK.NET_DVR_PICCFG_V40();
                strPicCfg.dwSize = strPicCfg.size();
                strPicCfg.write();
                Pointer pStrPicCfg = strPicCfg.getPointer();
                NativeLong lChannel = new NativeLong(chanNo);
                IntByReference pInt = new IntByReference(0);
                boolean b_GetPicCfg = hCNetSDK.NET_DVR_GetDVRConfig(camera.getLoginId(), HCNetSDK.NET_DVR_GET_PICCFG_V40, lChannel.intValue(), pStrPicCfg, strPicCfg.size(), pInt);
                boolean b_GetPicCfg = hCNetSDK.NET_DVR_GetDVRConfig(camera.getLoginId().intValue(), HCNetSDK.NET_DVR_GET_PICCFG_V40, lChannel.intValue(), pStrPicCfg, strPicCfg.size(), pInt);
                if (!b_GetPicCfg) {
                    // log.error("获取图像参数失败,错误码:" + hCNetSDK.NET_DVR_GetLastError());
                }
@@ -1932,11 +1918,11 @@
        }
        struGisInfo.read();
        Map<String, Object> map = new HashMap<>();
        map.put("p" , struGisInfo.struPtzPos.fPanPos);
        map.put("t" , struGisInfo.struPtzPos.fTiltPos < 0 ? struGisInfo.struPtzPos.fTiltPos + 360 : struGisInfo.struPtzPos.fTiltPos);
        map.put("z" , struGisInfo.struPtzPos.fZoomPos);
        map.put("fHorFieldAngle" , struGisInfo.fHorizontalValue);// æ°´å¹³è§†åœºè§’
        map.put("fVerFieldAngle" , struGisInfo.fVerticalValue);// åž‚直视场角
        map.put("p", struGisInfo.struPtzPos.fPanPos);
        map.put("t", struGisInfo.struPtzPos.fTiltPos < 0 ? struGisInfo.struPtzPos.fTiltPos + 360 : struGisInfo.struPtzPos.fTiltPos);
        map.put("z", struGisInfo.struPtzPos.fZoomPos);
        map.put("fHorFieldAngle", struGisInfo.fHorizontalValue);// æ°´å¹³è§†åœºè§’
        map.put("fVerFieldAngle", struGisInfo.fVerticalValue);// åž‚直视场角
        return AjaxResult.success(map);
    }
@@ -1996,7 +1982,7 @@
                return AjaxResult.warn("本地录像取流失败:" + SdkErrorCodeEnum.getDescByCode(code) + "(" + code + ")");
            }
            log.debug("本地录像开始");
            return AjaxResult.success("录像开始" , lRealHandle);
            return AjaxResult.success("录像开始", lRealHandle);
        } catch (Exception ex) {
            log.error("本地录像开始异常" + ex.getMessage());
            return AjaxResult.error("本地录像开始异常" + ex.getMessage());
ard-work/src/main/resources/mapper/device/ArdCamerasMapper.xml
@@ -15,6 +15,7 @@
        <result property="gdtype" column="gdtype"/>
        <result property="factory" column="factory"/>
        <result property="chanNum" column="channel"/>
        <result property="startChan" column="start_chan"/>
        <result property="longitude" column="longitude"/>
        <result property="latitude" column="latitude"/>
        <result property="altitude" column="altitude"/>
@@ -48,6 +49,7 @@
               c.gdtype,
               c.factory,
               c.channel,
               c.start_chan,
               c.longitude,
               c.latitude,
               c.altitude,
@@ -86,6 +88,7 @@
            <if test="gdtype != null  and gdtype != ''">and c.gdtype = #{gdtype}</if>
            <if test="factory != null  and factory != ''">and c.factory = #{factory}</if>
            <if test="chanNum != null ">and c.channel = #{chanNum}</if>
            <if test="startChan != null ">and c.start_chan = #{startChan}</if>
            <if test="longitude != null ">and c.longitude = #{longitude}</if>
            <if test="latitude != null ">and c.latitude = #{latitude}</if>
            <if test="altitude != null ">and c.altitude = #{altitude}</if>
@@ -123,6 +126,7 @@
            <if test="gdtype != null  and gdtype != ''">and c.gdtype = #{gdtype}</if>
            <if test="factory != null  and factory != ''">and c.factory = #{factory}</if>
            <if test="chanNum != null ">and c.channel = #{chanNum}</if>
            <if test="startChan != null ">and c.start_chan= #{startChan}</if>
            <if test="longitude != null ">and c.longitude = #{longitude}</if>
            <if test="latitude != null ">and c.latitude = #{latitude}</if>
            <if test="altitude != null ">and c.altitude = #{altitude}</if>
@@ -163,6 +167,7 @@
            <if test="factory != null">factory,</if>
            <if test="towerId != null">tower_id,</if>
            <if test="chanNum != null">channel,</if>
            <if test="startChan != null">start_chan,</if>
            <if test="longitude != null">longitude,</if>
            <if test="latitude != null">latitude,</if>
            <if test="altitude != null">altitude,</if>
@@ -197,6 +202,7 @@
            <if test="factory != null">#{factory},</if>
            <if test="towerId != null">#{towerId},</if>
            <if test="chanNum != null">#{chanNum},</if>
            <if test="startChan != null">#{startChan},</if>
            <if test="longitude != null">#{longitude},</if>
            <if test="latitude != null">#{latitude},</if>
            <if test="altitude != null">#{altitude},</if>
@@ -234,6 +240,7 @@
            <if test="factory != null">factory = #{factory},</if>
            <if test="towerId != null">tower_id = #{towerId},</if>
            <if test="chanNum != null">channel = #{chanNum},</if>
            <if test="startChan != null">start_chan = #{startChan},</if>
            <if test="longitude != null">longitude = #{longitude},</if>
            <if test="latitude != null">latitude = #{latitude},</if>
            <if test="altitude != null">altitude = #{altitude},</if>
ard-work/src/main/resources/templates/test.html
@@ -916,10 +916,32 @@
    let webrtcClient;
    //whep操作方法
    const restartPause = 2000;
    const retryPause = 2000;
    const video = document.getElementById('video');
    const message = document.getElementById('message');
    let nonAdvertisedCodecs = [];
    let pc = null;
    let restartTimeout = null;
    let sessionUrl = '';
    let offerData = '';
    let queuedCandidates = [];
    let defaultControls = false;
    const setMessage = (str) => {
        if (str !== '') {
            video.controls = false;
        } else {
            video.controls = defaultControls;
        }
        message.innerText = str;
    };
    const unquoteCredential = (v) => (
        JSON.parse(`"${v}"`)
    );
    const linkToIceServers = (links) => (
        (links !== null) ? links.split(', ').map((link) => {
            const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);
@@ -930,20 +952,21 @@
            if (m[3] !== undefined) {
                ret.username = unquoteCredential(m[3]);
                ret.credential = unquoteCredential(m[4]);
                ret.credentialType = "password";
                ret.credentialType = 'password';
            }
            return ret;
        }) : []
    );
    const parseOffer = (offer) => {
    const parseOffer = (sdp) => {
        const ret = {
            iceUfrag: '',
            icePwd: '',
            medias: [],
        };
        for (const line of offer.split('\r\n')) {
        for (const line of sdp.split('\r\n')) {
            if (line.startsWith('m=')) {
                ret.medias.push(line.slice('m='.length));
            } else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) {
@@ -955,7 +978,131 @@
        return ret;
    };
    const generateSdpFragment = (offerData, candidates) => {
    const enableStereoPcmau = (section) => {
        let lines = section.split('\r\n');
        lines[0] += ' 118';
        lines.splice(lines.length - 1, 0, 'a=rtpmap:118 PCMU/8000/2');
        lines.splice(lines.length - 1, 0, 'a=rtcp-fb:118 transport-cc');
        lines[0] += ' 119';
        lines.splice(lines.length - 1, 0, 'a=rtpmap:119 PCMA/8000/2');
        lines.splice(lines.length - 1, 0, 'a=rtcp-fb:119 transport-cc');
        return lines.join('\r\n');
    };
    const enableMultichannelOpus = (section) => {
        let lines = section.split('\r\n');
        lines[0] += " 112";
        lines.splice(lines.length - 1, 0, "a=rtpmap:112 multiopus/48000/3");
        lines.splice(lines.length - 1, 0, "a=fmtp:112 channel_mapping=0,2,1;num_streams=2;coupled_streams=1");
        lines.splice(lines.length - 1, 0, "a=rtcp-fb:112 transport-cc");
        lines[0] += " 113";
        lines.splice(lines.length - 1, 0, "a=rtpmap:113 multiopus/48000/4");
        lines.splice(lines.length - 1, 0, "a=fmtp:113 channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2");
        lines.splice(lines.length - 1, 0, "a=rtcp-fb:113 transport-cc");
        lines[0] += " 114";
        lines.splice(lines.length - 1, 0, "a=rtpmap:114 multiopus/48000/5");
        lines.splice(lines.length - 1, 0, "a=fmtp:114 channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2");
        lines.splice(lines.length - 1, 0, "a=rtcp-fb:114 transport-cc");
        lines[0] += " 115";
        lines.splice(lines.length - 1, 0, "a=rtpmap:115 multiopus/48000/6");
        lines.splice(lines.length - 1, 0, "a=fmtp:115 channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2");
        lines.splice(lines.length - 1, 0, "a=rtcp-fb:115 transport-cc");
        lines[0] += " 116";
        lines.splice(lines.length - 1, 0, "a=rtpmap:116 multiopus/48000/7");
        lines.splice(lines.length - 1, 0, "a=fmtp:116 channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4");
        lines.splice(lines.length - 1, 0, "a=rtcp-fb:116 transport-cc");
        lines[0] += " 117";
        lines.splice(lines.length - 1, 0, "a=rtpmap:117 multiopus/48000/8");
        lines.splice(lines.length - 1, 0, "a=fmtp:117 channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4");
        lines.splice(lines.length - 1, 0, "a=rtcp-fb:117 transport-cc");
        return lines.join('\r\n');
    };
    const enableL16 = (section) => {
        let lines = section.split('\r\n');
        lines[0] += " 120";
        lines.splice(lines.length - 1, 0, "a=rtpmap:120 L16/8000/2");
        lines.splice(lines.length - 1, 0, "a=rtcp-fb:120 transport-cc");
        lines[0] += " 121";
        lines.splice(lines.length - 1, 0, "a=rtpmap:121 L16/16000/2");
        lines.splice(lines.length - 1, 0, "a=rtcp-fb:121 transport-cc");
        lines[0] += " 122";
        lines.splice(lines.length - 1, 0, "a=rtpmap:122 L16/48000/2");
        lines.splice(lines.length - 1, 0, "a=rtcp-fb:122 transport-cc");
        return lines.join('\r\n');
    };
    const enableStereoOpus = (section) => {
        let opusPayloadFormat = '';
        let lines = section.split('\r\n');
        for (let i = 0; i < lines.length; i++) {
            if (lines[i].startsWith('a=rtpmap:') && lines[i].toLowerCase().includes('opus/')) {
                opusPayloadFormat = lines[i].slice('a=rtpmap:'.length).split(' ')[0];
                break;
            }
        }
        if (opusPayloadFormat === '') {
            return section;
        }
        for (let i = 0; i < lines.length; i++) {
            if (lines[i].startsWith('a=fmtp:' + opusPayloadFormat + ' ')) {
                if (!lines[i].includes('stereo')) {
                    lines[i] += ';stereo=1';
                }
                if (!lines[i].includes('sprop-stereo')) {
                    lines[i] += ';sprop-stereo=1';
                }
            }
        }
        return lines.join('\r\n');
    };
    const editOffer = (sdp) => {
        const sections = sdp.split('m=');
        for (let i = 0; i < sections.length; i++) {
            if (sections[i].startsWith('audio')) {
                sections[i] = enableStereoOpus(sections[i]);
                if (nonAdvertisedCodecs.includes('pcma/8000/2')) {
                    sections[i] = enableStereoPcmau(sections[i]);
                }
                if (nonAdvertisedCodecs.includes('multiopus/48000/6')) {
                    sections[i] = enableMultichannelOpus(sections[i]);
                }
                if (nonAdvertisedCodecs.includes('L16/48000/2')) {
                    sections[i] = enableL16(sections[i]);
                }
                break;
            }
        }
        return sections.join('m=');
    };
    const generateSdpFragment = (od, candidates) => {
        const candidatesByMedia = {};
        for (const candidate of candidates) {
            const mid = candidate.sdpMLineIndex;
@@ -965,12 +1112,12 @@
            candidatesByMedia[mid].push(candidate);
        }
        let frag = 'a=ice-ufrag:' + offerData.iceUfrag + '\r\n'
            + 'a=ice-pwd:' + offerData.icePwd + '\r\n';
        let frag = 'a=ice-ufrag:' + od.iceUfrag + '\r\n'
            + 'a=ice-pwd:' + od.icePwd + '\r\n';
        let mid = 0;
        for (const media of offerData.medias) {
        for (const media of od.medias) {
            if (candidatesByMedia[mid] !== undefined) {
                frag += 'm=' + media + '\r\n'
                    + 'a=mid:' + mid + '\r\n';
@@ -983,174 +1130,269 @@
        }
        return frag;
    }
    };
    class WHEPClient {
        constructor(whepUrl, videoId) {
            this.video = videoId;
            this.wurl = new URL('whep', whepUrl);
            this.pc = null;
            this.restartTimeout = null;
            this.eTag = '';
            this.queuedCandidates = [];
            this.start();
        }
    const loadStream = () => {
        requestICEServers();
    };
        start() {
            console.log("requesting ICE servers");
            fetch(this.wurl, {
                method: 'OPTIONS',
            })
                .then((res) => this.onIceServers(res))
    const supportsNonAdvertisedCodec = (codec, fmtp) => (
        new Promise((resolve, reject) => {
            const pc = new RTCPeerConnection({ iceServers: [] });
            pc.addTransceiver('audio', { direction: 'recvonly' });
            pc.createOffer()
                .then((offer) => {
                    if (offer.sdp.includes(' ' + codec)) { // codec is advertised, there's no need to add it manually
                        resolve(false);
                        return;
                    }
                    const sections = offer.sdp.split('m=audio');
                    const lines = sections[1].split('\r\n');
                    lines[0] += ' 118';
                    lines.splice(lines.length - 1, 0, 'a=rtpmap:118 ' + codec);
                    if (fmtp !== undefined) {
                        lines.splice(lines.length - 1, 0, 'a=fmtp:118 ' + fmtp);
                    }
                    sections[1] = lines.join('\r\n');
                    offer.sdp = sections.join('m=audio');
                    return pc.setLocalDescription(offer);
                })
                .then(() => {
                    return pc.setRemoteDescription(new RTCSessionDescription({
                        type: 'answer',
                        sdp: 'v=0\r\n'
                            + 'o=- 6539324223450680508 0 IN IP4 0.0.0.0\r\n'
                            + 's=-\r\n'
                            + 't=0 0\r\n'
                            + 'a=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\n'
                            + 'm=audio 9 UDP/TLS/RTP/SAVPF 118\r\n'
                            + 'c=IN IP4 0.0.0.0\r\n'
                            + 'a=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\n'
                            + 'a=ice-ufrag:29e036dc\r\n'
                            + 'a=sendonly\r\n'
                            + 'a=rtcp-mux\r\n'
                            + 'a=rtpmap:118 ' + codec + '\r\n'
                            + ((fmtp !== undefined) ? 'a=fmtp:118 ' + fmtp + '\r\n' : ''),
                    }));
                })
                .then(() => {
                    resolve(true);
                })
                .catch((err) => {
                    console.log('error: ' + err);
                    this.scheduleRestart();
                    resolve(false);
                })
                .finally(() => {
                    pc.close();
                });
        }
        })
    );
        onIceServers(res) {
            this.pc = new RTCPeerConnection({
                iceServers: linkToIceServers(res.headers.get('Link')),
    const getNonAdvertisedCodecs = () => {
        Promise.all([
            ['pcma/8000/2'],
            ['multiopus/48000/6', 'channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'],
            ['L16/48000/2']
        ].map((c) => supportsNonAdvertisedCodec(c[0], c[1]).then((r) => (r) ? c[0] : false)))
            .then((c) => c.filter((e) => e !== false))
            .then((codecs) => {
                nonAdvertisedCodecs = codecs;
                loadStream();
            });
    };
            const direction = "sendrecv";
            this.pc.addTransceiver("video", {direction});
            this.pc.addTransceiver("audio", {direction});
    const onError = (err) => {
        if (restartTimeout === null) {
            setMessage(err + ', retrying in some seconds');
            this.pc.onicecandidate = (evt) => this.onLocalCandidate(evt);
            this.pc.oniceconnectionstatechange = () => this.onConnectionState();
            if (pc !== null) {
                pc.close();
                pc = null;
            }
            this.pc.ontrack = (evt) => {
                console.log("new track:", evt.track.kind);
                document.getElementById(this.video).srcObject = evt.streams[0];
            };
            restartTimeout = window.setTimeout(() => {
                restartTimeout = null;
                loadStream();
            }, retryPause);
            this.pc.createOffer()
                .then((offer) => this.onLocalOffer(offer));
        }
        onLocalOffer(offer) {
            this.offerData = parseOffer(offer.sdp);
            this.pc.setLocalDescription(offer);
            console.log("sending offer");
            console.log(this.wurl);
            fetch(this.wurl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/sdp',
                },
                body: offer.sdp,
            })
                .then((res) => {
                    if (res.status !== 201) {
                        throw new Error('bad status code');
                    }
                    // this.eTag = res.headers.get('ETag');
                    this.eTag = res.headers.get("ETag") || res.headers.get('E-Tag');
                    return res.text();
                })
                .then((sdp) => this.onRemoteAnswer(new RTCSessionDescription({
                    type: 'answer',
                    sdp,
                })))
                .catch((err) => {
                    console.log('error: ' + err);
                    this.scheduleRestart();
            if (sessionUrl) {
                fetch(sessionUrl, {
                    method: 'DELETE',
                });
            }
            sessionUrl = '';
            queuedCandidates = [];
        }
    };
        onConnectionState() {
            if (this.restartTimeout !== null) {
                return;
            }
            console.log("peer connection state:", this.pc.iceConnectionState);
            switch (this.pc.iceConnectionState) {
                case "disconnected":
                    this.scheduleRestart();
            }
        }
        onRemoteAnswer(answer) {
            if (this.restartTimeout !== null) {
                return;
            }
            this.pc.setRemoteDescription(new RTCSessionDescription(answer));
            if (this.queuedCandidates.length !== 0) {
                this.sendLocalCandidates(this.queuedCandidates);
                this.queuedCandidates = [];
            }
        }
        onLocalCandidate(evt) {
            if (this.restartTimeout !== null) {
                return;
            }
            if (evt.candidate !== null) {
                if (this.eTag === '') {
                    this.queuedCandidates.push(evt.candidate);
                } else {
                    this.sendLocalCandidates([evt.candidate])
    const sendLocalCandidates = (candidates) => {
        fetch(sessionUrl + window.location.search, {
            method: 'PATCH',
            headers: {
                'Content-Type': 'application/trickle-ice-sdpfrag',
                'If-Match': '*',
            },
            body: generateSdpFragment(offerData, candidates),
        })
            .then((res) => {
                switch (res.status) {
                    case 204:
                        break;
                    case 404:
                        throw new Error('stream not found');
                    default:
                        throw new Error(`bad status code ${res.status}`);
                }
            }
        }
        sendLocalCandidates(candidates) {
            fetch(this.wurl, {
                method: 'PATCH',
                headers: {
                    'Content-Type': 'application/trickle-ice-sdpfrag',
                    'If-Match': this.eTag,
                },
                body: generateSdpFragment(this.offerData, candidates),
            })
                .then((res) => {
                    if (res.status !== 204) {
                        throw new Error('bad status code');
                    }
                })
                .catch((err) => {
                    console.log('error: ' + err);
                    this.scheduleRestart();
                });
            .catch((err) => {
                onError(err.toString());
            });
    };
    const onLocalCandidate = (evt) => {
        if (restartTimeout !== null) {
            return;
        }
        scheduleRestart() {
            if (this.restartTimeout !== null) {
                return;
        if (evt.candidate !== null) {
            if (sessionUrl === '') {
                queuedCandidates.push(evt.candidate);
            } else {
                sendLocalCandidates([evt.candidate])
            }
        }
    };
            if (this.pc !== null) {
                this.pc.close();
                this.pc = null;
            }
            this.restartTimeout = window.setTimeout(() => {
                this.restartTimeout = null;
                this.start();
            }, restartPause);
            this.eTag = '';
            this.queuedCandidates = [];
    const onRemoteAnswer = (sdp) => {
        if (restartTimeout !== null) {
            return;
        }
        stop() {
            if (this.pc) {
                try {
                    this.pc.close();
                } catch (e) {
                    console.log("Failure close peer connection:" + e);
        pc.setRemoteDescription(new RTCSessionDescription({
            type: 'answer',
            sdp,
        }))
            .then(() => {
                if (queuedCandidates.length !== 0) {
                    sendLocalCandidates(queuedCandidates);
                    queuedCandidates = [];
                }
                this.pc = null;
            }
            })
            .catch((err) => {
                onError(err.toString());
            });
    };
    const sendOffer = (offer) => {
        fetch(new URL('whep', window.location.href) + window.location.search, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/sdp',
            },
            body: offer.sdp,
        })
            .then((res) => {
                switch (res.status) {
                    case 201:
                        break;
                    case 404:
                        throw new Error('stream not found');
                    case 400:
                        return res.json().then((e) => { throw new Error(e.error); });
                    default:
                        throw new Error(`bad status code ${res.status}`);
                }
                sessionUrl = new URL(res.headers.get('location'), window.location.href).toString();
                return res.text()
                    .then((sdp) => onRemoteAnswer(sdp));
            })
            .catch((err) => {
                onError(err.toString());
            });
    };
    const createOffer = () => {
        pc.createOffer()
            .then((offer) => {
                offer.sdp = editOffer(offer.sdp);
                offerData = parseOffer(offer.sdp);
                pc.setLocalDescription(offer)
                    .then(() => {
                        sendOffer(offer);
                    })
                    .catch((err) => {
                        onError(err.toString());
                    });
            })
            .catch((err) => {
                onError(err.toString());
            });
    };
    const onConnectionState = () => {
        if (restartTimeout !== null) {
            return;
        }
    }
        if (pc.iceConnectionState === 'disconnected') {
            onError('peer connection closed');
        }
    };
    const onTrack = (evt) => {
        setMessage('');
        video.srcObject = evt.streams[0];
    };
    const requestICEServers = () => {
        fetch(new URL('whep', window.location.href) + window.location.search, {
            method: 'OPTIONS',
        })
            .then((res) => {
                pc = new RTCPeerConnection({
                    iceServers: linkToIceServers(res.headers.get('Link')),
                    // https://webrtc.org/getting-started/unified-plan-transition-guide
                    sdpSemantics: 'unified-plan',
                });
                const direction = 'sendrecv';
                pc.addTransceiver('video', { direction });
                pc.addTransceiver('audio', { direction });
                pc.onicecandidate = (evt) => onLocalCandidate(evt);
                pc.oniceconnectionstatechange = () => onConnectionState();
                pc.ontrack = (evt) => onTrack(evt);
                createOffer();
            })
            .catch((err) => {
                onError(err.toString());
            });
    };
    const parseBoolString = (str, defaultVal) => {
        str = (str || '');
        if (['1', 'yes', 'true'].includes(str.toLowerCase())) {
            return true;
        }
        if (['0', 'no', 'false'].includes(str.toLowerCase())) {
            return false;
        }
        return defaultVal;
    };
    const loadAttributesFromQuery = () => {
        const params = new URLSearchParams(window.location.search);
        video.controls = parseBoolString(params.get('controls'), true);
        video.muted = parseBoolString(params.get('muted'), true);
        video.autoplay = parseBoolString(params.get('autoplay'), true);
        video.playsInline = parseBoolString(params.get('playsinline'), true);
        defaultControls = video.controls;
    };
    function realView(whepUrl, videoId) {
        console.log(whepUrl)
ard-work/src/main/resources/templates/test1.html
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1161 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>测试页</title>
    <script th:src="@{/js/jquery-3.6.4.min.js}"></script>
    <link rel="stylesheet" th:href="@{/css/bootstrap.css}"/>
    <script th:src="@{/js/bootstrap.js}"></script>
    <style>
        .top-buffer {
            margin-top: 10px;
        }
        .container {
            border: 2px solid #1b6d85;
            padding: 15px;
        }
    </style>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-5">
            <div class="row top-buffer">
                è®¾å¤‡ï¼š<select id="selectDev" style="width: 330px;">
            </select>
            </div>
            <div class="row top-buffer">
                é€šé“:<select id="selectChn" style="width: 330px;">
            </select>
            </div>
            <div class="row top-buffer">
                <div class="col-md-1 col-md-offset-1">
                    <button id="up" type="button" class="btn btn-primary">上</button>
                </div>
                <div class="col-md-6 col-md-offset-2">
                    <div class="btn-group" role="group">
                        <button id="controlZoomIn" type="button" class="btn btn-primary">调焦-</button>
                        <button id="controlZoomOut" type="button" class="btn btn-primary">调焦+</button>
                    </div>
                </div>
            </div>
            <div class="row ">
                <div class="col-md-1">
                    <button id="left" type="button" class="btn btn-primary">å·¦</button>
                </div>
                <div class="col-md-1 col-md-offset-1">
                    <button id="right" type="button" class="btn btn-primary">右</button>
                </div>
                <div class="col-md-6 col-md-offset-1">
                    <div class="btn-group" role="group">
                        <button id="controlFocusNear" type="button" class="btn btn-primary">聚焦-</button>
                        <button id="controlFocusFar" type="button" class="btn btn-primary">聚焦+</button>
                    </div>
                </div>
            </div>
            <div class="row ">
                <div class="col-md-1 col-md-offset-1">
                    <button id="down" type="button" class="btn btn-primary">下</button>
                </div>
                <div class="col-md-6 col-md-offset-2">
                    <div class="btn-group" role="group">
                        <button id="controlIrisOpen" type="button" class="btn btn-primary">光圈-</button>
                        <button id="controlIrisClose" type="button" class="btn btn-primary">光圈+</button>
                    </div>
                </div>
            </div>
            <div class="row ">
                <div class="col-md-10">
                    <div class="row top-buffer">
                        <div class="input-group">
                            <span class="input-group-addon">目的坐标值:</span>
                            <input id="targetPostion" class="form-control" placeholder="目的坐标"/>
                            <button id="setTargetPostion" type="button" class="btn btn-default">指向坐标</button>
                        </div>
                        <div class="input-group">
                            <span class="input-group-addon">P值:</span>
                            <input id="p" class="form-control" placeholder="请输入P值"/>
                        </div>
                        <div class="input-group">
                            <span class="input-group-addon">T值:</span>
                            <input id="t" class="form-control" placeholder="请输入T值"/>
                        </div>
                        <div class="input-group">
                            <span class="input-group-addon">Z值:</span>
                            <input id="z" class="form-control" placeholder="请输入Z值"/>
                        </div>
                    </div>
                    <div class="row top-buffer">
                        <div class="btn-group" role="group">
                            <button id="getPTZ" type="button" class="btn btn-default">获取ptz</button>
                            <button id="setPTZ" type="button" class="btn btn-default">设置ptz</button>
                            <button id="setPreset" type="button" class="btn btn-default">设预置点</button>
                            <button id="gotoPreset" type="button" class="btn btn-default">调预置点</button>
                            <button id="setZeroPTZ" type="button" class="btn btn-default">设置零方位角</button>
                        </div>
                    </div>
                    <div class="row top-buffer">
                        <div class="btn-group" role="group">
                            <button id="FocusMode" type="button" class="btn btn-default">手动聚焦</button>
                            <div id="focusDiv" class="input-group">
                                <span class="input-group-addon">聚焦值:</span>
                                <input id="focus" class="form-control" placeholder="聚焦值"/>
                            </div>
                            <button id="getFocusPos" type="button" class="btn btn-default">获取聚焦值</button>
                            <button id="setFocusPos" type="button" class="btn btn-default">设置聚焦值</button>
                        </div>
                    </div>
                    <div class="row top-buffer">
                        <div class="btn-group" role="group">
                            <button id="WiperPwron" type="button" class="btn btn-default">开启雨刷</button>
                            <button id="Defogcfg" type="button" class="btn btn-default">开启透雾</button>
                            <button id="Infrarecfg" type="button" class="btn btn-default">开启红外</button>
                            <button id="HeateRpwron" type="button" class="btn btn-default">开启云台加热</button>
                            <button id="CameraDeicing" type="button" class="btn btn-default">开启镜头加热</button>
                        </div>
                    </div>
                    <div class="row top-buffer">
                        <div class="btn-group" role="group">
                            <button id="voice" type="button" class="btn btn-default">开始语音对讲</button>
                            <button id="record" type="button" class="btn btn-default">开始录像</button>
                            <button id="saveCutPic" type="button" class="btn btn-default">存储抓图</button>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="col-md-6">
            <div class="row">
                <div class="row top-buffer">
                    <button id="preview" type="button" class="btn btn-primary">预览</button>
                    <button id="previewStop" type="button" class="btn btn-primary">停止</button>
                </div>
                <div class="row top-buffer">
                    <video id="video" muted autoplay loop controls
                           style="width: 100%; height: 360px; object-fit: fill; border: 2px solid #3498db;"/>
                </div>
                <div class="row">
                    <img class="thumbnail" id="imgContainer"
                         style="width: 100%; height: 360px; border: 2px solid #3498db;"/>
                </div>
            </div>
        </div>
    </div>
</div>
<script th:inline="javascript">
    var cameraId, chanNo,opt, optOpen, optClose, token;
    window.onload = function () {
        console.log(RTCRtpReceiver.getCapabilities('video').codecs)
        opt = {"username": "admin", "password": "admin123"};
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            },
            url: "../login",
            type: "post",
            dataType: "json",
            data: JSON.stringify(opt),
            success: function (data) {
                token = data.token;
                getDeviceList();// èŽ·å–è®¾å¤‡åˆ—è¡¨
            }
        })
        // åˆå§‹åŒ–内容
        console.log(cameraMap);
    }
    //获取设备
    function getDeviceList() {
        $.ajax({
            url: "../cameraSdk/list",
            type: "get",
            success: function (data) {
                console.log(data);
                var arr = data.data;
                for (var i = 0; i < arr.length; i++) {
                    console.log(arr[i].id);
                    console.log(arr[i].name)
                    var camera = {
                        name: arr[i].name,
                        factory: arr[i].factory,
                        ipaddr: arr[i].ip,
                        username: arr[i].username,
                        password: arr[i].password,
                        port: arr[i].rtspPort,
                        longitude: arr[i].longitude,
                        latitude: arr[i].latitude,
                        altitude: arr[i].altitude
                    };
                    cameraMap.set(arr[i].id, camera);
                    //先创建好select里面的option元素
                    var option = $("<option>");
                    //给option的text赋值,这就是你点开下拉框能够看到的东西
                    $(option).val(arr[i].id);
                    $(option).text(arr[i].name);
                    //获取select ä¸‹æ‹‰æ¡†å¯¹è±¡,并将option添加进select
                    $('#selectDev').append(option);
                }
                $("#selectDev").trigger("change");
            }
        })
    }
    //选择设备
    $("#selectDev").change(function () {
        // åœ¨è¿™é‡Œå¤„理选择事件
        var cameraId = $(this).find("option:selected").val();
        var name = $(this).find("option:selected").text();
        getChannelList(cameraId);
        console.log("选择了:" + cameraId + "---" + name);
    });
    //获取通道
    function getChannelList(cameraId) {
        console.log(cameraId)
        var myEntity = {
            deviceId: cameraId,
            pageNum: 1,
            pageSize: 64
        }
        var queryString = $.param(myEntity);
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../device/channel/list?" + queryString,
            type: "get",
            success: function (data) {
                console.log(data);
                var arr = data.rows;
                $('#selectChn').empty();
                for (var i = 0; i < arr.length; i++) {
                    console.log(arr[i].chanNo);
                    console.log(arr[i].name);
                    //先创建好select里面的option元素
                    var option = document.createElement("option");
                    //给option的text赋值,这就是你点开下拉框能够看到的东西
                    $(option).text(arr[i].name);
                    $(option).val(arr[i].chanNo);
                    //获取select ä¸‹æ‹‰æ¡†å¯¹è±¡,并将option添加进select
                    $('#selectChn').append(option);
                }
            }
        })
    }
    //预览
    $('#preview').click(() => {
        var cameraId = $('#selectDev option:selected').val();
        var chanNo = $('#selectChn option:selected').val();
        console.log(cameraId + " " + chanNo)
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../vtdu/media/" + cameraId + "_" + chanNo,
            type: "get",
            dataType: "json",
            success: function (data) {
                realView(data.data.webrtcUrl + "/", "video");
            }
        })
    });
    //停止
    $('#previewStop').click(() => {
        webrtcClient.stop();
    });
    //云台上下左右
    $("#up").mousedown(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 2;
        var enable = true;
        commondMethod(url, code, enable);
    })
    $("#up").mouseup(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 2;
        var enable = false;
        commondMethod(url, code, enable);
    })
    $("#down").mousedown(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 8;
        var enable = true;
        commondMethod(url, code, enable);
    })
    $("#down").mouseup(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 8;
        var enable = false;
        commondMethod(url, code, enable);
    })
    $("#left").mousedown(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 4;
        var enable = true;
        commondMethod(url, code, enable);
    })
    $("#left").mouseup(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 4;
        var enable = false;
        commondMethod(url, code, enable);
    })
    $("#right").mousedown(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 6;
        var enable = true;
        commondMethod(url, code, enable);
    })
    $("#right").mouseup(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 6;
        var enable = false;
        commondMethod(url, code, enable);
    })
    //变倍
    $("#controlZoomIn").mousedown(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 10;
        var enable = true;
        commondMethod(url, code, enable);
    })
    $("#controlZoomIn").mouseup(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 10;
        var enable = false;
        commondMethod(url, code, enable);
    })
    $("#controlZoomOut").mousedown(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 11;
        var enable = true;
        commondMethod(url, code, enable);
    })
    $("#controlZoomOut").mouseup(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 11;
        var enable = false;
        commondMethod(url, code, enable);
    })
    //变焦
    $("#controlFocusNear").mousedown(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 12;
        var enable = true;
        commondMethod(url, code, enable);
    })
    $("#controlFocusNear").mouseup(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 12;
        var enable = false;
        commondMethod(url, code, enable);
    })
    $("#controlFocusFar").mousedown(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 13;
        var enable = true;
        commondMethod(url, code, enable);
    })
    $("#controlFocusFar").mouseup(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 13;
        var enable = false;
        commondMethod(url, code, enable);
    })
    //光圈
    $("#controlIrisOpen").mousedown(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 14;
        var enable = true;
        commondMethod(url, code, enable);
    })
    $("#controlIrisOpen").mouseup(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 14;
        var enable = false;
        commondMethod(url, code, enable);
    })
    $("#controlIrisClose").mousedown(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 15;
        var enable = true;
        commondMethod(url, code, enable);
    })
    $("#controlIrisClose").mouseup(function () {
        var url = "../cameraSdk/PTZControlWithSpeed";
        var code = 15;
        var enable = false;
        commondMethod(url, code, enable);
    })
    $("#setPreset").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        opt = {"cameraId": cameraId, "chanNo": chanNo, "speed": 8, "presetIndex": 1};
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../cameraSdk/setPreset",
            type: "post",
            dataType: "json",
            data: JSON.stringify(opt),
            success: function (data) {
                console.log(data);
            }
        })
    })
    $("#gotoPreset").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        opt = {"cameraId": cameraId, "chanNo": chanNo, "speed": 8, "presetIndex": 1};
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../cameraSdk/gotoPreset",
            type: "post",
            dataType: "json",
            data: JSON.stringify(opt),
            success: function (data) {
                console.log(data);
            }
        })
    })
    $("#getPTZ").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        opt = {"cameraId": cameraId, "chanNo": chanNo};
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../cameraSdk/getPTZ",
            type: "post",
            dataType: "json",
            data: JSON.stringify(opt),
            success: function (datas) {
                console.log(datas);
                $("#p").val(datas.data.p);
                $("#t").val(datas.data.t);
                $("#z").val(datas.data.z);
            }
        })
    })
    $("#setPTZ").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        var p = $('#p').val();
        var t = $('#t').val();
        var z = $('#z').val();
        //定义一个带有Map字段的实体对象
        var myEntity = {
            chanNo: chanNo,
            cameraId: cameraId,
            ptzMap: {
                p: p,
                t: t,
                z: z
            }
        };
        console.log(opt)
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../cameraSdk/setPTZ",
            type: "post",
            dataType: "json",
            data: JSON.stringify(myEntity),
            success: function (data) {
                console.log(data);
            }
        })
    })
    $("#setTargetPostion").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        var camera = cameraMap.get(cameraId);
        var camP = camera.longitude + ',' + camera.latitude + ',' + camera.altitude;
        var targetP = $('#targetPostion').val();
        var arr = targetP.split(",");
        arr = arr.map(item => parseFloat(item));
        //定义一个带有Map字段的实体对象
        var myEntity = {
            chanNo: chanNo,
            cameraId: cameraId,
            targetPosition: arr
        };
        console.log(myEntity)
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../cameraSdk/setTargetPosition",
            type: "post",
            dataType: "json",
            data: JSON.stringify(myEntity),
            success: function (data) {
                console.log(data);
            }
        })
    })
    $("#setZeroPTZ").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        opt = {"cameraId": cameraId, "chanNo": chanNo};
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../cameraSdk/setZeroPTZ",
            type: "post",
            dataType: "json",
            data: JSON.stringify(opt),
            success: function (data) {
                console.log(data);
            }
        })
    })
    $("#WiperPwron").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        opt = {"cameraId": cameraId, "chanNo": chanNo, "speed": 8, "enable": true, "code": 16};
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../cameraSdk/PTZControlWithSpeed",
            type: "post",
            dataType: "json",
            data: JSON.stringify(opt),
            success: function (data) {
                console.log(data);
            }
        })
    })
    var defogflag = true;
    $("#Defogcfg").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        optOpen = {"cameraId": cameraId, "chanNo": chanNo, "enable": true};
        optClose = {"cameraId": cameraId, "chanNo": chanNo, "enable": false};
        if (defogflag) {
            $(this).text("关闭透雾");
            defogflag = false;
            $.ajax({
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                url: "../cameraSdk/defogcfg",
                type: "post",
                dataType: "json",
                data: JSON.stringify(optOpen),
                success: function (data) {
                    console.log(data);
                }
            })
        } else {
            $(this).text("开启透雾");
            defogflag = true;
            $.ajax({
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                url: "../cameraSdk/defogcfg",
                type: "post",
                dataType: "json",
                data: JSON.stringify(optClose),
                success: function (data) {
                    console.log(data);
                }
            })
        }
    })
    var infrareflag = true;
    $("#Infrarecfg").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        optOpen = {"cameraId": cameraId, "chanNo": chanNo, "enable": true};
        optClose = {"cameraId": cameraId, "chanNo": chanNo, "enable": false};
        if (infrareflag) {
            $(this).text("关闭红外");
            infrareflag = false;
            $.ajax({
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                url: "../cameraSdk/infrarecfg",
                type: "post",
                dataType: "json",
                data: JSON.stringify(optOpen),
                success: function (data) {
                    console.log(data);
                }
            })
        } else {
            $(this).text("开启红外");
            infrareflag = true;
            $.ajax({
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                url: "../cameraSdk/infrarecfg",
                type: "post",
                dataType: "json",
                data: JSON.stringify(optClose),
                success: function (data) {
                    console.log(data);
                }
            })
        }
    })
    var focusModeflag = true;
    $("#FocusMode").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        optOpen = {"cameraId": cameraId, "chanNo": chanNo, "enable": true};
        optClose = {"cameraId": cameraId, "chanNo": chanNo, "enable": false};
        if (focusModeflag) {
            $(this).text("自动聚焦");
            focusModeflag = false;
            $.ajax({
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                url: "../cameraSdk/focusMode",
                type: "post",
                dataType: "json",
                data: JSON.stringify(optOpen),
                success: function (data) {
                    console.log(data);
                }
            })
        } else {
            $(this).text("手动聚焦");
            focusModeflag = true;
            $.ajax({
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                url: "../cameraSdk/focusMode",
                type: "post",
                dataType: "json",
                data: JSON.stringify(optClose),
                success: function (data) {
                    console.log(data);
                }
            })
        }
    })
    $("#getFocusPos").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        opt = {"cameraId": cameraId, "chanNo": chanNo};
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../cameraSdk/getFocusPos",
            type: "post",
            dataType: "json",
            data: JSON.stringify(opt),
            success: function (datas) {
                console.log(datas);
                $("#focus").val(datas.data);
            }
        })
    })
    var heateRpwronflag = true;
    $("#HeateRpwron").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        optOpen = {"cameraId": cameraId, "chanNo": chanNo, "enable": true};
        optClose = {"cameraId": cameraId, "chanNo": chanNo, "enable": false};
        if (heateRpwronflag) {
            $(this).text("关闭云台加热");
            heateRpwronflag = false;
            $.ajax({
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                url: "../cameraSdk/heateRpwron",
                type: "post",
                dataType: "json",
                data: JSON.stringify(optOpen),
                success: function (data) {
                    console.log(data);
                }
            })
        } else {
            $(this).text("开启云台加热");
            heateRpwronflag = true;
            $.ajax({
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                url: "../cameraSdk/heateRpwron",
                type: "post",
                dataType: "json",
                data: JSON.stringify(optClose),
                success: function (data) {
                    console.log(data);
                }
            })
        }
    })
    var CameraDeicingflag = true;
    $("#CameraDeicing").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        optOpen = {"cameraId": cameraId, "chanNo": chanNo, "enable": true};
        optClose = {"cameraId": cameraId, "chanNo": chanNo, "enable": false};
        if (CameraDeicingflag) {
            $(this).text("关闭镜头加热");
            CameraDeicingflag = false;
            $.ajax({
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                url: "../cameraSdk/cameraDeicing",
                type: "post",
                dataType: "json",
                data: JSON.stringify(optOpen),
                success: function (data) {
                    console.log(data);
                }
            })
        } else {
            $(this).text("开启镜头加热");
            CameraDeicingflag = true;
            $.ajax({
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                url: "../cameraSdk/cameraDeicing",
                type: "post",
                dataType: "json",
                data: JSON.stringify(optClose),
                success: function (data) {
                    console.log(data);
                }
            })
        }
    })
    $("#realCutPic").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        opt = {"cameraId": cameraId, "chanNo": chanNo};
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../cameraSdk/captureJPEGPicture",
            type: "post",
            dataType: "json",
            data: JSON.stringify(opt),
            success: function (data) {
                console.log(data.data);
                $("#imgContainer").attr("src", "data:image/png;base64," + data.data);
            }
        })
    })
    $("#saveCutPic").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        opt = {"cameraId": cameraId, "chanNo": chanNo};
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../cameraSdk/picCutCate",
            type: "post",
            dataType: "json",
            data: JSON.stringify(opt),
            success: function (data) {
                console.log(data.data);
                setTimeout(() => {
                    $('#imgContainer').attr('src', data.data);
                }, 1000)
            }
        })
    })
    var recordflag = true;
    $("#record").click(function () {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        optOpen = {"cameraId": cameraId, "chanNo": chanNo, "enable": true};
        optClose = {"cameraId": cameraId, "chanNo": chanNo, "enable": false};
        if (recordflag) {
            $(this).text("停止录像");
            recordflag = false;
            $.ajax({
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                url: "../cameraSdk/record",
                type: "post",
                dataType: "json",
                data: JSON.stringify(optOpen),
                success: function (data) {
                    console.log(data);
                }
            })
        } else {
            $(this).text("开始录像");
            recordflag = true;
            $.ajax({
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                url: "../cameraSdk/record",
                dataType: "json",
                data: JSON.stringify(optClose),
                type: "post",
                success: function (data) {
                    console.log(data);
                }
            })
        }
    })
    var cameraMap = new Map();
    /*云台公共方法*/
    function commondMethod(url, code, enable) {
        cameraId = $('#selectDev option:selected').val();
        chanNo = $('#selectChn option:selected').val();
        opt = {"cameraId": cameraId, "chanNo": chanNo, "speed": 4, "enable": enable, "code": code};
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: url,
            type: "post",
            dataType: "json",
            data: JSON.stringify(opt),
            success: function (data) {
                console.log(data);
            }
        })
    }
    $('video').click(function (e) {
        var cameraId = $('#selectDev option:selected').val();
        var chanNo = $('#selectChn option:selected').val();
        console.log(cameraId + " " + chanNo)
        $.ajax({
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Authorization': token
            },
            url: "../vtdu/media/" + cameraId + "_" + chanNo,
            type: "get",
            dataType: "json",
            success: function (data) {
                realView(data.data.webrtcUrl + "/", e.target.id);
            }
        })
    });
    let webrtcClient;
    //whep操作方法
    const restartPause = 2000;
    const unquoteCredential = (v) => (
        JSON.parse(`"${v}"`)
    );
    const linkToIceServers = (links) => (
        (links !== null) ? links.split(', ').map((link) => {
            const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);
            const ret = {
                urls: [m[1]],
            };
            if (m[3] !== undefined) {
                ret.username = unquoteCredential(m[3]);
                ret.credential = unquoteCredential(m[4]);
                ret.credentialType = "password";
            }
            return ret;
        }) : []
    );
    const parseOffer = (offer) => {
        const ret = {
            iceUfrag: '',
            icePwd: '',
            medias: [],
        };
        for (const line of offer.split('\r\n')) {
            if (line.startsWith('m=')) {
                ret.medias.push(line.slice('m='.length));
            } else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) {
                ret.iceUfrag = line.slice('a=ice-ufrag:'.length);
            } else if (ret.icePwd === '' && line.startsWith('a=ice-pwd:')) {
                ret.icePwd = line.slice('a=ice-pwd:'.length);
            }
        }
        return ret;
    };
    const generateSdpFragment = (offerData, candidates) => {
        const candidatesByMedia = {};
        for (const candidate of candidates) {
            const mid = candidate.sdpMLineIndex;
            if (candidatesByMedia[mid] === undefined) {
                candidatesByMedia[mid] = [];
            }
            candidatesByMedia[mid].push(candidate);
        }
        let frag = 'a=ice-ufrag:' + offerData.iceUfrag + '\r\n'
            + 'a=ice-pwd:' + offerData.icePwd + '\r\n';
        let mid = 0;
        for (const media of offerData.medias) {
            if (candidatesByMedia[mid] !== undefined) {
                frag += 'm=' + media + '\r\n'
                    + 'a=mid:' + mid + '\r\n';
                for (const candidate of candidatesByMedia[mid]) {
                    frag += 'a=' + candidate.candidate + '\r\n';
                }
            }
            mid++;
        }
        return frag;
    }
    class WHEPClient {
        constructor(whepUrl, videoId) {
            this.video = videoId;
            this.wurl = new URL('whep', whepUrl);
            this.pc = null;
            this.restartTimeout = null;
            this.eTag = '';
            this.queuedCandidates = [];
            this.start();
        }
        start() {
            console.log("requesting ICE servers");
            fetch(this.wurl, {
                method: 'OPTIONS',
            })
                .then((res) => this.onIceServers(res))
                .catch((err) => {
                    console.log('error: ' + err);
                    this.scheduleRestart();
                });
        }
        onIceServers(res) {
            this.pc = new RTCPeerConnection({
                iceServers: linkToIceServers(res.headers.get('Link')),
            });
            const direction = "sendrecv";
            this.pc.addTransceiver("video", {direction});
            this.pc.addTransceiver("audio", {direction});
            this.pc.onicecandidate = (evt) => this.onLocalCandidate(evt);
            this.pc.oniceconnectionstatechange = () => this.onConnectionState();
            this.pc.ontrack = (evt) => {
                console.log("new track:", evt.track.kind);
                document.getElementById(this.video).srcObject = evt.streams[0];
            };
            this.pc.createOffer()
                .then((offer) => this.onLocalOffer(offer));
        }
        onLocalOffer(offer) {
            this.offerData = parseOffer(offer.sdp);
            this.pc.setLocalDescription(offer);
            console.log("sending offer");
            console.log(this.wurl);
            fetch(this.wurl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/sdp',
                },
                body: offer.sdp,
            })
                .then((res) => {
                    if (res.status !== 201) {
                        throw new Error('bad status code');
                    }
                    // this.eTag = res.headers.get('ETag');
                    this.eTag = res.headers.get("ETag") || res.headers.get('E-Tag');
                    return res.text();
                })
                .then((sdp) => this.onRemoteAnswer(new RTCSessionDescription({
                    type: 'answer',
                    sdp,
                })))
                .catch((err) => {
                    console.log('error: ' + err);
                    this.scheduleRestart();
                });
        }
        onConnectionState() {
            if (this.restartTimeout !== null) {
                return;
            }
            console.log("peer connection state:", this.pc.iceConnectionState);
            switch (this.pc.iceConnectionState) {
                case "disconnected":
                    this.scheduleRestart();
            }
        }
        onRemoteAnswer(answer) {
            if (this.restartTimeout !== null) {
                return;
            }
            this.pc.setRemoteDescription(new RTCSessionDescription(answer));
            if (this.queuedCandidates.length !== 0) {
                this.sendLocalCandidates(this.queuedCandidates);
                this.queuedCandidates = [];
            }
        }
        onLocalCandidate(evt) {
            if (this.restartTimeout !== null) {
                return;
            }
            if (evt.candidate !== null) {
                if (this.eTag === '') {
                    this.queuedCandidates.push(evt.candidate);
                } else {
                    this.sendLocalCandidates([evt.candidate])
                }
            }
        }
        sendLocalCandidates(candidates) {
            fetch(this.wurl, {
                method: 'PATCH',
                headers: {
                    'Content-Type': 'application/trickle-ice-sdpfrag',
                    'If-Match': this.eTag,
                },
                body: generateSdpFragment(this.offerData, candidates),
            })
                .then((res) => {
                    if (res.status !== 204) {
                        throw new Error('bad status code');
                    }
                })
                .catch((err) => {
                    console.log('error: ' + err);
                    this.scheduleRestart();
                });
        }
        scheduleRestart() {
            if (this.restartTimeout !== null) {
                return;
            }
            if (this.pc !== null) {
                this.pc.close();
                this.pc = null;
            }
            this.restartTimeout = window.setTimeout(() => {
                this.restartTimeout = null;
                this.start();
            }, restartPause);
            this.eTag = '';
            this.queuedCandidates = [];
        }
        stop() {
            if (this.pc) {
                try {
                    this.pc.close();
                } catch (e) {
                    console.log("Failure close peer connection:" + e);
                }
                this.pc = null;
            }
        }
    }
    function realView(whepUrl, videoId) {
        console.log(whepUrl)
        webrtcClient = new WHEPClient(whepUrl, videoId);
    }
</script>
</body>
</html>
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/SyncTask.java
@@ -84,28 +84,24 @@
    /**
     * @Author åˆ˜è‹ä¹‰
     * @Description åŒæ­¥nvr通道状态实时同步流媒体
     * @Description åŒæ­¥é€šé“状态实时同步流媒体
     * @Date 2024/7/9 16:01
     */
    public void nvrChannelState() {
    public void syncChannelState() {
        log.warn("定时同步通道任务开始");
        //获取所有nvr设备
        List<ArdCameras> ardCamerasList = iArdCamerasService.selectArdCamerasListNoDataScope(new ArdCameras());
        if (ardCamerasList.size() > 0) {
            ardCamerasList.stream()
                    .filter(ardCameras -> ardCameras.getGdtype().equals("2"))
                    .forEach(ardCameras -> {
                        //通过SDK获取NVR当前通道
                        //通过SDK获取NVR实际通道
                        CameraSDK cameraSDK = cameraSDKFactory.createCameraSDK(ardCameras.getFactory());
                        List<ArdChannel> ardChannelList = cameraSDK.getChannels(ardCameras);
                        //同步通道表
                        ArdChannel ardChannel=new ArdChannel();
                        ardChannel.setDeviceId(ardCameras.getId());
                        List<ArdChannel> ardChannelListDb = ardChannelService.selectArdChannelList(ardChannel);
                        ardChannelService.asyncChannel(ardChannelListDb,ardChannelList);
                        //同步流媒体表
                        //同步流媒体api
                        ardChannelService.asyncChannel(ardCameras,ardChannelListDb,ardChannelList);
                    });
        }
    }
@@ -142,9 +138,10 @@
     * åˆ˜è‹ä¹‰
     * 2023/10/13 14:13:53
     */
    public void vtdu() {
    public void syncVtdu() {
        log.warn("定时同步流媒体任务开始");
        List<String> nameList = mediaService.getNameList();
        List<Vtdu> vtduList = vtduService.selectVtduList(new Vtdu());
        vtduService.asyncVtdu(vtduList,nameList);
    }
}
}
server/mediamtx/mediamtx.yml
@@ -225,17 +225,17 @@
webrtcICEInterfaces: []
# List of public IP addresses that are to be used as a host.
# This is used typically for servers that are behind 1:1 D-NAT.
webrtcICEHostNAT1To1IPs: []
webrtcICEHostNAT1To1IPs: [192.168.1.227]
# Address of a ICE UDP listener in format host:port.
# If filled, ICE traffic will pass through a single UDP port,
# allowing the deployment of the server inside a container or behind a NAT.
webrtcICEUDPMuxAddress:
webrtcICEUDPMuxAddress: :8189
# Address of a ICE TCP listener in format host:port.
# If filled, ICE traffic will pass through a single TCP port,
# allowing the deployment of the server inside a container or behind a NAT.
# Using this setting forces usage of the TCP protocol, which is not
# optimal for WebRTC.
webrtcICETCPMuxAddress: 192.168.1.227:19302
webrtcICETCPMuxAddress:
###############################################
# Global settings -> SRT