vue3+setup使用rtsp视频流实现实时监控,全屏,拍摄,自动拍摄等功能(纯前端)
概要
本文介绍了如何在Vue应用中通过WebRTC技术获取摄像头的rtsp视频流,同时展示了实时监控,全屏,拍摄,自动拍摄等功能。
一、获取rtsp流并确保其可用
1.因为是纯前端角度,所以从后端可以获取http开头的视频流地址(其实是在后端电脑打开,然后本地接入后端电脑,进行查看或修改配置)因为rstp流需要转码,所以在摄像头配置里面要修改转码格式
2.前端自己可以下载安装VLC播放器然后进行调试设置
二、在vue3项目里引入rtsp流
1.下载webrtc-streamer
下载地址:https://github.com/mpromonet/webrtc-streamer/releases
下载版本根据自己电脑进行选择,双击webrtc-streamer.exe可执行文件后在浏览器访问http://127.0.0.1:8000,可以看到自己电脑正在操作的内容。
2.将/html/libs目录下的adapter.min.js和/html目录下面的webrtcstreamer.js文件分别拷贝到vue项目public中,并在index.html文件全局引入。
<script src="/adapter.min.js"></script>
<script src="/webrtcstreamer.js"></script>
三、具体代码,video封装
1.父页面
<script setup>
import { ref } from 'vue'
import videoRtmp from './videoRtmp.vue'
const list = ref([])
const idlist = ref([])
const imgArr = ref([]) // 手动抓拍的图片
const screenshotUrl = ref([]) // 自动抓拍的图片
const total = ref(0)
const currentPage = ref(1)
const keyWord = ref('')
const rtmpUrl = ref('rtsp://用户:密码@电脑网址:554')
// videoFlag 判断是单个还是多个video 对应不同的样式 false是单个 true是多个
const form = ref({ videoFlag: false, videoList: [] })
const openVideo = ref(false)
const screenButton = ref(false) // 默认解禁 点击抓拍按钮后禁止 抓拍完成后解禁
const sButton = ref(true) // 默认禁止 加载完成后解禁
let screenNums = 0 // 加载完成的数量
const loadData = (pageNum = currentPage.value, pageSize = 10) => {
list.value = [
{
id: 1,
idName: 'id1',
name: '设备1',
rtmpUrl: 'rtsp://用户:密码@电脑网址:554',
userName: 'admin',
screenFlag: false,
},
{
id: 2,
idName: 'id2',
name: '设备2',
code: '002',
userName: 'admin',
password: 'VWEFKS',
screenFlag: false,
rtmpUrl: 'rtsp://用户:密码@电脑网址:554',
},
]
total.value = list.value.length
}
//分页
const handlePageChange = (page) => {
currentPage.value = page
loadData()
}
// 搜索
const searchHandle = async () => {
currentPage.value = 1
loadData()
}
/** 选择条数 */
function handleSelectionChange(selection) {
console.log(selection)
idlist.value = selection
}
//单个点击观看
const videoLook = (row) => {
console.log(row)
form.value.videoList = [row]
form.value.videoFlag = false
openVideo.value = true
}
//多个观看
const videoAllLook = (row) => {
console.log(row)
form.value.videoList = idlist.value
form.value.videoFlag = idlist.value.length > 1 ? true : false
openVideo.value = true
}
//点击叉号
const closeHandle = () => {
resetData()
}
//清空数据
const resetData = () => {
openVideo.value = false
imgArr.value = []
screenshotUrl.value = []
screenNums = 0
screenButton.value = false
form.value = {}
loadData()
}
// 抓拍
const screenshot = () => {
form.value.videoList.forEach((v) => {
v.screenFlag = true
})
screenButton.value = true
}
//父子传值 获取抓拍的
const handleScreenshot = (data, flag) => {
console.log(data.id, data,flag)
if (!flag) {
// 自动抓拍
screenshotUrl.value.push(data.image)
imageVideoApi()
return
}
// 手动抓拍
form.value.videoList.forEach((v) => {
if (v.id == data.id) {
v.screenFlag = false
}
})
imgArr.value.push(data.image)
if (!form.value.videoList.some((a) => a.screenFlag)) {
screenButton.value = false
imageVideoApi()
}
}
// 父子传值 获取video是否成功
// 因为video加载需要一段时间,在这段时间里,不能抓拍,所以要在这段时间给它禁用掉,等到video加载完后,在解禁
// 这里做判断 在子页面加载完成emit返回主页面 然后判断返回的数量 符合video的数量,说明都加载完了
const screenButtonS = () => {
screenNums++
if (form.value.videoList.length == screenNums) {
sButton.value = false
}
}
// 图片检测接口
const imageVideoApi = async () => {
let files = base64ToFile(imgArr.value[0], 'image.png')
let res = await imageVideo({ file: files })
}
// base64转file
function base64ToFile(base64Data, filename) {
// 将base64的数据部分提取出来
const arr = base64Data.split(',')
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
// 将Uint8Array转换为Blob对象
const blob = new Blob([u8arr], { type: mime })
// 创建File对象
const file = new File([blob], filename, { type: mime })
return file
}
loadData()
</script>
<template>
<div>
<el-form class="main-form" :inline="true">
<el-form-item label="设备名称:">
<el-input v-model="keyWord" placeholder="请输入设备名称" />
</el-form-item>
<el-form-item>
<el-button :icon="Search" type="primary" @click="searchHandle">搜索</el-button>
</el-form-item>
<el-form-item>
<el-button :icon="Search" type="primary" :disabled="idlist.length <= 0" @click="videoAllLook">多流播放</el-button>
</el-form-item>
</el-form>
<!-- 列表部分 -->
<el-table border :data="list" height="460" scrollbar-always-on="true" @selection-change="handleSelectionChange">
<el-table-column align="center" type="selection" width="50" />
<el-table-column label="设备名称" prop="name" />
<el-table-column label="账号" prop="userName" width="100" />
<el-table-column fixed="right" label="操作" width="160">
<template #default="scope">
<el-button size="small" type="primary" @click="videoLook(scope.row)">播放</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
background
:current-page="currentPage"
:hide-on-single-page="true"
layout="prev, pager, next"
:total="total"
@current-change="handlePageChange"
/>
<el-dialog title="实时监控" width="900px" :close-on-click-modal="false" @close="closeHandle" v-model="openVideo">
<div v-if="!form.videoFlag">
<h2 style="text-align: center">{{ form.videoList[0]?.name }}</h2>
<video-rtmp
:rtspUrl="form.videoList[0]?.rtmpUrl"
:id="form.videoList[0]?.idName"
:screenFlag="form.videoList[0]?.screenFlag"
:data="form.videoList[0]"
@screenshot="handleScreenshot"
@screenButton="screenButtonS"
/>
</div>
<div class="videoAll" v-else>
<div v-for="(item, index) in form.videoList" :key="index">
<h2 style="text-align: center">{{ item?.name }}</h2>
<video-rtmp
:rtspUrl="item?.rtmpUrl"
:id="item?.idName"
:screenFlag="item?.screenFlag"
:data="item"
@screenshot="handleScreenshot"
@screenButton="screenButtonS"
/>
</div>
</div>
<div>
<el-button type="primary" :disabled="screenButton || sButton" v-preventReClick @click="screenshot">抓拍</el-button>
</div>
<div>
手动抓拍
<img style="width: 300px; height: 100px" v-for="(item, index) in imgArr" :key="index" :src="item" />
</div>
<div>
自动抓拍
<img style="width: 100px; height: 50px" v-for="(item, index) in screenshotUrl" :key="index" :src="item" />
</div>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.videoAll {
> div {
width: 45%;
height: 45%;
display: inline-block;
}
> div:nth-child(odd) {
margin-right: 5%;
}
}
</style>
2.子页面
<script setup>
defineOptions({
name: 'VideoRtmp',
})
// 如果不想放到public里面并在index.html里面引入,也可以选择其他位置
// import WebRtcStreamer from './webrtcstreamer.js'
import { ref } from 'vue'
const emit = defineEmits()
const props = defineProps({
rtspUrl: {
type: String,
default: '',
},
id: {
type: String,
default: '',
},
data: {
type: Object,
default: '',
},
screenFlag: {
type: Boolean,
default: '',
},
})
const videoRef = ref(null)
const webRtcServer = ref(null)
const srcUrl = ref(process.env.NODE_ENV === 'development' ? 'http://网址:端口' : 'http://网址:端口') //这里看自己的需要,也可以传入另一台电脑的ip,前提是都得在在一个局域网内
// 设置抓拍间隔时间,单位为毫秒,这里设置为5000毫秒(即5秒)
const captureInterval = 5000
// 新增:用于存储定时器标识
const captureTimer = ref(null)
watch(
() => props.rtspUrl,
(val) => {
if (val) {
// 这里放开会报错,还没有找到原因
// webRtcServer.value.disconnect()
nextTick(() => {
initVideo()
})
}
},
{ deep: true, immediate: true }
)
watch(
() => props.screenFlag,
(val) => {
if (val) {
handleScreenshot(true)
}
},
{ deep: true, immediate: true }
)
function initVideo() {
try {
//video:需要绑定的video控件ID
//127.0.0.1:8000:启动webrtc-streamer的设备IP和端口,默认8000
//连接后端的IP地址和端口
// 静音
videoRef.value.volume = 0
webRtcServer.value = new WebRtcStreamer(props.id, srcUrl.value)
//需要查看的rtsp地址,根据自己的摄像头传入对应的rtsp地址即可。注意:视频编码格式必须是H264的,否则无法正常显示,编码格式可在摄像头的后台更改
//向后端发送rtsp地址
webRtcServer.value.connect(props.rtspUrl)
setTimeout(() => {
emit('screenButton')
}, 1000)
} catch (error) {
console.log(error)
}
}
/* 处理双击 视频全屏*/
const dbClick = () => {
const elVideo = document.getElementById(props.id)
if (elVideo.webkitRequestFullScreen) {
elVideo.webkitRequestFullScreen()
} else if (elVideo.mozRequestFullScreen) {
elVideo.mozRequestFullScreen()
} else if (elVideo.requestFullscreen) {
elVideo.requestFullscreen()
}
}
/* 抓拍*/
function handleScreenshot(flag) {
let video = document.getElementById(props.id) //获取dom节点
let canvas = document.createElement('canvas') //创建canvas节点
let w = window.innerWidth
let h = (window.innerWidth / 16) * 9
canvas.width = w
canvas.height = h //设置canvas宽高
const ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, w, h) //video写入到canvas
let obj = {
name: props.data.name,
id: props.data.id,
image: canvas.toDataURL('image/jpge'), //生成截图地址
flags: flag,
}
// flag true 手动抓拍 false 自动抓拍
emit('screenshot', obj, flag)
}
// 开始自动抓拍
const startAutoCapture = () => {
captureTimer.value = setInterval(() => {
handleScreenshot(false)
}, captureInterval)
}
// 新增:停止自动抓拍功能
const stopAutoCapture = () => {
if (captureTimer.value) {
clearInterval(captureTimer.value)
captureTimer.value = null
}
}
onMounted(() => {
startAutoCapture()
})
//销毁视频流
onUnmounted(() => {
webRtcServer.value.disconnect()
stopAutoCapture()
})
</script>
<!-- style="object-fit: fill; 设置视频内容撑满整个video标签 -->
<template>
<div class="video-contianer">
<video ref="videoRef" :id="id" controls autoplay muted width="100%" height="100%" style="object-fit: fill"></video>
<div class="mask" @dblclick="dbClick"></div>
</div>
</template>
<style scoped lang="scss">
.video-contianer {
position: relative;
.mask {
position: absolute;
cursor: pointer;
top: 25%;
left: 0;
width: 100%;
height: 50%;
}
}
</style>
小结
第一次写这种实时监控加抓拍的功能,可能还有一些不足,后面再看看有没有其他更好的办法。 本地开发的时候要双击运行webrtc-streamer.exe可执行文件后,vue项目才能展示实时监控,打包发布到线上的时候可能要让后台在服务器上安装webrtc-streamer并提供真实的地址,关于这点现在还没来得及试