场景
SpringBoot+Vue+Openlayers实现地图上新增和编辑坐标并保存提交:
SpringBoot+Vue+Openlayers实现地图上新增和编辑坐标并保存提交_霸道流氓气质的博客-CSDN博客
开源流媒体服务器ZLMediaKit在Windows上运行、配置、按需拉流拉取摄像头rtsp视频流)并使用http-flv网页播放:
开源流媒体服务器ZLMediaKit在Windows上运行、配置、按需拉流拉取摄像头rtsp视频流)并使用http-flv网页播放_srs按需拉流_霸道流氓气质的博客-CSDN博客
GeoServer简介、下载、配置启动、发布shapefile全流程(图文实践):
GeoServer简介、下载、配置启动、发布shapefile全流程(图文实践)_霸道流氓气质的博客-CSDN博客
若依前后端分离版手把手教你本地搭建环境并运行项目:
若依前后端分离版手把手教你本地搭建环境并运行项目_ruoyi本地调式_霸道流氓气质的博客-CSDN博客
结合以上流程,需要实现对摄像头名称、在地图上位置的增删改查以及摄像头的预览功能。
注意这里的摄像头只支持H264编码格式的拉流和播放。
实现摄像头预览效果
地图上新增和编辑坐标效果
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
实现
1、参考以上搭建前后端分离框架,建表如下
建表语句:
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;
2、摄像头拉流预览流程
选择某个摄像头进行预览时,先校验流媒体相关信息是否设置,是才能预览
拿着该条数据的rtsp流地址调用ZLMediaKit的拉流的api进行拉流。
api拼接规则:
http://流媒体服务ip:流媒体服务端口/index/api/addStreamProxy?vhost=流媒体服务ip&app=live&stream=拼接时间戳&url=摄像头rtsp地址&secret= 流媒体服务秘钥
调用拉流api时需要流媒体服务器的ip和端口以及秘钥信息,通过配置页面进行配置并进行修改时的二次密码校验
然后解析拉流接口返回的json数据
状态码不为0或接口不通无响应等则提示不可预览,状态码为0则可预览。
解析data下的key,按照/解析,比如上面解析出ip:127.0.0.1,流应用live,流id为test。
进行http-flv视频预览, 预览url拼接规则:
http://key解析的ip:流媒体服务端口/key解析的live/key解析的test.flv
这里的流程可参考上面博客。
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;
}
流媒体设置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;
}
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);
}
注意这里使用了自定义缓存注解。
SpringBoot中通过自定义缓存注解(AOP切面拦截)实现数据库数据缓存到Redis:
SpringBoot中通过自定义缓存注解(AOP切面拦截)实现数据库数据缓存到Redis_redis切面_霸道流氓气质的博客-CSDN博客
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>
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);
}
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);
}
}
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));
}
}
注意上面流媒体参数设置和获取接口直接存储进redis中。
前缀使用常量类存储:
public class RedisPTConstants {
public static final String STREAMMEDIAPARAM = "streamMediaParam:";
}
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>
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>
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>
6、以上所需引入依赖
vue-drag-resize 可拖动缩放的组件
npm i -s vue-drag-resize
flv.js
npm install --save flv.js
可参考如下:
Nginx-http-flv-module流媒体服务器搭建+模拟推流+flv.js在前端html和Vue中播放HTTP-FLV视频流:
Nginx-http-flv-module流媒体服务器搭建+模拟推流+flv.js在前端html和Vue中播放HTTP-FLV视频流_霸道流氓气质的博客-CSDN博客
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
})
}
8、如何模拟一个rtsp的视频流
这里以海康威视摄像rtsp流地址模拟为例
Windows上使用FFmpeg实现本地视频推送模拟海康协议rtsp视频流:
Windows上使用FFmpeg实现本地视频推送模拟海康协议rtsp视频流_霸道流氓气质的博客-CSDN博客
其他厂家协议格式在页面上新增说明按钮,格式内容
大华
大华摄像机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
示例代码下载:
含数据库mysql、前后端代码、Zlmediakit在windows上编译后程序以及运行报错常用dll
https://download.csdn.net/download/BADAO_LIUMANG_QIZHI/89034337