import * as THREE from "../../../libs/three.js/build/three.module.js"; //import "../../../libs/other/hls.js"; import BasicMaterial from '../materials/BasicMaterial.js' import {TextSprite} from "./TextSprite.js"; //import DepthBasicMaterial from "../materials/DepthBasicMaterial.js"; import math from '@sdk/utils/math' /* import SecurityCamera from '../core/cameras/SecurityCamera' import SecurityControls from '../core/controls/SecurityControls' */ import {transitions, easing, lerp} from '../utils/transitions.js' import CursorDeal from '../utils/CursorDeal.js' import FlvVideoPlayerBase from '../utils/media/FlvVideoPlayerBase' import H5VideoPlayerBase from '../utils/media/H5VideoPlayerBase' const planeGeometry = new THREE.PlaneGeometry(1,1) const testUrl = 'https://newcntv.qcloudcdn.com/asp/hls/4000/0303000a/3/default/d907ef756138493e9fe5b35b4ab69642/4000.m3u8' const videoPlayer = Potree.browser.nonsupportH5Video ? new FlvVideoPlayerBase() : new H5VideoPlayerBase() const normalColor = new THREE.Color(1, 1, 1) const hoverColor = new THREE.Color(2, 2, 2) const vec1 = new THREE.Vector3(), vec2 = new THREE.Vector3(), vec3 = new THREE.Vector3(), quat1 = new THREE.Quaternion(), quat2 = new THREE.Quaternion() let cameraModel, loadingCamModel //测试场景: http://192.168.0.25/mix3d/#/home/671 潘少 Aa123456 http://192.168.0.25/epg.html?m=YZL-jm-lrZcWxzPaPJ#/tag //接收4dkk的监控 暂不支持修改 可以直接加到模型上,注意所附模型旋转90度后角度才对。 export default class Monitor extends THREE.Object3D{ constructor(data, model){ super() this.isMonitor = true this.external = external//全景时不换材质 warnHls() data.video = testUrl this.data = data /* for(let i in data){ this[i] = data[i] } */ this.fov = data.data.fov this.cylinderNear = data.data.near || 0.03 this.cylinderFar = data.data.far || 3 model.add(this) let videoUrl = this.getVideoSrc() this.video = videoPlayer.getVideo(videoUrl ,this) if (data.video) { //console.error('createVideo', this.videoSrc, this.sid) /* this.video.onloadedmetadata = () => { this.video.canPlayed = true this.video.masters.forEach(v => { //一个video可以对应多个camera,因为它们链接一样 v.dispatchEvent({ type: 'loadedmetadata' }) }) } */ let canPlayed = e => { //this.player.$app.Camera.emit('SecurityCamera.videoActive', this.sid) this.videoActive = true //this.updateInfo(true) } if (this.video.canPlayed) { canPlayed() } else { this.addEventListener('loadedmetadata', canPlayed) } if (Hls.isSupported() && data.urlType == 1) { //似乎Hls不支持就无法播放 let hls = new Hls() hls.loadSource(videoUrl) hls.attachMedia(this.video) hls.on(Hls.Events.ERROR, (event, data) =>{ console.log('HLS加载失败', data.name, event, data)}) this.hlsVideoPlayer = hls } this.play() // ios需要 this.pause() } this.obj3d = new THREE.Group() this.add(this.obj3d) // 投射体材质 this.normalMat = new BasicMaterial({ color: new THREE.Color(0x00c8af), transparent: true, opacity: 0.1, side: THREE.DoubleSide, depthTest: false, visible: !!data.showScope }) // 监控视频材质 this.videoMat = new BasicMaterial({ map: new THREE.VideoTexture(this.video), side: THREE.DoubleSide, depthTest: false, transparent: true, }) // 摄像头 if (!cameraModel) { if(!loadingCamModel){ loadingCamModel = true viewer.loadModel({fileType:'glb', url:`${Potree.resourcePath}/models/glb/monitor.glb`},(model)=>{ cameraModel = model.children[0].children[0] cameraModel.geometry.translate(30, 50, -10) cameraModel.quaternion.setFromEuler(new THREE.Euler(Math.PI / 2+0.24, Math.PI, 0)) cameraModel.name = 'cameraModel' console.log('load monitor glb', cameraModel.uuid) model.parent.remove(model) viewer.scene.monitors.forEach(e=>e.modelLoaded()) loadingCamModel = false }) } }else{ this.modelLoaded() } this.updateAspect() if(data.showTitle){ let group = new THREE.Shim.FollowRootObject(this) //透明有问题,只有放到overlayScene里渲染了 (大小好像还是跟随模型了 this.titleLabel = new TextSprite({ text: data.name, backgroundColor: { r: 255, g: 255, b: 255, a: 0 }, textColor: { r: 255, g: 255, b: 255, a: 1 }, textshadowColor: '#666', borderRadius: 2, fontsize: 34, renderOrder: 5, margin: { x: 12, y: 10 }, sizeInfo: { scale: 0.4, nearBound: 3 }, }) this.titleLabel.sprite.material.depthTest = this.titleLabel.sprite.material.depthWrite = true this.titleLabel.position.set(0, -0.2, 0.1) group.add(this.titleLabel) viewer.scene.overlayScene.add(group) group.name = 'monitorLabel' this.addEventListener('isVisible',()=>{ Potree.Utils.updateVisible(group,'follow',this.realVisible()) }) this.parent.addEventListener('isVisible',()=>{ Potree.Utils.updateVisible(group,'follow',this.realVisible()) }) } { this.posOri = new THREE.Vector3(parseFloat(data.data['posOri-x']), parseFloat(data.data['posOri-y']), parseFloat(data.data['posOri-z'])), this.posOffset = new THREE.Vector3(parseFloat(data.data['posOffset-x']), parseFloat(data.data['posOffset-y']), parseFloat(data.data['posOffset-z'])), // data.position && this.position.copy(data.position) this.position.copy(this.posOri).add(this.posOffset) this.position.copy(Potree.Utils.tran4dkkVecInModel(this.position, model)) // target的优先级大于rotation if (data.data.target) { this.target = Potree.Utils.tran4dkkVecInModel(data.data.target, model) this.lookAt(data.data.target) } else { data.data.rotation && this.quaternion.setFromEuler(data.data.rotation) this.target = new THREE.Vector3(0, 0, -1).applyQuaternion(this.quaternion).add(this.position) } this.roll = 0 data.data.pitch && (this.pitch = data.data.pitch) data.data.yaw && (this.yaw = data.data.yaw) data.data.roll && this.setRoll(this.data.data.roll) if(model.props.baseRotation?.x == 0){ //该模型已经矫正,无需旋转90度,但是场景的数据需要,导致monitor和模型不匹配,需要再旋转 this.quaternion.copy(Potree.math.convertQuaternion.YupToZup(this.quaternion)) } } //this.updateInfo(true) this.events = { setSize: (width, height) => { if (this.isWatching) { this.updateAspect() } }, update: ()=>{ if(!this.video.paused){ viewer.dispatchEvent('content_changed') } } } viewer.addEventListener('viewerResize', this.events.setSize ) viewer.addEventListener('update', this.events.update ) window.monitor = this Potree.Utils.setObjectLayers(this,'dontIntersect') Potree.Utils.setObjectLayers(this.cylinder.bottom,'monitor') this.cameraModel && Potree.Utils.setObjectLayers(this.cameraModel,'sceneObjects') } getVideoSrc(){ if(this.data.urlType === 2){ let a = this.parent.props.raw.surveillancePath.split('/'); //用户上传的文件 a.pop() return a.join('/') + '/' + this.data.fileName }else{ return this.data.playUrl } } modelLoaded(){ this.cameraModel = cameraModel.clone() this.cameraModel.material = cameraModel.material.clone() this.obj3d.add(this.cameraModel) this.cameraModel.addEventListener('mouseover',()=>{ CursorDeal.add('hoverMonitor') this.highlight(true) }) this.cameraModel.addEventListener('mouseleave',()=>{ CursorDeal.remove('hoverMonitor') this.highlight(false) }) this.cameraModel.addEventListener('click',()=>{ if(viewer.scene.monitors.some(e=>e.isWatching))return this.watch() }) } watch(){ if (!this.videoActive){ console.log('monitorError src:', this.video.src) return viewer.dispatchEvent('monitorError' ) } let camera = viewer.mainViewport.camera this.updateAspect() this.isWatching = true //this.target.set(0, 0, -1).applyQuaternion(this.quaternion).add(this.position) viewer.mainViewport.view.cancelFlying() this.oldState = { fov: camera.fov, position: camera.position.clone(), quaternion: camera.quaternion.clone() } this.data.showScope && this.showVideo(true) this.video.pause()//先显示出画面 setTimeout(()=>{ this.data.showScope || this.showVideo(true) viewer.mainViewport.cameraLayers = ['monitor'] //hide others viewer.scene.monitors.forEach(e=>Potree.Utils.updateVisible(e,'watch', e == this )) },800) viewer.mainViewport.view.setRotMode('free') viewer.mainViewport.view.setView({ position: this.getWorldPosition(new THREE.Vector3), quaternion: this.getWorldQuaternion(new THREE.Quaternion), onUpdate:(percent)=>{ camera.fov = this.fov * percent + this.oldState.fov * (1-percent) camera.updateProjectionMatrix() }, callback:()=>{ this.video.play() viewer.dispatchEvent({type:'watchMonitor',monitor:this}) }, duration:1500 }) viewer.renderArea.style['pointer-events'] = 'none'; } leave(){// 停止观看监控 let camera = viewer.mainViewport.camera this.isWatching = false this.cameraModel.material.color.copy(normalColor) viewer.mainViewport.view.cancelFlying() viewer.mainViewport.cameraLayers = null viewer.scene.monitors.forEach(e=>Potree.Utils.updateVisible(e,'watch', true )) this.data.showScope || this.showVideo(false) this.video.pause() viewer.mainViewport.view.setView({ position: this.oldState.position, quaternion: this.oldState.quaternion, onUpdate:(percent)=>{ camera.fov = this.fov * (1-percent) + this.oldState.fov * percent camera.updateProjectionMatrix() }, callback:()=>{ viewer.renderArea.style['pointer-events'] = ''; this.data.showScope && this.showVideo(false) viewer.mainViewport.view.setRotMode('standard') }, duration:1000 }) viewer.dispatchEvent({type:'exitWatchMonitor',monitor:this}) } // 显示监控视频或显示投射体 showVideo(isShow) { //console.warn('showVideo', this.info.sid, isShow ) if (!this.videoActive) return if (isShow) { /* if (browser.detectIOS()) { this.player.domElement.addEventListener('touchstart', this.events.onDomElementTouchStart, true) } */ this.play() this.normalMat.opacity = 0 Potree.Utils.updateVisible(this.cylinder, 'watch', true, 1, 'add') this.cylinder.bottom.material = this.videoMat //this.cylinder.bottom.renderOrder = RenderOrder.monitorPlane } else { this.pause() this.normalMat.opacity = 0.08 Potree.Utils.updateVisible(this.cylinder, 'watch', false, 1, 'cancel') this.cylinder.bottom.material = this.normalMat //this.cylinder.bottom.renderOrder = RenderOrder.visibleFloor } } play() { //console.log('play monitor', this.sid, this.videoSrc) this.shouldPlay = true if (Potree.browser.detectWeixin()) { //用微信平台的 WeixinJSBridge 越过 Autoplay Policy try { top.WeixinJSBridge && top.WeixinJSBridge.invoke( 'getNetworkType', {}, e => { this.video.play() }, false ) } catch (error) { this.video.play() } } else { this.video.play() if (this.video.paused) { console.log('播放不了') /* this.player.once('pointerStart', () => { this.play() }) */ } } } pause() { this.shouldPlay = false this.video.pause() } dispose(){ let index = viewer.scene.monitors.indexOf(this) if(index == -1)return viewer.scene.monitors.splice(index,1) viewer.removeEventListener("viewerResize", this.events.setSize ) viewer.removeEventListener("update", this.events.update ) transitions.cancelById('cameraHighlight_' + this.sid) /* this.tag.geometry.dispose() this.tag.material.dispose() this.cylinder.line.geometry.dispose() this.cylinder.line.material.dispose() this.cylinder.geometry.dispose() */ viewer.removeModel(this) this.normalMat.dispose() this.videoMat.dispose() this.titleLabel?.dispose() this.hlsVideoPlayer && this.hlsVideoPlayer.destroy() } highlight(state) {//相机hover高亮 if (this.hightlighted == state) return this.hightlighted = state transitions.cancelById('cameraHighlight_' + this.sid) transitions.start(lerp.color(this.cameraModel.material.color, state ? hoverColor : normalColor), 100, null, null, null, null, 'cameraHighlight_' + this.sid) } updateAspect() { //更新aspect且修改geometry let aspect = viewer.mainViewport.camera.aspect if (aspect != this.aspect) { this.aspect = aspect this.updateMesh() } } updateMesh() { let nearHeight, nearWidth, farHeight, farWidth nearHeight = Math.tan(THREE.MathUtils.degToRad(this.fov / 2)) * this.cylinderNear nearWidth = nearHeight * this.aspect //根据canvas比例调整视频面的比例,保持和canvas一致。 farHeight = Math.tan(THREE.MathUtils.degToRad(this.fov / 2)) * this.cylinderFar farWidth = farHeight * this.aspect let firstBuild = !this.cylinder if(this.data.showScope){ // 投射体(底面以外) let vertices = [], bottomVertices = [] vertices.push(-nearWidth, nearHeight, -this.cylinderNear) vertices.push(nearWidth, nearHeight, -this.cylinderNear) vertices.push(nearWidth, -nearHeight, -this.cylinderNear) vertices.push(-nearWidth, -nearHeight, -this.cylinderNear) bottomVertices.push(-farWidth, farHeight, -this.cylinderFar) bottomVertices.push(farWidth, farHeight, -this.cylinderFar) bottomVertices.push(farWidth, -farHeight, -this.cylinderFar) bottomVertices.push(-farWidth, -farHeight, -this.cylinderFar) vertices = vertices.concat(bottomVertices) if (firstBuild) { let uvs = [] uvs.push(0, 1, 1, 1, 1, 0, 0, 0) uvs.push(0, 1, 1, 1, 1, 0, 0, 0) let indexs = [] indexs.push(0, 1, 3, 2, 3, 1) indexs.push(0, 1, 4, 5, 4, 1) indexs.push(1, 2, 5, 6, 5, 2) indexs.push(2, 3, 6, 7, 6, 3) indexs.push(3, 0, 7, 4, 7, 0) // indexs.push(4, 7, 5, 6, 5, 7) let cylinderGeo = new THREE.BufferGeometry() cylinderGeo.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2)) cylinderGeo.setIndex(new THREE.BufferAttribute(new Uint16Array(indexs), 1)) this.cylinder = new THREE.Mesh(cylinderGeo, this.normalMat) //if (this.control.player.$app.config.mobile) this.cylinder.rotateZ(-Math.PI / 2) //用户自己旋转屏幕吧 this.obj3d.add(this.cylinder) } this.cylinder.geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3)) }else if(!this.cylinder){ this.cylinder = new THREE.Group() this.obj3d.add(this.cylinder) } // 投射体底面 (视频) const bottomGeo = new THREE.PlaneGeometry(farWidth * 2, farHeight * 2) if (firstBuild) { this.cylinder.bottom = new THREE.Mesh(bottomGeo, this.normalMat) this.cylinder.add(this.cylinder.bottom) } else { this.cylinder.bottom.geometry.dispose() this.cylinder.bottom.geometry = bottomGeo } this.cylinder.bottom.position.set(0, 0, this.cylinderNear - this.cylinderFar) if(this.data.showScope){ // 投射体线框 if (firstBuild) { this.cylinder.line = new THREE.LineSegments(new THREE.EdgesGeometry(this.cylinder.geometry), new THREE.LineBasicMaterial({ color: 0xffffff, opacity: 0.6, transparent: true })) this.cylinder.add(this.cylinder.line) } else { this.cylinder.line.geometry.dispose() this.cylinder.line.geometry = new THREE.EdgesGeometry(this.cylinder.geometry) } } } set pitch(pitch) { pitch = Math.min(Math.max(pitch, -89.9), 89.9) // 防止万向锁 let yaw = this.yaw < 0 ? this.yaw + 360 : this.yaw let upDir = vec1.set(0, 1, 0) let rightDir = vec2.set(1, 0, 0) //.applyQuaternion(this.quaternion) let yawQuat = quat1.setFromAxisAngle(upDir, THREE.MathUtils.degToRad(yaw)) let pitchQuat = quat2.setFromAxisAngle(rightDir, THREE.MathUtils.degToRad(pitch)) this.quaternion.multiplyQuaternions(yawQuat, pitchQuat) //this.updateTarget() } set yaw(yaw) { let pitch = this.pitch // 要先计算pitch,防止窜数据 let upDir = vec1.set(0, 1, 0) let rightDir = vec2.set(1, 0, 0) let yawQuat = quat1.setFromAxisAngle(upDir, THREE.MathUtils.degToRad(yaw)) let pitchQuat = quat2.setFromAxisAngle(rightDir, THREE.MathUtils.degToRad(pitch)) this.quaternion.multiplyQuaternions(yawQuat, pitchQuat) //this.updateTarget() } // 横滚角 setRoll(angle) {//因加在子物体上,非0时播放视频是歪的(4dkk也这样) this.roll = angle % 360 this.obj3d.quaternion.setFromAxisAngle(vec1.set(0, 0, -1) , THREE.MathUtils.degToRad(angle)) } get pitch() {// 上下转角 return this.data.data.pitch //xzw改 let lookAt = vec1.copy(this.cylinder.bottom.position).applyQuaternion(this.quaternion) let projectVec = vec2.copy(lookAt).projectOnPlane(vec3.set(0, 1, 0)) let pitch = THREE.MathUtils.radToDeg(lookAt.angleTo(projectVec) * Math.sign(lookAt.y)) % 180 if (pitch > 90) pitch = 90 - pitch return pitch } get yaw() {// 左右转角 return this.data.data.yaw //xzw改 原先的计算不对,0变180 let lookAt = vec1.copy(this.cylinder.bottom.position).applyQuaternion(this.quaternion) let lookAtXZ = lookAt.setY(0) let frontDir = vec2.set(0, 0, 1) let frontCross = vec3.set(1, 0, 0) let angle = (THREE.MathUtils.radToDeg(lookAtXZ.angleTo(frontDir) * Math.sign(lookAtXZ.dot(frontCross))) + 180) % 360 if (angle > 180) angle = angle - 360 return angle } } function warnHls(){ if (!window.Hls) { console.error('没加载Hls.js文件!') } if (!window.Hls?.isSupported()) { //在融合页面导入hls文件 console.error('Hls is not Supported, 部分监控视频可能不支持') //iphoneX不支持 小米Civi 1S支持。 } } /* viewer.addMonitor(undefined, viewer.objs.children[0]) viewer.scene.monitors[0].leave() */