前置学习需要先了解一下WebAssembly,定义和目前支持情况。
WebAssembly
WebAssembly(简称Wasm)是一种低级的二进制指令格式,设计用于在Web浏览器中以接近原生代码的速度高效执行。它于2017年由W3C标准化,并得到主流浏览器的全面支持(截至2025年)。以下是其核心特点和应用场景:
核心定义与特点
-
高性能
- 作为编译目标语言,允许将C/C++、Rust等语言编写的代码编译成紧凑的二进制格式,直接在浏览器中运行,性能接近原生程序,尤其适合计算密集型任务(如游戏、图像处理)。
-
跨平台与安全
- 运行在沙箱环境中,无法直接访问系统资源,需通过浏览器API交互,确保安全性。
- 支持跨浏览器和跨设备运行,无需修改代码。
-
与JavaScript互补
- 不替代JavaScript,而是通过JavaScript API加载Wasm模块,二者协同工作:JavaScript处理逻辑交互,Wasm处理高性能计算。
-
多语言支持
开发者可使用C/C++、Rust、Go等语言编写代码,再编译为Wasm,拓展了Web开发的工具链。
典型应用场景(2025年主流方向)
- 图形与游戏:3D渲染、物理引擎(如Unity引擎导出至Web端)。
- 音视频处理:实时编解码、AI驱动的图像识别(如浏览器内视频剪辑工具)。
- 云原生与边缘计算:在服务端(如Docker替代方案)或物联网设备中运行轻量级沙箱应用。
- 跨平台应用:通过WebAssembly System Interface(WASI),实现在浏览器外的操作系统级调用。
萤石开发接入步骤
- 创建Vue3项目:可能需要使用Vue CLI或Vite初始化项目,这在前面的搜索结果中有提到安装Vue CLI的步骤。
- 安装必要的依赖:如axios用于API请求,ezuikit-js用于萤石云播放器。
- 获取萤石云的访问凭证:AppKey、AppSecret和设备信息,这需要用户注册萤石云平台。
- 在Vue组件中初始化播放器:使用EZUIKit的API,并处理生命周期钩子以确保正确初始化和销毁实例。
- 添加控制功能:如云台控制、全屏播放等,参考搜索结果中的代码示例。
- 处理安全性和性能优化:如动态获取accessToken,避免硬编码,处理浏览器的自动播放策略。
一、前置准备
-
环境要求
- Vue 3.2+
- Node.js 16+
- 萤石云账号(需注册并开通开发者权限,获取
AppKey和AppSecret) -
安装依赖
npm install ezuikit-js axios # 播放器SDK + HTTP请求库
二、核心步骤
1. 初始化播放器组件
<!-- VideoPlayer.vue -->
<template>
<div ref="playerContainer" class="video-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import axios from 'axios';
// 容器引用
const playerContainer = ref(null);
let player = null;
// 动态获取AccessToken(避免硬编码)
const getAccessToken = async () => {
const response = await axios.post('https://open.ys7.com/api/lapp/token/get', {
appKey: 'your-app-key',
appSecret: 'your-app-secret'
});
return response.data.data.accessToken;
};
// 初始化播放器
onMounted(async () => {
const accessToken = await getAccessToken();
player = new EZUIKit.EZUIPlayer({
id: playerContainer.value, // 直接绑定DOM元素
accessToken: accessToken,
url: 'ezopen://open.ys7.com/设备序列号/1.live', // 示例设备地址
autoplay: false, // 需用户交互后播放
width: '100%',
height: '500px',
controls: true
});
// 事件监听
player.on('play', () => console.log('Playback started'));
player.on('error', (err) => console.error('Error:', err));
});
// 销毁实例
onUnmounted(() => {
if (player) player.destroy();
});
</script>
<style scoped>
.video-container {
margin: 20px auto;
border: 1px solid #eee;
}
</style>
2. 云台控制扩展
<script setup>
// 在组件内添加控制方法
const controlPTZ = (command, speed = 0.5) => {
if (!player) return;
player.ptzControl({
command: command.toUpperCase(), // LEFT/RIGHT/UP/DOWN/STOP
speed: Math.min(1, Math.max(0.1, speed))
});
};
</script>
<template>
<div>
<button @click="controlPTZ('left')">左转</button>
<button @click="controlPTZ('right')">右转</button>
<button @click="controlPTZ('stop')">停止</button>
</div>
</template>
三、安全与优化
-
Token 动态管理
-
通过后端接口获取Token(前端不应存储
appSecret) -
使用Vue的
provide/inject全局共享Token
-
-
自动播放策略
<button @click="player.play()" v-if="!isPlaying">手动启动播放</button> -
多实例处理
同一页面多个摄像头时,需为每个实例分配独立容器:
<div v-for="(device, index) in devices" :key="index">
<VideoPlayer :device-serial="device.serial" />
</div>
四、常见问题解决
| 问题 | 解决方案 |
|---|---|
| 黑屏无画面 | 检查设备是否在线、url格式是否正确(需包含ezopen://协议头) |
| 控制指令无响应 | 确认设备支持云台功能,检查萤石云控制台的权限设置 |
| 内存泄漏 | 确保在onUnmounted中调用player.destroy() |
| 跨域错误 | 在萤石云控制台配置域名白名单(如 https://your-domain.com) |
账号
AppKey | AppSecret | accessToken |
|---|---|---|
| xx27 | xx9 | at.xxm3 |
| 设备序列号 | 萤石协议规范的url | |
| FQ9862222 | url: 'ezopen://open.ys7.com/设备序列号/1.live', |
需求
使用萤石 ezuikit-js SDK,结合vue3技术,实现PC端控制视频的直播和回放,设备控制、设备查询与管理功能。
- 默认展示第一个视频,支持切换1/4/9宫格。
- 每个视频可单独控制播放、回放、云台(放大缩小)、截图功能。
- 整体操作区切换布局。
- 设备查询与管理功能
- 设备查询与管理:需要从萤石云API获取设备列表,显示设备状态(在线/离线),并允许用户选择设备来添加到当前布局中。这部分可能需要调用萤石的设备列表接口,并处理分页和筛选。
- 性能优化:多视频流同时播放可能影响性能,需要考虑按需加载、非活动窗口暂停播放、分辨率切换等策略。例如,使用Intersection Observer来检测视频元素是否在视口中,从而暂停或恢复播放。
- 错误处理与用户反馈:处理视频加载失败、设备离线等情况,给出提示信息。例如,当设备离线时,显示离线状态并禁用控制按钮。
这个视频列表,默认展示一个屏幕展示第一个返回的视频。可选择4个视频在屏幕中间同时展示、也可以选择9个视频在屏幕中间同时展示。
每个视频可以单独控制放大、缩小、播放、回放等的操作区,也有一个整体的操作区,能控制1屏展示1个、4个、9个视频的控制。注意控制分屏后,视频加载的性能。避免卡顿。视频设备离线状态的给出列表说明。
视频列表。一个完整屏幕查看1个,可分屏 4个一屏、9个一屏’
使用萤石 ezuikit-js SDK,结合vue3技术,实现PC端控制视频的直播和回放,设备控制、设备查询与管理功能。操作区功能:开始录像、结束录像、视频截图、全屏、取消全屏、开始对讲、结束对讲
ertc-web-demo: ertc-web react 项目 demo
git clone gitee.com/Ezviz-OpenB…
常见的视频监控系统设计
<template>
<div
:id="id"
class="text-icon-container"
:class="{
'can-hover': isButton,
'not-allow': disabled,
active: isActive
}"
@click="handleClick"
>
<i
:class="{ menuNotClickIcon: menuNotClickIcon }"
:style="{
backgroundImage: 'url(' + getAssetUrl(icon) + ')'
}"
></i>
<span :class="disabled ? 'disabled-text' : ''">{{ text }}</span>
</div>
</template>
<script setup name="IconText">
const props = defineProps({
id: {
type: String,
default: ''
},
// 文本内容
text: {
type: String,
default: ''
},
// 图标
icon: {
type: String,
default: 'recent_view',
required: true
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否是按钮,若不是按钮则样式有区别
isButton: {
type: Boolean,
default: true
},
menuNotClickIcon: {
type: Boolean,
default: false // 一级菜单不允许点击icon
}
})
const emits = defineEmits(['click', 'reset'])
const isActive = ref('')
let handleOutsideClick = null
// 处理点击事件
function handleClick() {
if (!props.disabled) {
emits('click')
isActive.value = 'isActive'
addOutsideClickListener()
}
}
// 判断是否是一级菜单或三级菜单
function isMenuClick(event) {
// 一级菜单的判断条件
const isFirstLevelMenu = event.target.className === 'menuNotClickIcon'
console.log('isFirstLevelMenu:', event.target.className)
// 三级菜单的判断条件
const isThirdLevelMenu = [
'电费管理',
'报表管理',
'资产管理',
'配置管理',
'运行监控',
'分析诊断',
'智慧运维',
'运营中心',
'系统管理'
].includes(event.target.innerText)
return isFirstLevelMenu || isThirdLevelMenu
}
// 添加外部点击监听器
function addOutsideClickListener() {
const excludeElement = document.getElementById(props.id)
handleOutsideClick = (event) => {
// 如果点击的是一级菜单或三级菜单,则不关闭浮层
if (isMenuClick(event)) {
return
}
// 先确定可以用于判断的IconText组件元素,给isClickInside赋值
let isClickInside
if (
event.target.parentElement?.id &&
props.id &&
event.target.parentElement?.id === props.id
) {
// 判断点击事件的目标元素是否是排除元素本身,或者是否是排除元素的子元素
isClickInside = excludeElement?.contains(event.target)
// console.log("isClickInside:", isClickInside);
}
// 如果点击发生在排除元素外部,则收起浮层
if (!isClickInside && !excludeElement?.contains(event.target)) {
emits('reset')
isActive.value = ''
removeOutsideClickListener()
}
}
document.addEventListener('click', handleOutsideClick)
}
// 移除外部点击监听器
function removeOutsideClickListener() {
document.removeEventListener('click', handleOutsideClick)
}
// 组件卸载时移除监听器
onUnmounted(() => {
// removeOutsideClickListener()
})
const getAssetUrl = (name) => {
return new URL(`/src/assets/images/${name}.png`, import.meta.url).href
}
</script>
<style lang="scss" scoped>
.text-icon-container {
@apply h-10 leading-10 mr-2.5 py-0 px-4;
font-size: 1rem; /* 16/16 */
border-radius: 1.25rem; /* 20/16 */
&.can-hover {
cursor: pointer;
&:hover {
background: #edf4fc;
}
}
&.not-allow {
cursor: not-allowed;
&:hover {
background: none;
}
}
&.active {
background: #edf4fc;
}
i {
display: inline-block;
width: 1.25rem; /* 20/16 */
height: 1.25rem; /* 20/16 */
background-repeat: no-repeat;
background-size: 100% 100%;
vertical-align: middle;
}
span {
vertical-align: middle;
margin-left: 0.3125rem; /* 5/16 */
&.disabled-text {
color: #a4aab3;
}
}
}
</style>