资讯动态

Vue3+Cesium三维GIS场景录屏:Canvas捕获到MediaRecorder编码实践

发布时间:2026/10/24 4:34:04 来源:尧图企业网站定制
做三维 GIS 项目这几年我明显感觉到“屏幕录制”被写进需求文档的频率越来越高。不管是给客户输出一段地图巡航演示视频、在群里复现某个瓦片加载闪白的问题还是把车辆轨迹回放留档只要场景基于 Cesium录屏就成了绕不开的刚需。这篇文章我会直接围绕 Vue3 Cesium 这套组合讲清楚从方案选型到 MediaRecorder 编码输出再到稳定性优化的完整落地过程让同样在做 Web 三维地图、可视化大屏录屏需求的同学少踩几个坑。适用人群很明确已经在用 Vue3 开发 Cesium 应用、被“怎么把三维场景录成视频”困扰的开发者或者产品上需要“一键导出操作过程”但还没定技术方案的团队。读完你可以直接抄走核心实现也能理解为什么有些录出来会黑屏、卡顿、音画不同步。1. 项目背景与录制方案选型1.1 需求场景拆解先别急着写代码录屏功能首先得想清楚“录给谁看、录什么内容、在什么环境录”这直接决定技术选型。我接到的真实需求大致分三类阶段汇报与交付演示客户现场网络不稳定或者客户的机器没有独立显卡直接跑系统很容易掉帧。最稳妥的方式是提前在本地把关键操作路径录成 MP4/WebM汇报时放视频不赌现场环境。问题复现与远程排障三维场景的渲染问题往往不具备确定性比如 3D Tiles 偶发白斑、粒子特效闪烁、光照异常开发在自己机器上复现不出来。让现场人员用系统自带的录屏按钮录一段操作过程对定位问题帮助巨大。轨迹回放与时空归档飞行巡检、车辆 GPS 轨迹、管网巡检路线这类业务经常要把一个时间段内的场景动态导出成视频方便归档、审计或做进一步分析。这三种场景对录制能力的要求不太一样汇报演示要求画质清晰、不能卡顿问题复现要求录制入口简单、文件体积可控轨迹回放则要求时间轴与场景相机同步准确。但底层核心是一致的把 Cesium 渲染出来的每一帧画面稳定地采集下来编码成视频文件。1.2 方案选型对比与取舍逻辑实现“录屏”在浏览器里有两条主流路线维度方案 AgetDisplayMedia 整屏/窗口捕获方案 Bcanvas.captureStream MediaRecorder捕获对象浏览器窗口、整个屏幕或某个标签页指定的 canvas 元素Cesium 底层就是 canvas画面清晰度取决于系统显示分辨率与缩放比例与 canvas 渲染分辨率一致可控性更强内容隔离性容易录到无关窗口、弹窗、系统通知只录三维场景画布不掺杂质音频采集系统内音频需要浏览器设置或单独插件配合可以单独接麦克风音轨或合并系统音频轨道实现成本低代码很少中需要处理帧同步与资源释放长时间录制稳定性通常稳定但切窗口、缩放后画面会变糊相对稳定但要关注 GPU 内存与编码压力是否依赖用户手势必须在用户点击事件中调用否则浏览器拒绝MediaRecorder 本身也建议在用户手势中启动从代码量看方案 A 更简单const stream await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: 30 }, audio: true, });但实际用下来问题很多。最典型的是用户在录屏过程中弹了个系统通知或者切了一下窗口视频里就出现了不该出现的内容。更麻烦的是Cesium 页面往往有多个 canvas整屏捕获会录到无关的 UI 元素、地图控件、甚至是控制台调试面板。我曾在汇报视频里录进了自己终端上的报错日志场面非常尴尬。方案 B 的优势在于“精准采集”。Cesium 渲染结果就是一张 canvasconst stream canvas.captureStream(30); const recorder new MediaRecorder(stream, { mimeType: video/webm });它录出的画面只有三维场景本身分辨率由 canvas 决定还可以在后处理阶段把业务 UI 图层合成进去。代价是——你必须处理 Cesium 自己那套渲染调度机制否则录出来的视频就是静止的或黑屏。我个人的选型结论是能用方案 B就不用方案 A。如果产品经理坚持要“连同左侧业务面板一起录制”可以在方案 B 基础上做一个离屏合流画布把 Cesium canvas 绘制到更大的 canvas 上再把业务 DOM 的截图合成进去。这样既保持了内容隔离又满足了“录完整系统”的需求。2. 搭建 Vue3 工程与 Cesium 实例基础配置2.1 Vite 引入 Cesium 容易踩的坑先说工程基础。我用的是 Vue3 ViteCesium 通过 npm 安装后有一个容易被忽略的问题Cesium 的静态资源Assets、Workers、ThirdParty目录需要被正确拷贝和访问否则控制台会疯狂报错甚至连 Viewer 都建不起来。Vite 下推荐做法是安装vite-plugin-static-copy或者手动配置public目录。以下是我验证过可用的一种方式// vite.config.js import { defineConfig } from vite; import vue from vitejs/plugin-vue; import { viteStaticCopy } from vite-plugin-static-copy; export default defineConfig({ plugins: [ vue(), viteStaticCopy({ targets: [ { src: node_modules/cesium/Build/Cesium/Workers, dest: cesium }, { src: node_modules/cesium/Build/Cesium/ThirdParty, dest: cesium }, { src: node_modules/cesium/Build/Cesium/Assets, dest: cesium }, { src: node_modules/cesium/Build/Cesium/Widgets, dest: cesium }, ], }), ], });然后在入口文件设置全局路径// main.js import * as Cesium from cesium; window.CESIUM_BASE_URL /cesium;不配置CESIUM_BASE_URL的话Cesium 会尝试从默认路径加载 worker而 Vite 的打包结果里不一定有这个路径直接导致调用createWorldTerrainAsync或影像图层时静默失败。这个问题很隐蔽我一度以为是录制代码的问题排查了半天才发现是静态资源没拷全。2.2 影响画面质量的 Cesium 渲染参数配置Viewer 初始化时有几个参数会直接影响录制画面的效果——注意不只是性能还有“录出来好不好看”。template div refcontainerRef classcesium-container/div /template script setup import * as Cesium from cesium; import { onMounted, onUnmounted, ref } from vue; const containerRef ref(null); let viewer null; onMounted(() { if (!containerRef.value) return; viewer new Cesium.Viewer(containerRef.value, { // 关闭默认加载的影像图层避免录屏时换底图被录进去 baseLayerPicker: false, // 用法自己控制加载某影像图层 imageryProvider: false, // 地形可以另配这里先留空 terrainProvider: undefined, // 关闭星空背景视频里星空噪点非常影响编码压缩率 skyBox: false, // 按需渲染模式录制时需要用 requestRender 主动驱动 requestRenderMode: true, // 这个很关键录屏时如果用户误拖动了控件会影响输出 animation: false, timeline: false, fullscreenButton: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false, selectionIndicator: false, }); // 抗锯齿与分辨率设置 const scene viewer.scene; scene.msaaSamples 4; scene.fxaa true; scene.highDynamicRange false; scene.globe.depthTestAgainstTerrain true; // 限制相机远近避免录制时相机拉到奇怪位置导致场景崩溃 scene.screenSpaceCameraController.minimumZoomDistance 50; scene.screenSpaceCameraController.maximumZoomDistance 50000; // 加一个基础影像做底 const layer viewer.imageryLayers.addImageryProvider( new Cesium.OpenStreetMapImageryProvider({ url: https://tile.openstreetmap.org/, }) ); layer.alpha 1; }); /script这里有些细节必须说明关闭skyBox录制场景时默认的星空背景在视频编码里会产生大量噪声导致同码率下画面更糊。在部分偏暗的汇报环境里星空背景也显得不够正式。改为纯色天空盒或者直接关闭会更干净。开启 MSAACesium 的默认场景抗锯齿较弱直接录出来的视频边缘锯齿感很重。把msaaSamples设置为 4配合fxaa: true在线条、道路边缘上有肉眼可见的改善。但注意 MSAA 会增加 GPU 压力如果目标机器性能不行优先保帧率。requestRenderMode: true的取舍这是 Cesium 的按需渲染模式静态场景不重绘大幅降低 CPU/GPU 占用。但录屏时“按需”恰恰是我们最讨厌的——画面不变时captureStream采的就是同一个静止画面。所以我的做法是初始化时开这个模式等用户点击录制按钮后在postRender回调里不停调用scene.requestRender()强制重绘录制结束再恢复。关闭默认控件animation、timeline、baseLayerPicker这些控件就算 UI 上可以接受录进视频之后也容易暴露版本差异或操作痕迹。做演示视频时画面越干净越好。这里补充一个观点录屏工具的“画质”不全靠编码参数场景本身的抗锯齿、光照、天空盒、LOD 策略占了大头。同样一段航线漫游一个配置得当的场景和一个默认配置的场景录出来的观感差距非常明显。3. 录制核心链路canvas 流捕获与 MediaRecorder 编码3.1 核心原理captureStream 到底做了什么先理解这条链路的底层机制canvas.captureStream(frameRate)并不是每帧回调一次而是浏览器以固定帧率对 canvas 当前内容做“截图采样”生成一个MediaStreamTrack。这个流轨道可以被MediaRecorder编码成视频。有一个很容易被忽略的坑captureStream的采样帧率和 Cesium 的实际渲染帧率是解耦的。Cesium 在requestRenderMode下可能 100ms 才渲染一帧但captureStream却在按 30fps 采样结果就是录出来的视频大量重复帧看起来像卡顿。反过来如果 Cesium 渲染帧率高于 30fpscaptureStream会丢帧。所以我的核心做法是录制期间用 Cesium 的postRender事件手动驱动重绘把“按需渲染”临时改成“持续渲染”节奏由我们自己控制。// 在开始录制后挂载这个监听 const renderListener () { if (isRecording.value) { viewer.scene.requestRender(); } }; viewer.scene.postRender.addEventListener(renderListener);这一段代码看起来简单但它是“录制卡顿/静止”问题的关键解药。没有它requestRenderMode模式下录出来的视频极大概率是“每隔几秒动一下”的幻灯片效果。3.2 可复制的 Vue3 录制实现代码下面直接给出一个可运行的 Vue3 Composition API 实现。核心思路用ref管理三个状态录制中、录制时间、错误信息用MediaRecordercanvas.captureStream(30)产生视频轨可选添加麦克风音频轨录制结束后把Blob转成可下载链接。template div classrecorder-wrap div refcontainerRef classcesium-container/div div classtoolbar button clickstartRecording :disabledisRecording {{ isRecording ? 录制中... : 开始录制 }} /button button clickstopRecording :disabled!isRecording停止并保存/button span v-ifrecordingTime{{ recordingTime }}s/span /div video v-ifvideoUrl :srcvideoUrl controls classpreview-video/video /div /template script setup import * as Cesium from cesium; import { onMounted, onUnmounted, ref } from vue; const containerRef ref(null); const previewVideo ref(null); const videoUrl ref(); const isRecording ref(false); const recordingTime ref(0); const errorMsg ref(); let viewer null; let mediaRecorder null; let recordedChunks []; let captureStream null; let timerInterval null; const renderListener () { if (isRecording.value viewer) { viewer.scene.requestRender(); } }; onMounted(() { initViewer(); }); onUnmounted(() { if (viewer) { viewer.scene.postRender.removeEventListener(renderListener); viewer.destroy(); } }); function initViewer() { viewer new Cesium.Viewer(containerRef.value, { baseLayerPicker: false, imageryProvider: false, skyBox: false, requestRenderMode: true, animation: false, timeline: false, fullscreenButton: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false, selectionIndicator: false, }); const scene viewer.scene; scene.msaaSamples 4; scene.fxaa true; scene.globe.depthTestAgainstTerrain true; // 这里建议先加载好底图再允许录制 const defaultLayer viewer.imageryLayers.addImageryProvider( new Cesium.OpenStreetMapImageryProvider({ url: https://tile.openstreetmap.org/, }) ); defaultLayer.alpha 1; // 初始化相机到某个可用的视角 viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 15000), }); } async function startRecording() { if (!viewer) return; // 1. 清空上次录制遗留 recordedChunks []; videoUrl.value ; // 2. 从 Cesium 的 canvas 获取视频流 const canvas viewer.scene.canvas; captureStream canvas.captureStream(30); // 3. 如果需要录音可以在这里添加麦克风轨道 let audioTrack null; try { const audioStream await navigator.mediaDevices.getUserMedia({ audio: true, }); audioTrack audioStream.getAudioTracks()[0]; captureStream.addTrack(audioTrack); } catch (e) { // 用户拒绝麦克风权限或设备不存在仅录制画面不阻断全局流程 console.warn(未获取到音频轨道录制将无声音, e); } // 4. 选择浏览器支持的编码格式 const mimeType MediaRecorder.isTypeSupported(video/webm;codecsvp9) ? video/webm;codecsvp9 : video/webm; mediaRecorder new MediaRecorder(captureStream, { mimeType, videoBitsPerSecond: 8_000_000, // 8Mbps画质与体积平衡点 }); // 5. 收集数据 mediaRecorder.ondataavailable (e) { if (e.data e.data.size 0) { recordedChunks.push(e.data); } }; // 6. 开启录制 mediaRecorder.start(1000); // timeslice 1000ms每秒钟触发一次 dataavailable 便于分片记录 isRecording.value true; recordingTime.value 0; timerInterval setInterval(() { recordingTime.value 1; }, 1000); // 7. 挂载帧同步回调 viewer.scene.postRender.addEventListener(renderListener); } async function stopRecording() { if (!mediaRecorder || !isRecording.value) return; return new Promise((resolve) { mediaRecorder.onstop () { // 1. 生成 Blob const blob new Blob(recordedChunks, { type: video/webm }); // 2. 生成预览地址 if (videoUrl.value) URL.revokeObjectURL(videoUrl.value); videoUrl.value URL.createObjectURL(blob); // 3. 清理资源 if (captureStream) { captureStream.getTracks().forEach((track) track.stop()); captureStream null; } mediaRecorder null; isRecording.value false; clearInterval(timerInterval); recordingTime.value 0; viewer.scene.postRender.removeEventListener(renderListener); resolve(blob); }; mediaRecorder.stop(); }); }几个关键点展开说明编码格式选择video/webm;codecsvp9优先VP9 在同等码率下画质明显好于 VP8但编码慢一截。如果录制时长较短3 分钟以内VP9 完全可接受。如果长时间录制、又不希望浏览器内存爆掉退回video/webm默认编码更稳妥。H.264 在浏览器中的支持很不统一不建议在 WebM 里强求。码率设置videoBitsPerSecond: 8_000_000是我实测比较平衡的值。Cesium 场景的特点是地图瓦片细节多、相机移动时全屏变化码率太低会出现大量压缩伪影。给客户演示的视频我一般开到 10Mbps内部排障录屏用 4Mbps 就够了文件体积小一半。timeslice 的意义mediaRecorder.start(1000)表示每 1 秒触发一次ondataavailable。这么做有两个好处一是即使浏览器崩溃已经触发过的分片数据还在内存里不至于全部丢失二是可以针对大数据量做分片比如每 10 秒一个 chunk最后合并。3.3 UI 业务浮层合屏录制很多业务系统不止要录三维场景还要录左上角的项目名称、右侧的天气面板、底部的统计卡片。如果选方案 B这些 DOM 元素默认不会被录进 canvas 流。我的实现思路是“离屏画布合流”准备一个更大的 canvas在postRender或者requestAnimationFrame回调里先把 Cesium canvas 绘制上去再用 Canvas 2D 把业务 DOM 信息或者前面生成的 DOM 截图画到对应位置。这样最终的captureStream是从合流 canvas 取的录出来的视频包含全部业务信息。function compositeFrame() { const compositeCanvas document.getElementById(composite-canvas); const ctx compositeCanvas.getContext(2d); // 先把 Cesium 画面画到底层 ctx.drawImage(viewer.scene.canvas, 0, 0, compositeCanvas.width, compositeCanvas.height); // 再把业务信息绘制到左上角 ctx.save(); ctx.fillStyle rgba(0,0,0,0.55); ctx.fillRect(20, 20, 240, 60); ctx.fillStyle #ffffff; ctx.font 28px sans-serif; ctx.fillText(某智慧园区三维平台, 40, 60); ctx.restore(); }这套“合屏”方案的坑在于Cesium canvas 可能需要高 DPI 适配比如devicePixelRatio 2而合流 canvas 直接用 CSS 尺寸会导致画面被拉伸或裁切。正确做法是让合流 canvas 的宽高等于 Cesium canvas 的宽高与 DPR 的乘积再通过 CSS 样式缩放到目标显示大小。4. 录制稳定性、性能优化与内存管理4.1 requestRenderMode 下的帧同步再深入一点前面提到录制时要手动requestRender()但这里还有一个隐藏问题captureStream(30)采样帧率是固定的 30fps而viewer.scene.postRender回调触发频率其实是跟随requestRender的节奏的。如果某个时段场景因为加载纹理卡顿实际渲染可能只有 10fps那么captureStream就会反复采样同一帧画面视频看起来就是跳帧。我的解决方案是动态调整目标帧率而不是把captureStream死定在 30fps。可以维护一个滑动窗口统计最近 3 秒的平均帧耗时如果平均帧耗时超过 45ms就把录制帧率降到 24fps如果超过 70ms就降到 15fps并且把videoBitsPerSecond同步下调避免编码堆积。let frameTimestamps []; function collectFrameTime() { const now performance.now(); frameTimestamps.push(now); // 只保留最近 3 秒的数据 while (frameTimestamps.length 0 now - frameTimestamps[0] 3000) { frameTimestamps.shift(); } // 简单换算 FPS if (frameTimestamps.length 1) { const avgFps (frameTimestamps.length - 1) / (now - frameTimestamps[0]); // 根据 avgFps 决定是否需要调整录制参数 } }这个思路比单纯设定固定帧率更贴近真实场景。我在某个大屏项目里地图拖动时 FPS 可能掉到 18但如果录制帧率也是 18视频观感其实尚可问题出在“录制定在 30但实际只有 18”产生大量重复帧视频才会显得一顿一顿。4.2 长时间录制的内存管理MediaRecorder 本身不会直接吃掉 Cesium 的纹理内存但录制期间为了保持画面更新Cesium 被迫每帧都在渲染。长时间运行后最容易出现两个问题3D Tiles 纹理加载堆积相机不断移动大量瓦片被创建显存持续上升。录制持续 20 分钟以上某些机器会直接触发WEBGL_lose_context。Blob 分片堆积ondataavailable收集的 chunks 如果体积很大又全部保存在 JS 堆里会造成内存翻倍。特别是 10Mbps 码率10 分钟就是 750MB加上 Blob 合并时的临时转换很容易顶爆普通 8GB 内存的机器。针对性的优化措施录制时临时提高scene.maximumScreenSpaceError让远处瓦片更早简化减少纹理压力。用timeslice分片 分段保存替代单文件长录制例如每 60 秒把当前 chunks 合并保存为临时文件然后清空recordedChunks数组。录制过程中隐藏业务上不重要的图层、实体、广告牌。信息量越少编码压力越小。录制结束时立即调用captureStream.getTracks().forEach(track track.stop())解除流引用否则 canvas 会被持续采样Cesium 销毁时可能出现异常。这里特别强调一点录制过程中不要动态调整 canvas 的 width/height。有些同学为了让画面更清晰在录制中途把 canvas.width 改大这会导致captureStream内部轨道重置录出来的视频前半段和后半段分辨率不一致播放器兼容性很差。要调整分辨率必须在captureStream()之前改好。4.3 WebGL 上下文丢失的处理长时录制最常见的致命伤是 WebGL 上下文丢失。现象是浏览器标签页一直开着GPU 进程因为显存压力或驱动 reset 被杀掉页面上的三维场景瞬间变白/变黑MediaRecorder 却还在继续编码最终录出来的视频后半段全是空白。建议在创建 Viewer 后立即监听上下文事件const canvas viewer.scene.canvas; canvas.addEventListener(webglcontextlost, (e) { e.preventDefault(); // 录制中遇到上下文丢失最好直接终止本次录制提示用户重新开始 if (isRecording.value) { stopRecording(); } // 由业务决定是否重建 viewer }); canvas.addEventListener(webglcontextrestored, () { // 上下文恢复后 Cesium 一般会自动重绘但录制可能需要重新初始化 stream console.warn(WebGL 上下文已恢复需要重新初始化 Viewer 或重新录制); });注意preventDefault()很关键。不调用的话浏览器会在上下文丢失后直接抛弃整个 canvasCesium 后续再调用 WebGL API 就会一直报错。调用后我们有概率在 GPU 进程恢复时继续使用同一个 viewer。5. 常见问题与排查技巧实录整理了我在实际项目中反复遇到的问题基本能覆盖 80% 的录制异常场景。现象可能原因解决与规避录制出来是黑屏captureStream绑定到了错误的 canvasWebGL 上下文丢失确认 Cesium 场景用viewer.scene.canvas监听webglcontextlost视频非常卡像幻灯片requestRenderMode下没有手动requestRender()渲染帧率低于捕获帧率postRender回调中调用scene.requestRender()动态调整目标帧率视频清晰度低文字发糊编码码率不够canvas 分辨率不足提高videoBitsPerSecond录制前检查canvas.width与 DPR录制文件体积巨大码率设置过高场景信息量大降低码率隐藏不必要的图元分片保存音画不同步用户在录制中切了标签页getUserMedia音频时钟与视频时钟漂移合理设置 timeslice 分片避免录制中切换 Tab长视频分段录制录制开始后画面静止不动captureStream已启动但 Cesium 没有主动渲染临时关闭requestRenderMode或挂postRender监听录制结束保存的 WebM 打不开Chrome 保存的 WebM 缺少某些 metadata浏览器崩溃导致 Blob 不完整分片录制用timeslice降低数据丢失风险用播放器兼容性较好的video/webm编码下面挑三个比较典型的把排查思路展开讲。5.1 帧同步与音画不同步排查我在一次客户演示的录制里发现视频播放到 30 秒以后画面和声音越来越对不上。排查过程先确认是不是getUserMedia的麦克风音轨出现了时钟漂移。把音频轨去掉重新录制发现画面本身没问题。再用performance.now()对比录制时间与本地时间确认 Cesium 的clock没有异常跳变。最后定位到问题录制过程中我打开了一个新的浏览器标签页主线程的requestAnimationFrame被浏览器降级暂停导致 Cesium 渲染变慢但MediaRecorder的音频轨道还在以实时时钟写入数据于是音画就错开了。这个问题的直接规避方式录制前提示用户不要切换标签页或者干脆用document.visibilityState在页面隐藏时自动暂停录制避免产生无效数据。5.2 录制场景的渲染调优速查表下面这张表是给团队内部整理过的调优顺序按照性价比从高到低排列优先级调优项具体做法效果高关闭默认星空和控件skyBox: false隐藏所有不必要控件提升画面纯净度与编码压缩率高固定相机范围minimumZoomDistance/maximumZoomDistance防止相机拉太近导致瓦片加载爆炸中开 MSAA FXAAmsaaSamples4fxaatrue明显改善边缘锯齿中调整 LOD 策略maximumScreenSpaceError临时调大降低瓦片加载压力中隐藏次要图层imageryLayers中临时关闭业务影像层减少纹理内存低降低录制帧率动态 30fps → 24fps → 15fps降低编码压力减少文件体积低开启分片录制mediaRecorder.start(1000)崩溃时保留数据降低内存峰值这里要特别提醒每次录制结束恢复调优前的参数状态。如果录制时调大了maximumScreenSpaceError停止录制后必须调回默认值否则用户正常使用系统时会发现远处地物加载粗糙且很难联想到是录屏代码留下的“后遗症”。5.3 纹理闪白与首帧白屏的规避还有一个现象在大场景项目里非常常见动态加载 BIM 模型或 3D Tiles 时录制视频的第一秒会出现白屏或闪白之后恢复正常。原因是纹理上传是异步的GPU 还没有完成纹理绑定canvas 就已经被captureStream采样了。性能差的机器上这种“纹理追赶”甚至会持续好几秒。我的做法是开始录制之前强制让 Cesium 预先渲染一段时间把需要的瓦片先加载进来。async function warmUpScene(waitMs 1500) { // 预渲染一小段时间让纹理和瓦片尽量就绪 const start performance.now(); while (performance.now() - start waitMs) { viewer.scene.render(); await new Promise((resolve) setTimeout(resolve, 16)); } }在startRecording()里第一步调用warmUpScene()再执行canvas.captureStream(30)可以显著降低首帧白屏概率。不过这只能缓解纹理异步上传如果模型本身很大建议在录制前先让模型空闲加载完成比如先飞一遍相机或预加载 bounding sphere。6. 结尾经验小结与后续扩展最后说一个我踩过几次坑之后总结出来的习惯不要把 MediaRecorder 的调用直接写进业务页面里。创建一个独立的RecorderManager类或 Vue composable统一管理录制状态、资源释放、帧监听这样无论是接入汇报模式、巡检模式还是问题复现模式都能复用同一套录制逻辑。另一个值得提前准备的接口是为“按关键帧导出图片序列”留一个钩子。我在postRender回调里记录录制帧的同时会把当前帧的时间戳和关键元数据同步保存下来后面做时空回放、AI 训练数据标注时这个帧索引可以省掉大量重构工作。对于一套要长期维护的三维 GIS 系统来说这个设计比“能录出 WebM”这件事本身更有价值。我自己的做法是每次录制结束保存 WebM 后顺手把每一帧的缩略图抽出来做索引按时间点定位视频内容。在给客户演示、回放一条轨迹时这个“小功能”真的能救命——你不用从头播放整段视频找目标点直接看缩略图就能跳到对应位置。如果你正好也在做 Cesium 视频输出类的需求非常推荐把这个思路纳入你的方案里。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑