<!--
|
* @Author : yuan
|
* @Date : 2025-08-05 10:02:04
|
* @LastEditors : yuan
|
* @LastEditTime : 2025-08-07 10:40:16
|
* @FilePath : \video.html
|
* @Description :
|
* Copyright 2025 OBKoro1, All Rights Reserved.
|
* 2025-08-05 10:02:04
|
-->
|
<!DOCTYPE html>
|
<html lang="en">
|
|
<head>
|
<meta charset="utf-8">
|
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
|
<meta name="viewport"
|
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
|
<title></title>
|
<script src="./Build/Cesium.js"></script>
|
<script src="./ZLMRTCClient.js"></script>
|
|
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
|
|
<link href="https://cdn.bootcdn.net/ajax/libs/video.js/5.15.0/video-js.css" rel="stylesheet">
|
<script src="https://cdn.bootcdn.net/ajax/libs/video.js/5.15.0/video.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-hls/5.15.0/videojs-contrib-hls.min.js"
|
type="text/javascript"></script>
|
|
<style>
|
@import url(./Build/Widgets/widgets.css);
|
|
html,
|
body,
|
#cesiumContainer {
|
width: 100%;
|
height: 100%;
|
margin: 0;
|
padding: 0;
|
overflow: hidden;
|
}
|
|
.box-container {
|
position: fixed;
|
top: 0;
|
left: 0;
|
z-index: 99;
|
text-align: center;
|
background: yellowgreen;
|
}
|
|
.box-container>div {
|
display: flex;
|
flex-wrap: wrap;
|
}
|
|
.box-container>div>div {
|
width: 96px;
|
height: 36px;
|
line-height: 36px;
|
}
|
|
#VIDEO {
|
position: fixed;
|
left: 10px;
|
bottom: 10px;
|
width: 160px;
|
height: 90px;
|
z-index: 99;
|
}
|
</style>
|
|
|
</head>
|
|
<body>
|
<div id="cesiumContainer">
|
|
|
</div>
|
|
<div class="box-container">
|
<div>
|
<div>heading</div>
|
<div>
|
<button onclick="optionsChange('heading', 1, 1)">+1</button>
|
</div>
|
<div>
|
<button onclick="optionsChange('heading', 2, 1)">-1</button>
|
</div>
|
</div>
|
<div>
|
<div>pitch</div>
|
<div>
|
<button onclick="optionsChange('pitch', 1, 1)">+1</button>
|
</div>
|
<div>
|
<button onclick="optionsChange('pitch', 2, 1)">-1</button>
|
</div>
|
</div>
|
<div>
|
<div>roll</div>
|
<div>
|
<button onclick="optionsChange('roll', 1, 1)">+1</button>
|
</div>
|
<div>
|
<button onclick="optionsChange('roll', 2, 1)">-1</button>
|
</div>
|
</div>
|
|
|
<div>
|
<div>heading</div>
|
<div>
|
<button onclick="optionsChange('videoHeading', 1, 1)">+1</button>
|
</div>
|
<div>
|
<button onclick="optionsChange('videoHeading', 2, 1)">-1</button>
|
</div>
|
</div>
|
<div>
|
<div>pitch</div>
|
<div>
|
<button onclick="optionsChange('videoPitch', 1, 1)">+1</button>
|
</div>
|
<div>
|
<button onclick="optionsChange('videoPitch', 2, 1)">-1</button>
|
</div>
|
</div>
|
<div>
|
<div>roll</div>
|
<div>
|
<button onclick="optionsChange('videoRoll', 1, 1)">+1</button>
|
</div>
|
<div>
|
<button onclick="optionsChange('videoRoll', 2, 1)">-1</button>
|
</div>
|
</div>
|
</div>
|
|
<video src="./5b49f586a6ad3.mp4" id="VIDEO" class="video-player" controls autoplay muted playsinline>
|
您的浏览器太旧,不支持HTML5视频,请升级浏览器。
|
</video>
|
|
<script>
|
let ele = document.getElementById('VIDEO')
|
|
let webrtcPlayer = new window.ZLMRTCClient.Endpoint({
|
element: ele, // video 标签
|
debug: true, // 是否打印日志
|
zlmsdpUrl: 'https://wrj.shuixiongit.com/index/api/webrtc?app=live&stream=1581F8HGX252L00A01CG-99-0-0-normal-0&type=play', //流地址
|
simulecast: false,
|
useCamera: false,
|
audioEnable: true,
|
videoEnable: true,
|
recvOnly: true,
|
usedatachannel: false,
|
})
|
|
console.log(webrtcPlayer, 111111)
|
|
webrtcPlayer.on(window.ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, e => {
|
console.log(e, 'WEBRTC_ICE_CANDIDATE_ERROR')
|
})
|
webrtcPlayer.on(window.ZLMRTCClient.Events.WEBRTC_ON_REMOTE_STREAMS, e => {
|
console.log(e, 'WEBRTC_ON_REMOTE_STREAMS')
|
})
|
webrtcPlayer.on(window.ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, e => {
|
console.log(e, 'WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED')
|
})
|
webrtcPlayer.on(window.ZLMRTCClient.Events.WEBRTC_ON_LOCAL_STREAM, s => {
|
console.log(e, 'WEBRTC_ON_LOCAL_STREAM')
|
})
|
</script>
|
|
<script>
|
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzNjYzOTI5NC0yM2QyLTQyOTgtYWM5OS1lM2MwNTYwMGEzMjciLCJpZCI6ODQ1MjYsImlhdCI6MTY0NjM1ODM5OX0.BzsVR7Lt9RhsCia-R7E64KunaAME0HGD7Sv2-xF-RIQ'
|
|
var viewer = new Cesium.Viewer('cesiumContainer', {
|
|
|
imageryProvider: new Cesium.ArcGisMapServerImageryProvider({
|
url: "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer",
|
maximumLevel: 18,
|
}),
|
|
// terrainProvider: Cesium.createWorldTerrain({
|
// requestVertexNormals: true,
|
// requestWaterMask: true
|
// }),
|
shouldAnimate: true,
|
selectionIndicator: false,
|
infoBox: false,
|
geocoder: false, // 位置查找工具
|
baseLayerPicker: false,// 图层选择器(地形影像服务)
|
timeline: false, // 底部时间线
|
homeButton: false,// 视角返回初始位置
|
fullscreenButton: false, // 全屏
|
animation: false, // 左下角仪表盘(动画器件)
|
sceneModePicker: false,// 选择视角的模式(球体、平铺、斜视平铺)
|
navigationHelpButton: false, //导航帮助按钮
|
})
|
//viewer.scene.debugShowFramesPerSecond = true;
|
viewer.cesiumWidget.creditContainer.style.display = "none"
|
var towerJson
|
|
// viewer.flyTo(tileset);
|
var position = Cesium.Cartesian3.fromDegrees(114.82299452552775, 25.992290470862866, 1500)//定义飞行终点的坐标
|
|
viewer.camera.flyTo({
|
// 设置相机前往的位置
|
destination: position,
|
// 相机的朝向
|
orientation: {
|
// 如果围绕y轴旋转,偏航角
|
heading: Cesium.Math.toRadians(0),
|
// 如果围绕x轴旋转,俯仰角
|
pitch: Cesium.Math.toRadians(-90),
|
// 如果围绕z轴旋转,翻滚角
|
roll: 0.0,
|
},
|
duration: 1,
|
complete: function () {
|
// 在相机移动完成后执行
|
// console.log("已抵达北京天安门");
|
},
|
})
|
|
viewer.scene.globe.depthTestAgainstTerrain = true
|
|
const imageryProvider_stand = new Cesium.UrlTemplateImageryProvider({
|
url: `https://t{s}.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=ec574ab1eecd3b998ca8c92f91d3242e`,
|
subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
|
// format: 'image/jpeg',
|
// show: true,
|
maximumLevel: 18,
|
credit: 'stand_tc',
|
})
|
|
viewer?.imageryLayers.addImageryProvider(imageryProvider_stand)
|
|
async function loadTerrain () {
|
let worldTerrain
|
try {
|
worldTerrain = await Cesium.createWorldTerrainAsync()
|
viewer.scene.terrainProvider = worldTerrain
|
} catch (error) {
|
console.error('地形加载失败:', error)
|
}
|
}
|
|
loadTerrain()
|
|
class CreateFrustum {
|
constructor(viewer, options) {
|
this.position = options.position
|
this.viewer = viewer
|
this.fov = options.fov || 0
|
this.near = options.near || 0
|
this.far = options.far || 0
|
this.heading = options.heading || 0
|
this.pitch = options.pitch || 0
|
this.roll = options.roll || 0
|
this.width = options.width
|
this.height = options.height
|
|
this.videoScreen = null
|
this.add()
|
}
|
|
// 创建视锥体和轮廓线
|
add () {
|
this.clear()
|
this.viewer.scene.globe.depthTestAgainstTerrain = true
|
|
this.addFrustum()
|
this.addOutline()
|
this.addCenterline()
|
}
|
|
// 清除视锥体和轮廓线
|
clear () {
|
this.viewer.scene.globe.depthTestAgainstTerrain = false
|
|
if (this.videoScreen) {
|
this.videoScreen.destroyed()
|
|
this.videoScreen = null
|
}
|
|
this.clearFrustum()
|
this.clearOutline()
|
this.clearCenterline()
|
}
|
|
// 清除视锥体
|
clearFrustum () {
|
if (this.frustumPrimitive) {
|
this.viewer.scene.primitives.remove(this.frustumPrimitive)
|
this.frustumPrimitive = null
|
}
|
}
|
|
// 清除轮廓线
|
clearOutline () {
|
if (this.outlinePrimitive) {
|
this.viewer.scene.primitives.remove(this.outlinePrimitive)
|
this.outlinePrimitive = null
|
}
|
}
|
|
clearCenterline () {
|
if (this.centerDataSource) {
|
this.centerDataSource.entities.removeAll()
|
this.viewer.dataSources.remove(this.centerDataSource)
|
this.centerDataSource = null
|
}
|
}
|
|
// 创建视锥体
|
addFrustum () {
|
const frustum = new Cesium.PerspectiveFrustum({
|
// 查看的视场角,绕Z轴旋转,以弧度方式输入
|
fov: Cesium.Math.toRadians(this.fov),
|
// 视锥体的宽度/高度
|
aspectRatio: (this.width / this.height),
|
// 近面距视点的距离
|
near: this.near,
|
// 远面距视点的距离
|
far: this.far,
|
})
|
const position = Cesium.Cartesian3.fromDegrees(
|
this.position.longitude,
|
this.position.latitude,
|
this.position.altitude
|
)
|
const frustumGeometry = new Cesium.FrustumGeometry({
|
frustum: frustum,
|
origin: position,
|
orientation: Cesium.Transforms.headingPitchRollQuaternion(
|
position,
|
new Cesium.HeadingPitchRoll.fromDegrees(this.heading, this.pitch, this.roll)
|
),
|
vertexFormat: Cesium.VertexFormat.POSITION_ONLY,
|
})
|
|
const instance = new Cesium.GeometryInstance({
|
geometry: frustumGeometry,
|
attributes: {
|
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
Cesium.Color.fromBytes(0, 213, 144, 20)
|
),
|
},
|
})
|
const primitive = new Cesium.Primitive({
|
geometryInstances: instance,
|
releaseGeometryInstances: false,
|
appearance: new Cesium.PerInstanceColorAppearance({
|
closed: true,
|
flat: true,
|
}),
|
asynchronous: false,
|
})
|
|
this.frustumPrimitive = this.viewer.scene.primitives.add(primitive)
|
}
|
|
// 创建轮廓线
|
addOutline () {
|
const frustum = new Cesium.PerspectiveFrustum({
|
// 查看的视场角度,绕Z轴旋转,以弧度方式输入
|
// The angle of the field of view (FOV), in radians.
|
// This angle will be used as the horizontal FOV if the width is greater than the height, otherwise it will be the vertical FOV.
|
fov: Cesium.Math.toRadians(this.fov),
|
// 视锥体的宽度/高度
|
aspectRatio: (this.width / this.height),
|
// 近面距视点的距离
|
near: this.near,
|
// 远面距视点的距离
|
far: this.far,
|
})
|
|
const position = Cesium.Cartesian3.fromDegrees(
|
this.position.longitude,
|
this.position.latitude,
|
this.position.altitude
|
)
|
|
|
let hpr = Cesium.HeadingPitchRoll.fromDegrees(this.heading, this.pitch, this.roll)
|
let orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr)
|
|
const frustumGeometry = new Cesium.FrustumOutlineGeometry({
|
frustum: frustum,
|
origin: position,
|
orientation: orientation,
|
vertexFormat: Cesium.VertexFormat.POSITION_ONLY,
|
})
|
|
const instance = new Cesium.GeometryInstance({
|
geometry: frustumGeometry,
|
attributes: {
|
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
Cesium.Color.fromBytes(0, 213, 144, 255)
|
),
|
},
|
})
|
const primitive = new Cesium.Primitive({
|
geometryInstances: instance,
|
appearance: new Cesium.PerInstanceColorAppearance({
|
closed: true,
|
flat: true,
|
translucent: true
|
}),
|
asynchronous: false,
|
})
|
this.outlinePrimitive = this.viewer.scene.primitives.add(primitive)
|
|
if (!this.videoScreen) {
|
this.initVideoMesh({
|
orientation: orientation,
|
cameraPosition: position,
|
hpr: hpr
|
})
|
}
|
}
|
|
addCenterline () {
|
this.centerDataSource = new Cesium.CustomDataSource()
|
this.viewer.dataSources.add(this.centerDataSource)
|
|
const position = Cesium.Cartesian3.fromDegrees(
|
this.position.longitude,
|
this.position.latitude,
|
this.position.altitude
|
)
|
|
let hpr = Cesium.HeadingPitchRoll.fromDegrees(this.heading, this.pitch, this.roll)
|
|
let matrix = Cesium.Matrix3.fromQuaternion(
|
Cesium.Transforms.headingPitchRollQuaternion(
|
position,
|
hpr
|
),
|
new Cesium.Matrix3()
|
)
|
|
let right1 = Cesium.Matrix3.getColumn(matrix, 0, new Cesium.Cartesian3())
|
let up1 = Cesium.Matrix3.getColumn(matrix, 1, new Cesium.Cartesian3())
|
let direction = Cesium.Matrix3.getColumn(matrix, 2, new Cesium.Cartesian3())
|
|
// const direction = new Cartesian3(-0.10668226241650887, 0.8103322335050215, -0.5761775474872814)
|
|
let frontDirect = Cesium.Cartesian3.multiplyByScalar(
|
direction,
|
this.far,
|
new Cesium.Cartesian3()
|
)
|
let bottomCenter = Cesium.Cartesian3.add(position, frontDirect, new Cesium.Cartesian3())
|
|
// 创建Entity来显示中心线
|
this.centerDataSource.entities.add({
|
name: 'Center Line of Frustum',
|
polyline: {
|
positions: [position, bottomCenter],
|
width: 1,
|
// material: Cesium.Color.RED.withAlpha(0.5)
|
material: new Cesium.PolylineDashMaterialProperty({
|
color: Cesium.Color.fromBytes(0, 213, 144, 255),
|
dashLength: 30.0,
|
gapColor: Cesium.Color.TRANSPARENT,
|
gapWidth: 1.0,
|
}),
|
},
|
})
|
}
|
|
initVideoMesh (data) {
|
let element = document.getElementById('VIDEO')
|
|
if (element === '') return
|
|
this.videoScreen = new VideoScreen({
|
viewer: this.viewer,
|
options: {
|
element,
|
...data,
|
stRotation: 90 // 视频画面的旋转角度
|
}
|
})
|
}
|
}
|
|
function computeFarCenter (position, hpr, far) {
|
const quaternion = Cesium.Transforms.headingPitchRollQuaternion(position, hpr)
|
const rotMat = Cesium.Matrix3.fromQuaternion(quaternion)
|
const dirLocal = new Cesium.Cartesian3(0, 0, far)
|
const dirWorld = Cesium.Matrix3.multiplyByVector(rotMat, dirLocal, new Cesium.Cartesian3())
|
return Cesium.Cartesian3.add(position, dirWorld, new Cesium.Cartesian3())
|
}
|
|
/**
|
* 视频幕布
|
*/
|
class VideoScreen {
|
// 投射多边形
|
curMesh = null
|
|
/**
|
* @typedef {Object} Options 视频幕布配置参数
|
* @property {String} src 视频源地址
|
* @property {Array} [positions=[]] 视频幕布几何体顶点坐标
|
* @property {number} [stRotation=90.0] 几何体纹理的旋转角度
|
* @property {Boolean} [tapGround=false] 几何体是否完全贴地
|
*/
|
options = {
|
src: '',
|
positions: [],
|
stRotation: 0,
|
tapGround: false
|
}
|
|
constructor(config) {
|
const { viewer, options } = config
|
|
if (!viewer || !options) {
|
console.error('Missing required parameters when new FrustumEditor')
|
return
|
}
|
this._viewer = viewer
|
|
this._entityVideoDataSource = null
|
this._entityVideoDataSource = new Cesium.CustomDataSource('entityVideoDataSource')
|
this._viewer.dataSources.add(this._entityVideoDataSource)
|
|
this.options = {
|
...this.options,
|
...options
|
}
|
|
this.init()
|
}
|
|
init () {
|
const {
|
element,
|
tapGround,
|
positions,
|
stRotation,
|
orientation,
|
cameraPosition,
|
hpr
|
} = this.options
|
|
let width = option.far * Math.tan(Cesium.Math.toRadians(option.fov) * 0.5) * 2
|
let height = width / (option.videoWidth / option.videoHeight)
|
|
const mesh = this._entityVideoDataSource.entities.add({
|
position: new Cesium.CallbackProperty(() => {
|
return computeFarCenter(cameraPosition, hpr, option.far)
|
}, false),
|
orientation: new Cesium.CallbackProperty(() => {
|
return orientation
|
}, false),
|
plane: {
|
plane: new Cesium.Plane(new Cesium.Cartesian3(0, 0, -1), 0),
|
dimensions: new Cesium.Cartesian2(width, height),
|
material: element,
|
},
|
})
|
|
this.curMesh = mesh
|
}
|
|
destroyed () {
|
if (this._entityVideoDataSource) {
|
this._entityVideoDataSource?.entities.removeAll()
|
this._viewer.dataSources.remove(this._entityVideoDataSource, true)
|
this._entityVideoDataSource = null
|
}
|
|
this._viewer = null
|
}
|
}
|
|
let option = {
|
longitude: 114.82299452552775,
|
latitude: 25.992290470862866,
|
height: 250,
|
|
pitch: 0,
|
roll: 0,
|
heading: 90,
|
|
fov: 64,
|
far: 250,
|
|
videoWidth: 960,
|
videoHeight: 720,
|
|
videoHeading: -360,
|
videoPitch: 0,
|
videoRoll: 0,
|
}
|
|
let viewInfoFrustum = new CreateFrustum(viewer, {
|
position: {
|
longitude: option?.longitude,
|
latitude: option?.latitude,
|
altitude: option?.height,
|
},
|
|
width: option?.videoWidth,
|
height: option?.videoHeight,
|
|
fov: option.fov,
|
near: 0.01,
|
far: option.far,
|
|
roll: option.roll,
|
pitch: option.pitch,
|
heading: option.heading,
|
})
|
|
function optionsChange (type, changeType, val) {
|
viewInfoFrustum.clear()
|
|
changeType === 1 ? option[type] += 1 : option[type] -= 1
|
|
viewInfoFrustum = new CreateFrustum(viewer, {
|
position: {
|
longitude: option?.longitude,
|
latitude: option?.latitude,
|
altitude: option?.height,
|
},
|
|
width: option?.videoWidth,
|
height: option?.videoHeight,
|
|
fov: 64,
|
near: 0.01,
|
far: 250,
|
roll: option.roll,
|
pitch: option.pitch,
|
heading: option.heading,
|
})
|
}
|
</script>
|
</body>
|
|
</html>
|