前言
部分项目需求需使用第三方 RTSP 流和 WebSocket 地址进行视频投放。若直接使用示例代码更改视频流地址及 WebSocket 地址,往往无法加载出对应的流视频。
原因分析
以大华设备为例,主要原因是大华的 WebSocket 使用了私有子协议,而平台默认解析依赖基于明确的 WebSocket Streaming Protocol(公开 INIT/JOIN/WRAP 命令的子协议)。因此,标准解析器无法正确识别大华的 WebSocket 地址。
解决方案
大华官方提供了专用的依赖库用于解析其 WebSocket 地址。开发者需自行获取该依赖文件并替换原有解析逻辑。
实现步骤
1. 替换依赖文件
将代码中解析公开 WebSocket 子协议的 streamedian.js 替换为大华提供的 WSPlayer.js 和 PlaySDKInterface.js 文件。
引入资源代码如下:
<link href="../../Build/SuperMap3D/Widgets/widgets.css" rel="stylesheet">
<link href="./css/pretty.css" rel="stylesheet">
<link rel="stylesheet" href="./lib/WSPlayer/player.css">
<link rel="stylesheet" href="./lib/WSPlayer/window.division.css">
<script src="./js/echarts.min.js"></script>
<script src="js/polyfill.js"></script>
<script src="./js/jquery.min.js"></script>
<script src="./js/slider.js"></script>
<script type="text/javascript" src="../../Build/SuperMap3D/SuperMap3D.js"></script>
<script src="./js/config.js"></script>
<script type="text/javascript" src="./lib/WSPlayer/PlaySDKInterface.js"></script>
<script type="text/javascript" src="./lib/WSPlayer/WSPlayer.js"></script>
2. 修改 WebSocket 解析及视频播放逻辑
核心逻辑在于初始化 PlayerManager 并适配大华的私有协议接口。
<body>
<div id="Container"></div>
<div id="toolbar" class="param-container tool-bar">
<button type="button" id="active" class="button black">重新投放</button>
<button type="button" id="clear" class="button black">清除投放</button>
<br />
<label><input type="checkbox" id="visibleLine" checked> 显示投放参考线</label>
<br><br>
<div style="margin: 8px 0;"><b>宽度 (°)</b><br><input type="number" id="horizontal" min="1" step="0.1" value="20"></div>
<div style="margin: 8px 0;"><b>高度 (°)</b><br><input type="number" id="vertical" min="1" step="0.1" value="10"></div>
<div style="margin: 8px 0;"><b>投影距离 (m)</b><br><input type="number" id="distance" min="10" step="10" value="350"></div>
<div style="margin-top:15px;"><small>WS URL:</small><br><input id="real-ws" value="ws://ip:40088" style="width:220px;"></div>
<div style="margin-top:8px;"><small>RTSP:</small><br><input id="real-rtsp" value="rtsp://ip:40088/dss/monitor/param/cameraid=1003919%240%26substream=1" style="width:220px;"></div>
</div>
<div style="position: absolute; right: 20px; top: 120px;">
<div id="msChart" style="width: 300px; height: 150px; background:rgba(0,0,0,0.4);"></div>
</div>
<div style="position: absolute; right: 20px; top: 320px;">
<div id="fpsChart" style="width: 300px; height: 150px; background:rgba(0,0,0,0.4);"></div>
</div>
<div id="ws-real-player" style="width:720px; height:480px; visibility:hidden; position:absolute; top:-9999px; left:-9999px;"></div>
<script type="module">
import PlayerManager from './icc/PlayerManager.js';
function onload(SuperMap3D) {
const EngineType = getEngineType();
const viewer = new SuperMap3D.Viewer('Container', { contextOptions: { contextType: Number(EngineType) } });
viewer.scenePromise.then(scene => init(SuperMap3D, scene, viewer));
}
function init(SuperMap3D, scene, viewer) {
viewer.resolutionScale = window.devicePixelRatio;
addBaseMap(viewer);
// 性能监控
const msChart = echarts.init(document.getElementById('msChart'));
const fpsChart = echarts.init(document.getElementById('fpsChart'));
let msData = [], fpsData = [], step = 0;
scene.debugShowFramesPerSecond = true;
scene.shadowMap.darkness = 1.275;
scene.hdrEnabled = false;
scene.sun.show = true;
setInterval(() => {
if (step <= 6 && scene.debugShowFramesPerSecond) {
const msEl = document.querySelector(".supermap3d-performanceDisplay-ms");
const fpsEl = document.querySelector(".supermap3d-performanceDisplay-fps");
if (msEl && fpsEl) {
msData[step] = parseFloat(msEl.innerHTML.replace(/[^0-9.]/g, ''));
fpsData[step] = parseFloat(fpsEl.innerHTML.replace(/[^0-9.]/g, ''));
step++;
}
} else {
step = 0;
}
}, 200);
setInterval(() => {
msChart.setOption({ title: { subtext: 'MS', left: 'right', subtextStyle: { color: '#eee' } }, xAxis: { type: 'category', data: Array(7).fill(''), axisLabel: { color: '#ccc' } }, yAxis: { type: 'value', axisLabel: { color: '#ccc' } }, series: [{ data: msData, type: 'line' }], grid: { left: '5%', right: '5%', top: '25%', bottom: '5%', containLabel: true } });
fpsChart.setOption({ title: { subtext: 'FPS', left: 'right', subtextStyle: { color: '#eee' } }, xAxis: { type: 'category', data: Array(7).fill(''), axisLabel: { color: '#ccc' } }, yAxis: { type: 'value', axisLabel: { color: '#ccc' } }, series: [{ data: fpsData, type: 'line', areaStyle: {} }], grid: { left: '5%', right: '5%', top: '25%', bottom: '5%', containLabel: true } });
}, 1000);
if (!scene.pickPositionSupported) alert('当前浏览器/设备不支持深度拾取,投影功能受限');
// 加载模型
SuperMap3D.when.all([
scene.addS3MTilesLayerByScp(URL_CONFIG.SCP_CBD_BUILD, { name: 'build' }),
scene.addS3MTilesLayerByScp(URL_CONFIG.SCP_CBD_GROUND1, { name: 'ground1' }),
scene.addS3MTilesLayerByScp(URL_CONFIG.SCP_CBD_LAKE, { name: 'lake' }),
scene.addS3MTilesLayerByScp(URL_CONFIG.SCP_CBD_TREE, { name: 'tree' }),
scene.addS3MTilesLayerByScp(URL_CONFIG.SCP_CBD_ROAD, { name: 'road' }),
scene.addS3MTilesLayerByScp(URL_CONFIG.SCP_CBD_BRIDGE, { name: 'bridge' })
], layers => {
scene.camera.setView({ destination: SuperMap3D.Cartesian3.fromDegrees(116.4486, 39.9092, 91.3293), orientation: { heading: 3.179304500963121, pitch: -0.46239072362282485, roll: 6.283185307179583 } });
layers.forEach(l => l.selectEnabled = false);
const perfEl = document.querySelector(".supermap3d-performanceDisplay");
if (perfEl) perfEl.style.display = "none";
// 核心:视频投影对象
const projectionImage = new SuperMap3D.ProjectionImage(scene);
const targetLon = 116.4486895458964;
const targetLat = 39.907729088565965;
const targetHeight = 32.51768911137742;
// WSPlayer 初始化
const realPlayer = new PlayerManager({
el: "ws-real-player",
type: 'real',
maxNum: 1,
num: 1,
showControl: false,
prefixUrl: 'lib',
receiveMessageFromWSPlayer: (method, data, err) => {
if (method === 'initializationCompleted') {
realPlayer.setPlayerAdapter("stretching");
playVideoAndProject();
} else if (method === 'realSuccess') {
console.log("视频流播放成功,开始构建投影");
const videoEl = document.querySelector('#ws-real-player video');
if (videoEl) {
projectionImage.setImage({ video: videoEl });
projectionImage.build();
}
} else if (method === 'realError') {
console.error("视频流失败", err);
}
}
});
function projectToFixedPoint() {
const cameraPos = scene.camera.positionCartographic;
const viewPos = [SuperMap3D.Math.toDegrees(cameraPos.longitude), SuperMap3D.Math.toDegrees(cameraPos.latitude), cameraPos.height];
projectionImage.viewPosition = viewPos;
projectionImage.horizontalFov = Number($("#horizontal").val());
projectionImage.verticalFov = Number($("#vertical").val());
projectionImage.distance = Number($("#distance").val());
projectionImage.hintLineVisible = $("#visibleLine").is(":checked");
projectionImage.setDistDirByPoint([targetLon, targetLat, targetHeight]);
}
function playVideoAndProject() {
const wsURL = $("#real-ws").val();
const rtspURL = $("#real-rtsp").val();
realPlayer.realByUrl({ rtspURL, wsURL, selectIndex: 0, channelId: '', streamType: 1, playerAdapter: 'stretching' });
projectToFixedPoint();
}
setTimeout(() => playVideoAndProject(), 1500);
$("#active").click(() => { realPlayer.close(0); setTimeout(playVideoAndProject, 300); });
$("#clear").click(() => { realPlayer.close(0); projectionImage.destroy && projectionImage.destroy(); });
$("#horizontal,#vertical,#distance").on("input", projectToFixedPoint);
$("#visibleLine").on("change", projectToFixedPoint);
});
}
if (typeof SuperMap3D !== 'undefined') {
window.startupCalled = true;
onload(SuperMap3D);
}
</script>
</body>
效果展示
完成上述配置后,视频流可成功投影至三维模型建筑上。



