场景

SpringBoot+Vue+Openlayers实现地图上新增和编辑坐标并保存提交:

SpringBoot+Vue+Openlayers实现地图上新增和编辑坐标并保存提交_霸道流氓气质的博客

开源流媒体服务器ZLMediaKit在Windows上运行、配置、按需拉流拉取摄像头rtsp视频流)并使用http-flv网页播放:

开源流媒体服务器ZLMediaKit在Windows上运行、配置、按需拉流拉取摄像头rtsp视频流)并使用http-flv网页播放_srs按需拉流_霸道流氓气质的博客

GeoServer简介、下载、配置启动、发布shapefile全流程(图文实践):

GeoServer简介、下载、配置启动、发布shapefile全流程(图文实践)_霸道流氓气质的博客

若依前后端分离版手把手教你本地搭建环境并运行项目:

若依前后端分离版手把手教你本地搭建环境并运行项目_ruoyi本地调式_霸道流氓气质的博客

结合以上流程,需要实现对摄像头名称、在地图上位置的增删改查以及摄像头的预览功能。

注意这里的摄像头只支持H264编码格式的拉流和播放。

实现摄像头预览效果

ZLMediaKit+SpringBoot+Vue+Geoserver实现拉取摄像头rtsp流并在web端播放_流媒体

地图上新增和编辑坐标效果

ZLMediaKit+SpringBoot+Vue+Geoserver实现拉取摄像头rtsp流并在web端播放_ide_02

注:

实现

1、参考以上搭建前后端分离框架,建表如下

 

ZLMediaKit+SpringBoot+Vue+Geoserver实现拉取摄像头rtsp流并在web端播放_前端_03

建表语句:

DROP TABLE IF EXISTS `bus_stream_media_video`;
CREATE TABLE `bus_stream_media_video`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '序号',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '名称',
  `rtsp_stream_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'rtsp流地址',
  `area_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '位置',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 26 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '流媒体摄像头' ROW_FORMAT = DYNAMIC;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

2、摄像头拉流预览流程

选择某个摄像头进行预览时,先校验流媒体相关信息是否设置,是才能预览

拿着该条数据的rtsp流地址调用ZLMediaKit的拉流的api进行拉流。

api拼接规则:

http://流媒体服务ip:流媒体服务端口/index/api/addStreamProxy?vhost=流媒体服务ip&app=live&stream=拼接时间戳&url=摄像头rtsp地址&secret= 流媒体服务秘钥
  • 1.

调用拉流api时需要流媒体服务器的ip和端口以及秘钥信息,通过配置页面进行配置并进行修改时的二次密码校验

ZLMediaKit+SpringBoot+Vue+Geoserver实现拉取摄像头rtsp流并在web端播放_ide_04

然后解析拉流接口返回的json数据

ZLMediaKit+SpringBoot+Vue+Geoserver实现拉取摄像头rtsp流并在web端播放_ide_05

状态码不为0或接口不通无响应等则提示不可预览,状态码为0则可预览。

解析data下的key,按照/解析,比如上面解析出ip:127.0.0.1,流应用live,流id为test。

进行http-flv视频预览, 预览url拼接规则:

http://key解析的ip:流媒体服务端口/key解析的live/key解析的test.flv
  • 1.

这里的流程可参考上面博客。

2、使用框架自带的代码生成工具生成代码并进行实体类部分修改

摄像头实体类:

import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class BusStreamMediaVideo extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 序号 */
    private Long id;

    /** 名称 */
    @Excel(name = "名称")
    private String name;

    /** 位置x坐标 */
    private BigDecimal siteX;

    /** 位置y坐标 */
    private BigDecimal siteY;

    /** 区域位置 */
    @Excel(name = "区域位置")
    private String areaName;

    /** RTSP addresss */
    @Excel(name = "rtspAddress")
    private String rtspStreamAddress;

    private BigDecimal[] videoAdd;

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.

流媒体设置DTO类:

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class BusStreamMediaVideoParamDto
{

    private String ip;

    private String port;

    private String secret;

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

Mapper接口层:

import com.ruoyi.system.domain.BusStreamMediaVideo;
import com.ruoyi.system.redisAop.AopCacheEnable;
import com.ruoyi.system.redisAop.AopCacheEvict;
import java.util.List;

public interface BusStreamMediaVideoMapper
{
    /**
     * 查询摄像头参数
     *
     * @param id 摄像头参数ID
     * @return 摄像头参数
     */
        public BusStreamMediaVideo selectBusStreamMediaVideoById(Long id);

    /**
     * 查询摄像头参数列表
     *
     * @param
     * @return 摄像头参数集合
     */
    @AopCacheEnable(key = "busStreamMediaVideo",expireTime = 86400)
    public List<BusStreamMediaVideo> selectBusStreamMediaVideoList(BusStreamMediaVideo busStreamMediaVideo);

    /**
     * 新增摄像头参数
     *
     * @param
     * @return 结果
     */
    @AopCacheEvict(key = "busStreamMediaVideo")
    public int insertBusStreamMediaVideo(BusStreamMediaVideo busStreamMediaVideo);

    /**
     * 修改摄像头参数
     *
     * @param
     * @return 结果
     */
    @AopCacheEvict(key = "busStreamMediaVideo")
    public int updateBusStreamMediaVideo(BusStreamMediaVideo busStreamMediaVideo);

    /**
     * 删除摄像头参数
     *
     * @param id 摄像头参数ID
     * @return 结果
     */
    @AopCacheEvict(key = "busStreamMediaVideo")
    public int deleteBusStreamMediaVideoById(Long id);

    /**
     * 批量删除摄像头参数
     *
     * @param ids 需要删除的数据ID
     * @return 结果
     */
    @AopCacheEvict(key = "busStreamMediaVideo")
    public int deleteBusStreamMediaVideoByIds(Long[] ids);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.

注意这里使用了自定义缓存注解。

SpringBoot中通过自定义缓存注解(AOP切面拦截)实现数据库数据缓存到Redis:

SpringBoot中通过自定义缓存注解(AOP切面拦截)实现数据库数据缓存到Redis_redis切面_霸道流氓气质的博客

Mapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.BusStreamMediaVideoMapper">

    <resultMap type="BusStreamMediaVideo" id="BusStreamMediaVideoResult">
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="areaName" column="area_name"/>
        <result property="rtspStreamAddress" column="rtsp_stream_address"/>
    </resultMap>

    <sql id="selectBusStreamMediaVideoVo">
        select id, name, area_name, rtsp_stream_address
        from bus_stream_media_video
    </sql>

    <select id="selectBusStreamMediaVideoList" parameterType="BusStreamMediaVideo"
            resultMap="BusStreamMediaVideoResult">
        <include refid="selectBusStreamMediaVideoVo"/>
        <where>
            <if test="name != null  and name != ''">and name = #{name}</if>
            <if test="areaName!=null and areaName != ''">and area_name = #{areaName}</if>
            <if test="rtspStreamAddress!=null and rtspStreamAddress != ''">and rtsp_stream_address = #{rtspStreamAddress}</if>
        </where>
    </select>

    <select id="selectBusStreamMediaVideoById" parameterType="Long" resultMap="BusStreamMediaVideoResult">
        <include refid="selectBusStreamMediaVideoVo"/>
        where id = #{id}
    </select>

    <insert id="insertBusStreamMediaVideo" parameterType="BusStreamMediaVideo">
        insert into bus_stream_media_video
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="id != null">id,</if>
            <if test="name != null">name,</if>
            <if test="areaName != null">area_name,</if>
            <if test="rtspStreamAddress != null">rtsp_stream_address,</if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="id != null">#{id},</if>
            <if test="name != null">#{name},</if>
            <if test="areaName != null">#{areaName},</if>
            <if test="rtspStreamAddress != null">#{rtspStreamAddress},</if>
        </trim>
    </insert>

    <update id="updateBusStreamMediaVideo" parameterType="BusStreamMediaVideo">
        update bus_stream_media_video
        <trim prefix="SET" suffixOverrides=",">
            <if test="name != null">name = #{name},</if>
            <if test="areaName!=null">area_name =#{areaName},</if>
            <if test="rtspStreamAddress!=null">rtsp_stream_address =#{rtspStreamAddress},</if>
        </trim>
        where id = #{id}
    </update>

    <delete id="deleteBusStreamMediaVideoById" parameterType="Long">
        delete
        from bus_stream_media_video
        where id = #{id}
    </delete>

    <delete id="deleteBusStreamMediaVideoByIds" parameterType="String">
        delete from bus_stream_media_video where id in
        <foreach item="id" collection="array" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>

</mapper>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.

Service接口:

import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.system.domain.BusStreamMediaVideo;
import com.ruoyi.system.domain.BusStreamMediaVideoParamDto;
import java.util.List;

public interface IBusStreamMediaVideoService
{

    /**
     * 查询流媒体服务器参数
     * @return
     */
    public BusStreamMediaVideoParamDto getStreamMediaParam();
    /**
     * 设置流媒体参数
     * @param busStreamMediaVideoParamDto
     * @return
     */
    public AjaxResult setStreamMediaParam(BusStreamMediaVideoParamDto busStreamMediaVideoParamDto);

    /**
     * 查询摄像头参数
     *
     * @param id 摄像头参数ID
     * @return 摄像头参数
     */
    public BusStreamMediaVideo selectBusStreamMediaVideoById(Long id);

    /**
     * 查询摄像头参数列表
     *
     * @param BusStreamMediaVideo 摄像头参数
     * @return 摄像头参数集合
     */
    public List<BusStreamMediaVideo> selectBusStreamMediaVideoList(BusStreamMediaVideo BusStreamMediaVideo);

    /**
     * 新增摄像头参数
     *
     * @param BusStreamMediaVideo 摄像头参数
     * @return 结果
     */
    public int insertBusStreamMediaVideo(BusStreamMediaVideo BusStreamMediaVideo);

    /**
     * 修改摄像头参数
     *
     * @param BusStreamMediaVideo 摄像头参数
     * @return 结果
     */
    public int updateBusStreamMediaVideo(BusStreamMediaVideo BusStreamMediaVideo);

    /**
     * 批量删除摄像头参数
     *
     * @param ids 需要删除的摄像头参数ID
     * @return 结果
     */
    public int deleteBusStreamMediaVideoByIds(Long[] ids);

    /**
     * 删除摄像头参数信息
     *
     * @param id 摄像头参数ID
     * @return 结果
     */
    public int deleteBusStreamMediaVideoById(Long id);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.

ServiceImpl:

import com.ruoyi.common.constant.RedisPTConstants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.system.domain.BusStreamMediaVideo;
import com.ruoyi.system.domain.BusStreamMediaVideoParamDto;
import com.ruoyi.system.mapper.BusStreamMediaVideoMapper;
import com.ruoyi.system.service.IBusStreamMediaVideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;

@Service
public class BusStreamMediaVideoServiceImpl implements IBusStreamMediaVideoService {

    @Autowired
    private BusStreamMediaVideoMapper busStreamMediaVideoMapper;

    @Autowired
    private RedisCache redisService;

    @Override
    public BusStreamMediaVideoParamDto getStreamMediaParam() {
        if(redisService.hasKey(RedisPTConstants.STREAMMEDIAPARAM)){
            BusStreamMediaVideoParamDto busStreamMediaVideoParamDto = redisService.getCacheObject(RedisPTConstants.STREAMMEDIAPARAM);
            return busStreamMediaVideoParamDto;
        }else {
            return new BusStreamMediaVideoParamDto();
        }
    }

    @Override
    public AjaxResult setStreamMediaParam(BusStreamMediaVideoParamDto busStreamMediaVideoDto) {
        try {
            redisService.setCacheObject(RedisPTConstants.STREAMMEDIAPARAM,busStreamMediaVideoDto);
            return AjaxResult.success();
        }catch (Exception exception){
            return AjaxResult.error();
        }
    }

    /**
     * 查询摄像头参数
     *
     * @param id 摄像头参数ID
     * @return 摄像头参数
     */
    @Override
    public BusStreamMediaVideo selectBusStreamMediaVideoById(Long id) {
        return busStreamMediaVideoMapper.selectBusStreamMediaVideoById(id);
    }

    /**
     * 查询摄像头参数列表
     *
     * @param BusStreamMediaVideo 摄像头参数
     * @return 摄像头参数
     */
    @Override
    public List<BusStreamMediaVideo> selectBusStreamMediaVideoList(BusStreamMediaVideo BusStreamMediaVideo) {
        List<BusStreamMediaVideo> BusStreamMediaVideos = busStreamMediaVideoMapper.selectBusStreamMediaVideoList(BusStreamMediaVideo);
        BusStreamMediaVideos.forEach(videos -> {
            if (videos.getAreaName() != null) {
                String[] point = videos.getAreaName().substring(1, videos.getAreaName().length() - 1).split(",");
                if (!point[0].equals("null") && !point[1].equals("null")) {
                    videos.setSiteX(new BigDecimal(point[0]));
                    videos.setSiteY(new BigDecimal(point[1]));
                }
            }
        });
        return BusStreamMediaVideos;
    }

    /**
     * 新增摄像头参数
     *
     * @param BusStreamMediaVideo 摄像头参数
     * @return 结果
     */
    @Override
    public int insertBusStreamMediaVideo(BusStreamMediaVideo BusStreamMediaVideo) {

        String area = "[" + BusStreamMediaVideo.getSiteX() + "," + BusStreamMediaVideo.getSiteY() + "]";
        BusStreamMediaVideo.setAreaName(area);
        return busStreamMediaVideoMapper.insertBusStreamMediaVideo(BusStreamMediaVideo);
    }

    /**
     * 修改摄像头参数
     *
     * @param BusStreamMediaVideo 摄像头参数
     * @return 结果
     */
    @Override
    public int updateBusStreamMediaVideo(BusStreamMediaVideo BusStreamMediaVideo) {
        String area = "[" + BusStreamMediaVideo.getSiteX() + "," + BusStreamMediaVideo.getSiteY() + "]";
        BusStreamMediaVideo.setAreaName(area);
        return busStreamMediaVideoMapper.updateBusStreamMediaVideo(BusStreamMediaVideo);
    }

    /**
     * 批量删除摄像头参数
     *
     * @param ids 需要删除的摄像头参数ID
     * @return 结果
     */
    @Override
    public int deleteBusStreamMediaVideoByIds(Long[] ids) {
        return busStreamMediaVideoMapper.deleteBusStreamMediaVideoByIds(ids);
    }

    /**
     * 删除摄像头参数信息
     *
     * @param id 摄像头参数ID
     * @return 结果
     */
    @Override
    public int deleteBusStreamMediaVideoById(Long id) {
        return busStreamMediaVideoMapper.deleteBusStreamMediaVideoById(id);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.

Controller层:

import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.system.domain.BusStreamMediaVideo;
import com.ruoyi.system.domain.BusStreamMediaVideoParamDto;
import com.ruoyi.system.service.IBusStreamMediaVideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/streamMediaVideo")
public class BusStreamMediaVideoController extends BaseController
{
    @Autowired
    private IBusStreamMediaVideoService busStreamMediaVideoService;

    /**
     * 查询摄像头参数列表
     */
    @GetMapping("/getVideoInfo")
    public TableDataInfo list(BusStreamMediaVideo busStreamMediaVideo)
    {
        startPage();
        List<BusStreamMediaVideo> list = busStreamMediaVideoService.selectBusStreamMediaVideoList(busStreamMediaVideo);
        return getDataTable(list);
    }

    /**
     * 设置流媒体服务器参数
     */
    @PostMapping("/setStreamMediaParam")
    public AjaxResult setStreamMediaParam(BusStreamMediaVideoParamDto busStreamMediaVideoDto)
    {
        return busStreamMediaVideoService.setStreamMediaParam(busStreamMediaVideoDto);
    }

    /**
     * 查询流媒体服务器参数
     */
    @GetMapping("/getStreamMediaParam")
    public BusStreamMediaVideoParamDto getStreamMediaParam()
    {
        return busStreamMediaVideoService.getStreamMediaParam();
    }


    /**
     * 获取摄像头参数详细信息
     */
    @GetMapping(value = "/{id}")
    public AjaxResult getInfo(@PathVariable("id") Long id)
    {
        return AjaxResult.success(busStreamMediaVideoService.selectBusStreamMediaVideoById(id));
    }

    /**
     * 新增摄像头参数
     */
    @PostMapping
    public AjaxResult add(BusStreamMediaVideo busStreamMediaVideo)
    {
        return toAjax(busStreamMediaVideoService.insertBusStreamMediaVideo(busStreamMediaVideo));
    }

    /**
     * 修改摄像头参数
     */
    @PutMapping
    public AjaxResult edit(@RequestBody BusStreamMediaVideo busStreamMediaVideo)
    {
        return toAjax(busStreamMediaVideoService.updateBusStreamMediaVideo(busStreamMediaVideo));
    }

    /**
     * 删除摄像头参数
     */
 @DeleteMapping("/{ids}")
    public AjaxResult remove(@PathVariable Long[] ids)
    {
        return toAjax(busStreamMediaVideoService.deleteBusStreamMediaVideoByIds(ids));
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.

注意上面流媒体参数设置和获取接口直接存储进redis中。

前缀使用常量类存储:

public class RedisPTConstants {
    public static final String STREAMMEDIAPARAM = "streamMediaParam:";
}
  • 1.
  • 2.
  • 3.

3、前端代码实现

主页面代码streamVideo.vue:

<template>
  <div class="app-container">
    <el-form
      :model="queryParams"
      ref="queryForm"
      :inline="true"
      v-show="showSearch"
      label-width="68px"
    >
      <el-form-item label="名称" prop="name">
        <el-input
          v-model="queryParams.name"
          placeholder="请输入名称"
          clearable
          size="small"
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery"
          >搜索</el-button
        >
        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="el-icon-plus"
          size="mini"
          @click="handleAdd"
          v-hasPermi="['runcontrolmange:streamMediaVideo:add']"
          >新增</el-button
        >
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="el-icon-edit"
          size="mini"
          :disabled="single"
          @click="handleUpdate"
          v-hasPermi="['runcontrolmange:streamMediaVideo:edit']"
          >修改</el-button
        >
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="danger"
          plain
          icon="el-icon-delete"
          size="mini"
          :disabled="multiple"
          @click="handleDelete"
          v-hasPermi="['runcontrolmange:streamMediaVideo:remove']"
          >删除</el-button
        >
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="el-icon-setting"
          size="mini"
          :disabled="single"
          @click="videoChange"
          >预览</el-button
        >
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="warning"
          plain
          icon="el-icon-edit"
          size="mini"
          @click="handleSetting"
          v-hasPermi="['runcontrolmange:streamMediaVideo:set']"
          >设置</el-button
        >
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="el-icon-edit"
          size="mini"
          @click="handleIllustrate"
          >说明</el-button
        >
      </el-col>
    </el-row>

    <el-table
      v-loading="loading"
      :data="videoList"
      @selection-change="handleSelectionChange"
    >
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column
        show-overflow-tooltip
        label="摄像头名称"
        align="center"
        prop="name"
      />
      <el-table-column
        show-overflow-tooltip
        label="rtsp视频流Url"
        align="center"
        prop="rtspStreamAddress"
      />
      <el-table-column
        show-overflow-tooltip
        label="摄像头坐标"
        align="center"
        prop="areaName"
      />
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template slot-scope="scope">
          <el-button
            size="mini"
            type="text"
            icon="el-icon-edit"
            @click="handleUpdate(scope.row)"
            v-hasPermi="['runcontrolmange:streamMediaVideo:edit']"
            >修改</el-button
          >
          <el-button
            size="mini"
            type="text"
            icon="el-icon-delete"
            @click="handleDelete(scope.row)"
            v-hasPermi="['runcontrolmange:streamMediaVideo:remove']"
            >删除</el-button
          >
        </template>
      </el-table-column>
    </el-table>

    <pagination
      v-show="total > 0"
      :total="total"
      :page.sync="queryParams.pageNum"
      :limit.sync="queryParams.pageSize"
      @pagination="getList"
    />

    <!-- 添加或修改识别用户对话框 -->
    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
        <el-form-item label="摄像头名称" prop="name">
          <el-input v-model="form.name" placeholder="请输入摄像头名称" />
        </el-form-item>

        <el-form-item label="rtsp视频流Url" prop="rtspStreamAddress">
          <el-input v-model="form.rtspStreamAddress" placeholder="请输入rtsp视频流Url" />
        </el-form-item>

        <el-form-item label="摄像头位置" prop="coordinate">
          <el-input
            v-model="lightPoint"
            placeholder="点击新增/更改摄像头坐标"
            @focus="onMap"
          />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm">确 定</el-button>
        <el-button @click="cancel">取 消</el-button>
      </div>
    </el-dialog>

    <!-- 流媒体服务设置对话框 -->
    <el-dialog title="流媒体服务设置" :visible.sync="setVisible" width="500px" append-to-body>
      <el-form ref="setForm" :model="setForm" :rules="rules1" label-width="120px">
        <el-form-item label="IP" prop="ip">
          <el-input v-model="setForm.ip" placeholder="请输入IP" />
        </el-form-item>

        <el-form-item label="端口" prop="port">
          <el-input v-model.number="setForm.port" placeholder="请输入端口号" />
        </el-form-item>

        <el-form-item label="密钥" prop="secret">
          <el-input type="password" v-model="setForm.secret" placeholder="请输入密钥" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitSetForm">确 定</el-button>
        <el-button @click="() => {this.setVisible = false;}">取 消</el-button>
      </div>
    </el-dialog>
    <el-dialog title="rtsp地址规则说明" :visible.sync="illVisible" width="50%" append-to-body>
      <div style="margin: 20px 40px 40px 40px; height: 60vh; overflow-y: scroll;">
        大华<br/>
        大华摄像机RTSP地址规则为:rtsp://[username]:[password]@[ip]:[port]/cam/realmonitor?channel=1&subtype=0<br/>
        说明:<br/>
        username: 用户名。例如admin。<br/>
        password: 密码。例如admin123。<br/>
        ip: 为设备IP。例如 192.168.1.101。<br/>
        port: 端口号默认为554,若为默认可不填写。<br/>
        channel: 通道号,起始为1。例如通道2,则为channel=2。<br/>
        subtype: 码流类型,主码流为0(即subtype=0),辅码流为1(即subtype=1)。<br/>
        如:rtsp://admin:admin123@192.168.1.101/cam/realmonitor?channel=1&subtype=1<br/>
        <br/>
        海康<br/>
        海康摄像机RTSP地址规则为:rtsp://[username]:[password]@[ip]:[port]/[codec]/[channel]/[subtype]/av_stream<br/>
        说明:<br/>
        username: 用户名。例如admin。<br/>
        password: 密码。例如admin123。<br/>
        ip: 为设备IP。例如192.168.1.104。<br/>
        port: 端口号默认为554,若为默认可不填写。<br/>
        codec:有h264、MPEG-4、mpeg4这几种。<br/>
        channel: 通道号,起始为1。例如通道1,则为ch1。<br/>
        subtype: 码流类型,主码流为main,辅码流为sub。<br/>
        如:rtsp://admin:admin123@192.168.1.104/h264/ch1/subtype/av_stream<br/>
        <br/>
        宇视<br/>
        宇视摄像机RTSP地址规则为:rtsp://[username]:[password]@[ip]:[port]/media/video1/2/3<br/>
        说明:<br/>
        username: 用户名。例如admin。<br/>
        password: 密码。例如admin123。<br/>
        ip: 为设备IP。例如 192.168.1.107。<br/>
        port: 端口号默认为554,若为默认可不填写。<br/>
        video: 1代表主码流、2辅码流、3第三码流<br/>
        如:rtsp://admin:admin123@192.168.1.107/media/video2<br/>
        <br/>
        华为<br/>
        华为摄像机RTSP地址规则为:rtsp://[username]:[password]@[ip]:[port]/LiveMedia/[channel]/Media1/2<br/>
        说明:<br/>
        username: 用户名。例如admin。<br/>
        password: 密码。例如admin123。<br/>
        ip: 为设备IP。例如192.168.1.110。<br/>
        port: 端口号默认为554,若为默认可不填写。<br/>
        channel: 通道号,起始为1。例如通道1,则为ch1。
        Media:1代表主码流、2辅码流<br/>
        如:rtsp://admin:admin123@192.168.1.110/LiveMedia/ch1/Media2
      </div>
    </el-dialog>
    <!-- 摄像头预览 -->
    <preVideo ref="video" :videoOpen="videoOpen" @close="() => {this.videoOpen = false;this.previewUrl = ''}" :url="previewUrl" />
    <!-- 地图中显示摄像头坐标 -->
    <videoMap ref="videoMap" @childEvent="parentEvent"></videoMap>
  </div>
</template>

<script>
import {
  addVideo,
  updateVideo,
  delVideo,
  getVideoInfo,
  setStreamData,
  getStreamData,
} from "@/api/system/streamVideo";
import preVideo from "./previedVideo.vue";
import videoMap from "./videoMap";
import axios from 'axios'

export default {
  name: "carHkVideo",
  components: {
    preVideo,
    videoMap,
  },
  data() {
    return {
      // 遮罩层
      loading: false,
      // 选中数组
      ids: [],
      // 非单个禁用
      single: true,
      // 非多个禁用
      multiple: true,
      // 显示搜索条件
      showSearch: true,
      // 总条数
      total: 0,
      // 表格数据
      videoList: [],
      // 弹出层标题
      title: "",
      // 是否显示弹出层
      open: false,
      // 查询参数
      queryParams: {
        pageNum: 1,
        pageSize: 10,
      },
      // 表单参数
      form: {
        coordinate: "",
      },
      // 表单校验
      rules: {},
      rules1: {
        ip: [
          { required: true, trigger: "blur", message: "ip为必填项" },
          { pattern: /^(([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])$/, trigger: "blur", message: "必须输入合法的ip格式" },
        ],
        port: [
          {type: 'number', message: '端口号必须为数字'},
          {required: true, message: '端口号为必填项', trigger: 'blur'},
        ],
        secret: [
          {required: true, message: '密钥为必填项', trigger: 'blur'},
        ],
      },
      dialogVisible: false,
      rowList: {},
      // 预览摄像头的参数
      openVideoData: [{rtspStreamAddress:''}],
      lightPoint: [],
      setForm: {},
      setVisible: false,
      illVisible: false,
      videoOpen: false,
      previewUrl: '',
    };
  },
  created() {
    this.getList();
  },
  mounted() {},
  destroyed() {},
  methods: {
    videoChange() {
      this.videoOpen = true;
      getStreamData().then(res => {
        let {ip, port, secret} = res
        if(ip && port && secret){
          let {rtspStreamAddress} = Object.assign({}, this.openVideoData[0])
          let url = `http://${ip}:${port}/index/api/addStreamProxy?vhost=${ip}&app=live&stream=${new Date().getTime()}&url=${rtspStreamAddress}&secret=${secret}`
          axios({ url, method: "get", timeout: 5000}).then(({data}) => {
            if(data.code == 0) {
              let data1 = data.data.key
              let index = data1.indexOf('/')
              let ip = data1.substring(0, index)
              let urlPart = data1.substring(index + 1)
              let url = `http://${ip}:${port}/${urlPart}.flv`
              this.previewUrl = url
            } else this.$message.error('不可预览,请检查摄像头和流媒体服务的配置')
          }).catch(err => {
            console.log(err);
            this.$message.error(err + ',不可预览,请检查摄像头和流媒体服务的配置')
          })
        } else this.$message.error('预览前请先设置流媒体服务的IP、端口号和秘钥')
      })
    },
    handleSetting() {
      this.setVisible = true;
      getStreamData().then((res) => {
        this.setForm = Object.assign({}, res, {port: parseInt(res.port)})
      })
    },
    handleIllustrate() {
      this.illVisible = true;
    },
    parentEvent(data) {
      this.form.siteX = data[0];
      this.form.siteY = data[1];
      this.lightPoint = `${this.form.siteX},${this.form.siteY}`;
      this.form.coordinate = this.lightPoint;
    },
    onMap() {
      this.$refs.videoMap.dialogVisible = true;
      this.$refs.videoMap.init();
      this.$refs.videoMap.coordinate = this.form.coordinate;
      this.$refs.videoMap.drawPoint([this.form.siteX, this.form.siteY]);
    },
    getList() {
      getVideoInfo(this.queryParams).then((res) => {
        this.videoList = res.rows;
        this.total = res.total;
        this.loading = false;
      });
    },
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    resetQuery() {
      this.resetForm("queryForm");
      this.handleQuery();
    },
    handleAdd() {
      this.reset();
      this.open = true;
      this.lightPoint = "";
      this.title = "新增摄像头";
    },
    handleUpdate(row) {
      this.reset();
      this.title = "修改";
      this.open = true;
      let id;
      if (row.id) {
        id = row.id;
        this.videoList.forEach((item) => {
          if (item.id == id) {
            this.form = JSON.parse(JSON.stringify(item));
            this.lightPoint = item.areaName;
          }
        });
      } else {
        id = this.ids;
        this.videoList.forEach((item) => {
          if (item.id == id[0]) {
            this.form = JSON.parse(JSON.stringify(item));
            this.lightPoint = item.areaName;
          }
        });
      }
    },
    /** 提交按钮 */
    submitForm() {
      this.$refs["form"].validate((valid) => {
        if (valid) {
          if (this.form.id != null) {
            updateVideo(this.form).then((response) => {
              this.msgSuccess("修改成功");
              this.open = false;
              this.getList();
            });
          } else {
            addVideo(this.form).then((response) => {
              this.msgSuccess("新增成功");
              this.open = false;
              this.getList();
            });
          }
        }
      });
    },
    submitSetForm() {
      this.$refs["setForm"].validate((valid) => {
        if (valid) {
          this.$prompt('请输入二次密码校验权限', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            inputPattern: /^123$/,
            inputErrorMessage: '二次密码校验失败'
          }).then(({ value }) => {
            this.$message({
              type: 'success',
              message: '校验成功'
            });
            setStreamData(this.setForm).then(res => {
              if(res.code == 200) {
                this.$message.success('修改成功');
                this.setVisible = false;
              }
              else this.$message.error(res.msg);
            })
          }).catch(() => {
            this.$message({
              type: 'info',
              message: '取消修改'
            });       
          });
        }
      })
    },
    handleDelete(row) {
      const ids = row.id || this.ids;
      this.$confirm("是否确认删除该条数据项?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(function () {
          return delVideo(ids);
        })
        .then(() => {
          this.getList();
          this.msgSuccess("删除成功");
        });
    },
    // 多选框选中数据
    handleSelectionChange(selection) {
      this.openVideoData = [];
      this.openVideoData = selection;
      this.ids = selection.map((item) => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    cancel() {
      this.open = false;
      this.reset();
    },
    reset() {
      this.form = {
        ip: null,
        name: null,
        username: null,
        password: null,
        port: null,
        coordinate: "",
      };

      this.resetForm("form");
    },
  },
};
</script>
<style scoped>
.dialog_div {
  width: 100%;
}
.show1 {
  width: 90%;
  height: 433px;
  margin: auto;
}
.plugin {
  width: 100%;
  height: 400px;
}

.my-tag {
  margin-left: 3px;
}

.my-group-btn {
  margin-top: 5px;
}
</style>
<style lang="scss">
</style>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.
  • 227.
  • 228.
  • 229.
  • 230.
  • 231.
  • 232.
  • 233.
  • 234.
  • 235.
  • 236.
  • 237.
  • 238.
  • 239.
  • 240.
  • 241.
  • 242.
  • 243.
  • 244.
  • 245.
  • 246.
  • 247.
  • 248.
  • 249.
  • 250.
  • 251.
  • 252.
  • 253.
  • 254.
  • 255.
  • 256.
  • 257.
  • 258.
  • 259.
  • 260.
  • 261.
  • 262.
  • 263.
  • 264.
  • 265.
  • 266.
  • 267.
  • 268.
  • 269.
  • 270.
  • 271.
  • 272.
  • 273.
  • 274.
  • 275.
  • 276.
  • 277.
  • 278.
  • 279.
  • 280.
  • 281.
  • 282.
  • 283.
  • 284.
  • 285.
  • 286.
  • 287.
  • 288.
  • 289.
  • 290.
  • 291.
  • 292.
  • 293.
  • 294.
  • 295.
  • 296.
  • 297.
  • 298.
  • 299.
  • 300.
  • 301.
  • 302.
  • 303.
  • 304.
  • 305.
  • 306.
  • 307.
  • 308.
  • 309.
  • 310.
  • 311.
  • 312.
  • 313.
  • 314.
  • 315.
  • 316.
  • 317.
  • 318.
  • 319.
  • 320.
  • 321.
  • 322.
  • 323.
  • 324.
  • 325.
  • 326.
  • 327.
  • 328.
  • 329.
  • 330.
  • 331.
  • 332.
  • 333.
  • 334.
  • 335.
  • 336.
  • 337.
  • 338.
  • 339.
  • 340.
  • 341.
  • 342.
  • 343.
  • 344.
  • 345.
  • 346.
  • 347.
  • 348.
  • 349.
  • 350.
  • 351.
  • 352.
  • 353.
  • 354.
  • 355.
  • 356.
  • 357.
  • 358.
  • 359.
  • 360.
  • 361.
  • 362.
  • 363.
  • 364.
  • 365.
  • 366.
  • 367.
  • 368.
  • 369.
  • 370.
  • 371.
  • 372.
  • 373.
  • 374.
  • 375.
  • 376.
  • 377.
  • 378.
  • 379.
  • 380.
  • 381.
  • 382.
  • 383.
  • 384.
  • 385.
  • 386.
  • 387.
  • 388.
  • 389.
  • 390.
  • 391.
  • 392.
  • 393.
  • 394.
  • 395.
  • 396.
  • 397.
  • 398.
  • 399.
  • 400.
  • 401.
  • 402.
  • 403.
  • 404.
  • 405.
  • 406.
  • 407.
  • 408.
  • 409.
  • 410.
  • 411.
  • 412.
  • 413.
  • 414.
  • 415.
  • 416.
  • 417.
  • 418.
  • 419.
  • 420.
  • 421.
  • 422.
  • 423.
  • 424.
  • 425.
  • 426.
  • 427.
  • 428.
  • 429.
  • 430.
  • 431.
  • 432.
  • 433.
  • 434.
  • 435.
  • 436.
  • 437.
  • 438.
  • 439.
  • 440.
  • 441.
  • 442.
  • 443.
  • 444.
  • 445.
  • 446.
  • 447.
  • 448.
  • 449.
  • 450.
  • 451.
  • 452.
  • 453.
  • 454.
  • 455.
  • 456.
  • 457.
  • 458.
  • 459.
  • 460.
  • 461.
  • 462.
  • 463.
  • 464.
  • 465.
  • 466.
  • 467.
  • 468.
  • 469.
  • 470.
  • 471.
  • 472.
  • 473.
  • 474.
  • 475.
  • 476.
  • 477.
  • 478.
  • 479.
  • 480.
  • 481.
  • 482.
  • 483.
  • 484.
  • 485.
  • 486.
  • 487.
  • 488.
  • 489.
  • 490.
  • 491.
  • 492.
  • 493.
  • 494.
  • 495.
  • 496.
  • 497.
  • 498.
  • 499.
  • 500.
  • 501.
  • 502.
  • 503.
  • 504.
  • 505.
  • 506.
  • 507.
  • 508.
  • 509.
  • 510.
  • 511.
  • 512.
  • 513.
  • 514.
  • 515.
  • 516.
  • 517.
  • 518.
  • 519.
  • 520.
  • 521.
  • 522.
  • 523.
  • 524.
  • 525.
  • 526.
  • 527.
  • 528.
  • 529.
  • 530.
  • 531.
  • 532.
  • 533.

4、实现在地图上点选和回显坐标组件videoMap.vue

<template>
  <div class="cont">
    <el-dialog
      title="选取摄像头位置"
      :visible.sync="dialogVisible"
      width="70%"
      :modal="false"
      v-loading="loading"
      :before-close="handleClose"
    >
      <div id="pMap" v-if="dialogVisible"></div>
      <p class="showPoint">经纬度:{{ coordinate }}</p>
      <div class="dialogfooter">
        <el-button type="primary" size="small" @click="submitForm">确 定</el-button>
        <el-button size="small" @click="cancel">取 消</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import Map from "ol/Map";
import View from "ol/View";
import Feature from "ol/Feature";
import { Point } from "ol/geom";
import { Icon, Style } from "ol/style";
import { Image as ImageLayer, Vector as VectorLayer, Tile as TileLayer } from "ol/layer";
import { Vector as VectorSource, ImageWMS } from "ol/source";

export default {
  data() {
    return {
      dialogVisible: false,
      loading: false,
      layers: null,
      map: null,
      zoom: null,
      lightLayer: null, 
      coordinate: null,
    };
  },
  mounted() {
    if (this.coordinate) {
      this.drawPoint(this.coordinate);
    }
  },
  watch: {},
  methods: {
    // 初始化地图
    init() {
      let self = this;
      self.$nextTick(() => {
        self.layers = new ImageLayer({
          extent: [911908.3769988124, 110617.87078181792,1596307.9757537232, 420506.5270969288], // 边界,
          source: new ImageWMS({
            url: "http://127.0.0.1:8000/geoserver/nyc/wms",
            // Layers需要指定要显示的图层名
            params: {
              LAYERS: "nyc:nyc_roads",
              exceptions: "application/vnd.ogc.se_inimage",
              FORMAT: "image/png",
            },
            serverType: "geoserver",
          }),
          
        });
        // 摄像头位置所放的图层
        this.lightLayer = new VectorLayer({
          source: new VectorSource({ features: [] }),
        });
        // 绘制地图线的图层
        this.map = new Map({
          layers: [this.layers,this.lightLayer],
          target: "pMap",
          view: new View({
           //地图中心点
            center: [987777.93778, 213834.81024],
            zoom: 12,
            maxZoom: 20,
            minZoom: 4,
          }),
        });
         this.onPoint();
      });
    },
    drawPoint(data, isTrue) {
      let url = "";
      url = "/images/video.png";
      this.$nextTick(() => {
        if (isTrue) {
          this.removePoint();
        }
        let feature = new Feature({
          // geometry 几何图形
          geometry: new Point([Number(data[0]), Number(data[1])]),
        });
        let style = new Style({
          image: new Icon({
            scale: 0.3,
            src: url,
            anchor: [0.48, 0.52],
          }),
        });
        feature.setStyle(style);
        this.lightLayer.getSource().addFeature(feature);
      });
    },
    removePoint() {
      let self = this;
      let allPointFeatures = self.lightLayer.getSource().getFeatures();
      allPointFeatures.forEach((item) => {
        self.lightLayer.getSource().removeFeature(item);
      });
    },
    onPoint() {
      // 监听singleclick事件
      let _this = this;
      this.map.on("singleclick", function (e) {
        _this.coordinate = e.coordinate;
        if (_this.coordinate) {
          _this.drawPoint(_this.coordinate, true);
        }
      });
    },

    handleClose() {
      this.dialogVisible = false;
    },
    submitForm() {
      this.$emit("childEvent", this.coordinate);
      this.dialogVisible = false;
    },
    cancel() {
      this.dialogVisible = false;
    },
  },
};
</script>
<style>
#pMap {
  width: 100%;
  height: 80vh;
}
.el-dialog__header {
  background-color: #409eff;
}
.el-dialog__title,
.el-dialog__close {
  color: #fff !important;
}
.el-dialog__body {
  padding: 5px;
}

.showPoint {
  position: absolute;
  top: 50px;
  color: #070707;
  z-index: 1;
  left: 50px;
}
.dialogfooter {
  position: absolute;
  bottom: 10px;
  right: 10px;
}
</style>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.

5、实现flv.js进行摄像头预览组件previedVideo.vue

<template>
  <div class="container" v-if="videoOpen">
    <!-- 摄像头 -->
    <VueDragResize v-if="videoOpen" class="drag" :w="800" :h="600" v-on:resizing="resize" v-on:dragging="resize">
        <!-- 摄像头 -->
        <div v-if="videoOpen" class="video-dialog">
            <div class="body-container">
                <img
                    class="close-video"
                    src="@/assets/video/close.png"
                    alt=""
                    @click="videoClose"
                />
                <div v-loading="loading" :element-loading-text="loadingText" style="width: 100%;height: 100%">
                    <video v-if="videoOpen" ref="video" id="video" class="video" muted></video>
                </div>
              </div>
          </div>
      </VueDragResize>
  </div>
  </template>
    
  <script>
  import flvjs from "flv.js/dist/flv.min.js";
  import VueDragResize from "vue-drag-resize";
  export default {
    name: 'preVideo',
    components: { VueDragResize },
    props: {
        videoOpen: {
            default: false,
        },
        url: {
            type: String,
            default: '',
        }
    },
    data() {
      return {
        options: { top: 0, left: 0, width: 0, height: 0 },
        flvPlayer: null,
        loading: true,
        loadingText: '加载中',
      };
    },
    watch: {
        url(newVal) {
            if(newVal == '') this.stopPlay();
            else this.$nextTick(() => {
                this.startPlay()
            })
        },
    },
    methods: {
      resize(newRect) {
        this.options.width = newRect.width;
        this.options.height = newRect.height;
        this.options.top = newRect.top;
        this.options.left = newRect.left;
      },
      startPlay() {
        this.loading = true;
        if (flvjs.isSupported()) {
            let videoElement = document.getElementById("video");
            this.flvPlayer = flvjs.createPlayer(
                {
                    type: "flv",
                    url: this.url,
                    hasAudio: false,
                    isLive: false,
                }
            );
            this.flvPlayer.attachMediaElement(videoElement);
            this.flvPlayer.load();
            this.flvPlayer.play();
            this.loading = false;
        } else {
            this.loading = false;
            this.$message.error("当前浏览器暂不支持flvjs视频流播放");
        }
      },
      stopPlay() {
        if (!this.flvPlayer) return;
        this.flvPlayer.pause(); //停止播放
        this.flvPlayer.unload(); //停止加载
        this.flvPlayer.detachMediaElement(); //销毁实例
        this.flvPlayer.destroy();
        this.flvPlayer = null;
        this.loading = true;
      },
      videoClose() {
        this.$emit('close')
      }
    },
  };
  </script>
    
<style lang="scss" scoped>
.container {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1001;
}
.drag {
  cursor: move;
  z-index: 1000;
}

.video-dialog {
  z-index: 1000;
    width: 100%;
    height: 100%;
    display: flex;
    background-color: #00000064;
    justify-content: center;
    align-items: center;
    background-image: url("~@/assets/video/bg.png");
    background-repeat: no-repeat;
    background-size: 100% 100%;
    -moz-background-size: 100% 100%;
    ::v-deep .el-dialog__header,
    .el-dialog__footer {
      display: none !important;
    }
    ::v-deep .el-dialog__body {
      padding: 20px 20px 10px 20px;
    }
  }
  ::v-deep .el-dialog {
    background-color: transparent !important;
    box-shadow: none;
    margin-top: 0px !important;
    background-image: url("../../assets/video/bg.png");
    background-repeat: no-repeat;
    background-size: 100% 100%;
    -moz-background-size: 100% 100%;
  }
  .body-container {
    position: relative;
    width: 100%;
    height: 100%;
  }
  .close-video {
    position: absolute;
    width: 60px;
    height: 55px;
    top: 10px;
    right: 10px;
    z-index: 999;
    &:hover {
      cursor: pointer;
      color: rgba(112, 165, 255, 0.517);
    }
  }
  .videoPlayer {
    margin: 10px;
    display: flex;
    justify-content: center;
    align-items: center;
    ::v-deep .video-js {
      width: 100%;
      height: 80vh;
      margin: 20px;
      // height: 90%;
    }
  }
  .close-video {
    position: absolute;
    width: 60px;
    height: 55px;
    top: 0px;
    right: 0px;
    z-index: 9999;
    &:hover {
      cursor: pointer;
    }
  }
  #video {
    width: 100%;
    height: 100%;
    padding: 20px;
    position: absolute;
    bottom: 0;
  }
  </style>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.

6、以上所需引入依赖

vue-drag-resize 可拖动缩放的组件

npm i -s vue-drag-resize
  • 1.

flv.js

npm install --save flv.js
  • 1.

可参考如下:

Nginx-http-flv-module流媒体服务器搭建+模拟推流+flv.js在前端html和Vue中播放HTTP-FLV视频流:

Nginx-http-flv-module流媒体服务器搭建+模拟推流+flv.js在前端html和Vue中播放HTTP-FLV视频流_霸道流氓气质的博客

7、添加请求后台api的streamVideo.js

import request from '@/utils/request'

export function getVideoInfo(query) {
    return request({
        url: '/streamMediaVideo/getVideoInfo',
        method: 'get',
        params: query
    })
}
// 新增
export function addVideo(query) {
    return request({
        url: '/streamMediaVideo/',
        method: 'post',
        params: query
    })
}


// 修改
export function updateVideo(query) {
    return request({
        url: '/streamMediaVideo',
        method: 'put',
        data: query
    })
}

// 删除
export function delVideo(id) {
    return request({
        url: '/streamMediaVideo/' + id,
        method: 'delete'
    })
}

// 设置视频流服务数据get请求
export function setStreamData(query) {
    return request({
        url: '/streamMediaVideo/setStreamMediaParam',
        method: 'post',
        params: query
    })
}
// 设置视频流服务数据get请求
export function getStreamData(query) {
    return request({
        url: '/streamMediaVideo/getStreamMediaParam',
        method: 'get',
        params: query
    })
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.

8、如何模拟一个rtsp的视频流

这里以海康威视摄像rtsp流地址模拟为例

Windows上使用FFmpeg实现本地视频推送模拟海康协议rtsp视频流:

Windows上使用FFmpeg实现本地视频推送模拟海康协议rtsp视频流_霸道流氓气质的博客

其他厂家协议格式在页面上新增说明按钮,格式内容

大华

大华摄像机RTSP地址规则为:rtsp://[username]:[password]@[ip]:[port]/cam/realmonitor?channel=1&subtype=0
说明:

username: 用户名。例如admin。
password: 密码。例如admin123。
ip: 为设备IP。例如 192.168.1.101。
port: 端口号默认为554,若为默认可不填写。
channel: 通道号,起始为1。例如通道2,则为channel=2。
subtype: 码流类型,主码流为0(即subtype=0),辅码流为1(即subtype=1)。

如:rtsp://admin:admin123@192.168.1.101/cam/realmonitor?channel=1&subtype=1

海康

rtsp://[username]:[password]@[ip]:[port]/[codec]/[channel]/[subtype]/av_stream

说明:
username: 用户名。例如admin。
password: 密码。例如admin123。
ip: 为设备IP。例如192.168.1.104。
port: 端口号默认为554,若为默认可不填写。
codec:有h264、MPEG-4、mpeg4这几种。
channel: 通道号,起始为1。例如通道1,则为ch1。
subtype: 码流类型,主码流为main,辅码流为sub。

如:rtsp://admin:admin123@192.168.1.104/h264/ch1/subtype/av_stream

宇视

rtsp://[username]:[password]@[ip]:[port]/media/video1/2/3

说明:
username: 用户名。例如admin。
password: 密码。例如admin123。
ip: 为设备IP。例如 192.168.1.107。
port: 端口号默认为554,若为默认可不填写。
video: 1代表主码流、2辅码流、3第三码流

如:rtsp://admin:admin123@192.168.1.107/media/video2

华为

rtsp://[username]:[password]@[ip]:[port]/LiveMedia/[channel]/Media1/2

说明:
username: 用户名。例如admin。
password: 密码。例如admin123。
ip: 为设备IP。例如192.168.1.110。
port: 端口号默认为554,若为默认可不填写。
channel: 通道号,起始为1。例如通道1,则为ch1。
Media:1代表主码流、2辅码流

如:rtsp://admin:admin123@192.168.1.110/LiveMedia/ch1/Media2
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.

示例代码下载:

含数据库mysql、前后端代码、Zlmediakit在windows上编译后程序以及运行报错常用dll