<!-- 流媒体 --> <script lang="ts" setup name="videoControl"> import type { FormInstance, FormRules } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus' import html2canvas from 'html2canvas' import type { Ref } from 'vue' import { getCurrentInstance, nextTick, reactive, ref } from 'vue' import dayjs from 'dayjs' import { getMediaStream, getMediaToken } from '@/api/ycjg/media' import { debounce } from 'lodash-es' import useWebsocketStore from '@/store/modules/websocket' import { getDevInfo, ptzControl, restartDev } from '@/api/ptz/dev' import controlImg from '@/assets/images/control.png' import { devControl, devControlWithSpeed, devToPosition, } from '@/api/ptz/control' import { getPicListPage, pictureAdd } from '@/api/ycjg/ssjk' import { base64ToBlob, exportFile } from '@/utils/exportUtils' import { log } from '@/utils/log' import { getPreviewUrl } from '@/utils/hik' import { initPlugin, login, logout, preview, stop } from '@/utils/HKVideo' import { loginDH } from '@/utils/DHVideo' const websocket = useWebsocketStore() const $route = useRoute() const deviceData: Ref<any> = ref({}) const instance = getCurrentInstance() const videoControl = ref('videoControl') const loading = ref(false) const total = ref(0) const list = ref([]) const refreshHK = ref(true) const hkPlugin: any = ref(null) const dhVideo: any = ref(null) const bottomRef = ref() const lineList: Ref<any[]> = ref([]) const timeRange = ref<[any, any]>(['', '']) const mediaToken = ref('') // 流媒体token const mediaUrl = ref('') const video = ref(null) as any const videoRef = ref(null) const listQuery = ref({ devId: '', startTime: '', endTime: '', offset: 1, limit: 6, }) // socket更新数据 const unwatch = watch(websocket, (newVal) => { }) // 搜索重置 function fetchData() { search() } function search(isNowPage = false) { if (!isNowPage) { // 是否显示当前页,否则跳转第一页 listQuery.value.offset = 1 } loading.value = true getPicListPage(listQuery.value).then((res: any) => { list.value = res.data.rows.map((item: any) => { item.url = `${window.localStorage.getItem('baseurl-safe')}/picture/download/${item.url}?token=${window.localStorage.getItem('token')}` return item }) total.value = res.data.total console.log(list.value) loading.value = false }).catch(() => { loading.value = false }) } // 调整大小 const resize = debounce(() => { // if (deviceData.value.nvrManufacture === 'HIKVISION') { // stop(0) // refreshHK.value = true // nextTick(() => { // initPlugin(hkPlugin.value.clientWidth, hkPlugin.value.clientHeight, 1, '', false, () => { // setTimeout(() => { // preview(deviceData.value.nvrIp, '80', deviceData.value.nvrUser, deviceData.value.nvrPassword, 0, true, deviceData.value.nvrChannel) // }, 1000) // }, () => { // }) // }) // } }, 1000) const handleResize = () => { if (deviceData.value.nvrManufacture === 'HIKVISION') { refreshHK.value = false } resize() } function dataURLtoBlob(dataurl: any) { const arr = dataurl.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) } return new Blob([u8arr], { type: mime }) } // 抓拍 async function takePhoto() { const time = dayjs() log('视频抓拍', `${deviceData.value.monitorName}_抓拍时间:${time.format('YYYY-MM-DD HH:mm:ss')}`) const name = `${deviceData.value.monitorName}_${time.format('YYYY-MM-DD HH_mm_ss')}` console.log('流媒体抓拍') // const canvas: any = document.getElementById('myCanvas') // // const canvas: any = document.createElement('canvas') // const ctx = canvas!.getContext('2d') // canvas.width = video.value.videoWidth // canvas.height = video.value.videoHeight // ctx.drawImage(video.value, 0, 0, canvas.width, canvas.height) // // domToPic(bottomRef.value, time) // console.log(canvas.toDataURL('image/png')) const player: any = document.getElementById('myVideo') // 获取video的Dom节点 player.setAttribute('crossOrigin', 'anonymous') // 添加srossOrigin属性,解决跨域问题 const canvas = document.createElement('canvas') // const canvas: any = document.getElementById('myCanvas') const img = document.createElement('img') img.crossOrigin = 'anonymous' // 这行很重要 canvas.width = video.value.videoWidth canvas.height = video.value.videoHeight canvas.getContext('2d')!.drawImage(player, 0, 0, canvas.width, canvas.height) // 截图 img.src = canvas.toDataURL('image/png', 1)+ '?time=' + new Date() // img.src = canvas.toDataURL('image/png', 1) // image.onload = () => { // canvas.width = 500; // canvas.height = 500; // ctx.drawImage(image, 0, 0, 500, 500); // var imgSrc = canvas.toDataURL('image/png', 1); // console.log(imgSrc); // } // const imgUrl = canvas.toDataURL('image/png') // 将图片转成base64格式 // const aLink = document.createElement('a') // aLink.id = 'qrcodeimg' // aLink.href = imgUrl // aLink.download = `${deviceData.value.monitorName}_${time.format('YYYY-MM-DD HH_mm_ss')}.png` // 导出文件名 // document.body.appendChild(aLink) // // 模拟a标签点击事件 // aLink.click() // // 事件已经执行,删除本次操作创建的a标签对象 // document.body.removeChild(aLink) // // 上传到服务器 // const jpg = canvas.toDataURL('image/jpeg') // console.log(jpg); // setTimeout(() => { // pictureAdd({ // devId: $route.query.id, // picture: jpg, // }).then((res: any) => { // ElMessage.success('抓图成功') // search() // }) // }, 3000) // img.src = dataURL // img.width = player.clientWidth-200; //控制截出来的图片宽的大小 // img.height = player.clientHeight-200; //控制截出来的图片高的大小 // img.style.border="1px solid #333333" //控制截出来的图片边框的样式 // document.getElementById("cutImage").appendChild(img); //显示在页面中 // this.downFile(dataURL, "图片.jpg"); //下载截图 // canvas.toDataURL('image/png') // const a = document.createElement('a'); // a.href = canvas.toDataURL('image/png'); // a.download = `${Date.now()}`; // a.click(); } // html2canvas此方法video截不了 function domToPic(dom: any, time: any) { html2canvas(dom).then((res) => { // 下载到本地 const imgUrl = res.toDataURL('image/png') console.log('抓图url', imgUrl) const aLink = document.createElement('a') aLink.id = 'qrcodeimg' aLink.href = imgUrl aLink.download = `${deviceData.value.monitorName}_${time.format('YYYY-MM-DD HH_mm_ss')}.png` // 导出文件名 document.body.appendChild(aLink) // 模拟a标签点击事件 aLink.click() // 事件已经执行,删除本次操作创建的a标签对象 document.body.removeChild(aLink) // 上传到服务器 const jpg = res.toDataURL('image/jpeg') pictureAdd({ devId: $route.query.id, picture: jpg, }).then((res: any) => { ElMessage.success('抓图成功') search() }) }) } // 恢复 async function handleRealplay() { video.value.play() ElMessage.success('已恢复') } // 暂停 function handleStopPlay() { video.value.pause() ElMessage.success('已暂停') } // 放大 function zoomIn(action: string) { if (deviceData.value.deviceTypeName === '枪机') { ElMessage.warning('类型为枪机的设备,不可进行控制!') return false } const params = { command: action, horizonSpeed: '30', verticalSpeed: '30', zoomSpeed: '30', } ptzControl(deviceData.value.nvrIndexCode, deviceData.value.cameraIndexCode, params).then((res) => {}) } // 上下左右 function controlWithSpeed(event: any, type: any) { if (deviceData.value.deviceTypeName === '枪机' && type !== 'stop') { ElMessage.warning('类型为枪机的设备,不可进行控制!') return false } if (event.detail === 1) { const params = { command: type, zoomSpeed: '30', horizonSpeed: '30', verticalSpeed: '30', } ptzControl(deviceData.value.nvrIndexCode, deviceData.value.cameraIndexCode, params).then((res) => {}) } } function handleSizeChange(val: number) { // emit('change', { size: val }) listQuery.value.limit = val search(true) } // 改变当前页 function handleCurrentChange(val: number) { // emit('change', { page: val }) listQuery.value.offset = val search(true) } const disabledPastDateStart = (date: any) => { if (listQuery.value.endTime !== '') { return date.getTime() > new Date(`${listQuery.value.endTime} 23:59:59`).getTime() } else { return null } } const disabledPastDateEnd = (date: any) => { if (listQuery.value.startTime) { return date.getTime() < new Date(`${listQuery.value.startTime} 00:00:00`).getTime() } else { return null } } // 拉取流 async function fetchMediaStream(deviceId: string, channelId: string) { loading.value = true const res = await getMediaStream(deviceId, channelId, mediaToken.value) loading.value = false if (res && res.data) { mediaUrl.value = res.data.fmp4 return res.data.fmp4 } else { ElMessage.warning('设备未注册') } } watch(timeRange, (val) => { if (val) { listQuery.value.startTime = `${val[0]}` listQuery.value.endTime = `${val[1]}` } else { listQuery.value.startTime = '' listQuery.value.endTime = '' } }) onMounted(() => { if ($route.query && $route.query.id) { listQuery.value.devId = $route.query.id!.toString() getDevInfo(listQuery.value.devId).then((response) => { deviceData.value = response.data setTimeout(() => { video.value = document.getElementById('myVideo') // 获取流媒体token getMediaToken().then((res: any) => { mediaToken.value = res.data fetchMediaStream(deviceData.value.cameraIndexCode, deviceData.value.nvrIndexCode) }) handleResize() window.addEventListener('resize', handleResize) }, 200) }) fetchData() } }) onBeforeUnmount(() => { try { video.value = null } catch (e) {} unwatch() try { // logout(deviceData.value.nvrIp) // stop(0) } catch (e) {} window.removeEventListener('resize', handleResize) }) </script> <template> <div class="video-wrap"> <video id="myVideo" ref="videoRef" :src="mediaUrl" controls muted autoPlay class="videoControl" /> <!-- <canvas id="myCanvas" style="height: calc(100% - 165px);width: calc(100% - 390px);"/> --> <!-- <div class="right"> <el-tabs tab-position="top" style="height: calc(100% - 30px);width: 100%;margin-bottom: 10px;margin-top: -10px;"> <el-tab-pane label="历史截图"> <div style="display: flex;margin-bottom: 10px"> <el-date-picker v-model="timeRange" type="daterange" clearable size="small" format="YYYY-MM-DD" value-format="YYYY-MM-DD" start-placeholder="开始时间" end-placeholder="结束时间" range-separator="到" style="margin-right: 20px" /> <el-button type="primary" @click="fetchData" style="width: 60px" size="small">查 询</el-button> </div> <div style="background-color: white;overflow-y: hidden;overflow-x: scroll;width: 100%;display: flex;flex-wrap: wrap;"> <div v-for="(item, index) in list" :key="index" style="height: 100px;width: 160px;margin: 5px"> <el-image style="width: 160px; height: 100px;" :src="item.url" fit="fill" /> </div> </div> </el-tab-pane> </el-tabs> <div style="z-index: 99999999;margin: 10px 10px 0;"> <el-pagination small :current-page="listQuery.offset" :page-sizes="[6]" :page-size="listQuery.limit" :total="total" layout="total,prev,pager,next" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> </div> --> <div class="bottom" ref="bottomRef"> <div class="control-title"> 设备控制 </div> <div name="four" style="width: 150px; height: 130px;"> <el-image style="width: 130px; height: 130px;position: absolute;top: 5px;left: 50px;" :src="controlImg" fit="fill" /> <div> <div class="round-btn" style="top: 8px;left: 95px;" @mousedown="controlWithSpeed($event, 'up')" @mouseup="controlWithSpeed($event, 'stop')" /> <div class="round-btn" style="top: 90px;left: 95px;" @mousedown="controlWithSpeed($event, 'down')" @mouseup="controlWithSpeed($event, 'stop')" /> <div class="round-btn" style="top: 48px;left: 53px;" @mousedown="controlWithSpeed($event, 'left')" @mouseup="controlWithSpeed($event, 'stop')" /> <div class="round-btn" style="top: 48px;left: 135px;" @mousedown="controlWithSpeed($event, 'right')" @mouseup="controlWithSpeed($event, 'stop')" /> </div> </div> <div style="display: flex;max-width: 250px;flex-wrap: wrap;margin: 0 20px;"> <el-button type="primary" class="btn" @click="handleStopPlay"> <el-icon style="margin-right: 5px;"> <svg-icon name="icon-stop" /> </el-icon>暂 停 </el-button> <el-button type="primary" class="btn" @click="handleRealplay"> <el-icon style="margin-right: 5px;"> <svg-icon name="icon-play" /> </el-icon>恢 复 </el-button> <el-button :disabled="deviceData.deviceTypeName === '枪机'" type="primary" class="btn" @mousedown="zoomIn('zoomin')" @mouseup="zoomIn('stop')"> <el-icon style="margin-right: 5px;"> <svg-icon name="icon-zoomin" /> </el-icon>放 大 </el-button> <el-button :disabled="deviceData.deviceTypeName === '枪机'" type="primary" class="btn" @mousedown="zoomIn('zoomout')" @mouseup="zoomIn('stop')"> <el-icon style="margin-right: 5px;"> <svg-icon name="icon-zoomout" /> </el-icon>缩 小 </el-button> <!-- <el-button type="primary" class="btn" @click="takePhoto"> <el-icon style="margin-right: 5px;"> <svg-icon name="icon-camera" /> </el-icon>抓 拍 </el-button> --> </div> <div style="width: calc(100% - 350px);height: 100%;display: flex;background-color: rgb(255 255 255 / 65%);border-radius: 10px;"> <el-tabs v-if="deviceData !== {}" tab-position="left" style="height: 100%;width: 100%;"> <el-tab-pane label="设备信息"> <el-descriptions style="flex: 1;" class="margin-top" :column="3" border> <el-descriptions-item label="设备名称"> {{ deviceData.monitorName }} </el-descriptions-item> <el-descriptions-item label="设备类型"> {{ deviceData.deviceTypeName }} </el-descriptions-item> <el-descriptions-item label="所属单位"> {{ deviceData.deptName }} </el-descriptions-item> <el-descriptions-item label="所属区域"> {{ deviceData.areaName }} </el-descriptions-item> <el-descriptions-item label="经度"> {{ deviceData.longitude }} </el-descriptions-item> <el-descriptions-item label="纬度"> {{ deviceData.latitude }} </el-descriptions-item> <el-descriptions-item label="位置"> {{ deviceData.location }} </el-descriptions-item> <el-descriptions-item label="备注"> {{ deviceData.description }} </el-descriptions-item> </el-descriptions> </el-tab-pane> </el-tabs> </div> </div> </div> </template> <style lang="scss" scoped> .video-wrap { display: flex; flex-wrap: wrap; } .videoControl { height: calc(100% - 165px); // width: calc(100% - 390px); width: calc(100% - 20px); margin-left: 10px; margin-top: 10px; position: absolute; background-color: #111; } .right { height: calc(100% - 165px); width: 360px; background-color: white; border-radius: 10px; padding: 10px; position: absolute; top: 10px; right: 10px; } .bottom { width: calc(100% - 10px); height: 140px; position: absolute; left: 0; bottom: 5px; display: flex; border-radius: 10px; margin: 5px; box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%); overflow: hidden; padding: 5px; justify-content: space-between; } .control-title { color: white; background-color: #ff9f11; border-bottom-right-radius: 10px; border-top-right-radius: 10px; line-height: 30px; font-weight: bold; text-align: center; letter-spacing: 1px; font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif; writing-mode: vertical-rl; text-orientation: upright; width: 30px; height: 100px; } .btn { margin: 3px auto; width: 100px; } .round-btn { position: absolute; top: 14px; left: 129px; width: 42px; height: 42px; z-index: 111111; border-radius: 21px; cursor: pointer; } .round-btn:hover { background-color: rgb(61 125 254 / 53%); } </style>