Vue版EasyMedia流媒体前端模板:RTSP/FLV直播+单屏至十六屏自由切换
简介:基于Vue开发的即用型流媒体前端界面,专为对接EasyMedia后台设计,无需后端改造即可快速启用。支持RTSP协议实时拉流和FLV格式低延迟播放,适配安防监控、在线教学、远程会议等多路视频场景。内置四种标准视图布局:单画面全屏、2×2四分屏、3×3九分屏、4×4十六分屏,所有布局均可在Stream.vue主页面动态切换。项目结构规范,包含完整路由管理(router)、Vuex状态控制(store)、统一HTTP请求封装(http.js)、可复用自定义播放器组件(CusPlayer.vue)、流媒体预览页(Preview.vue)及核心播放视图(Stream.vue)。已配置vue.config.js构建参数,兼容Chrome/Firefox/Edge等主流现代浏览器,附带基础图标资源(logo.png、favicon.ico)与详细README说明文档。开箱即用,适合嵌入已有流媒体系统或作为独立可视化终端快速部署。
1. 项目概述:为什么这套Vue前端成了我接手流媒体项目的“第一块砖”
去年帮一家做智慧园区的客户做视频中台升级,他们后台已经跑着一套稳定运行三年的EasyMedia服务——RTSP设备接入、FLV转推、流状态管理都在线上扛着高并发。但前端一直用的是官方提供的静态HTML+Video.js demo页面,连个路由都没有,十几个摄像头全挤在一页里,拖拽缩放靠手动改CSS,运维同事每次新增一路流都要找我改index.html。我说:“这哪是前端,这是电子表格。”
后来我自己搭了一套Vue模板,从零开始写到能交付只用了三天。现在回头看,这套“Vue版EasyMedia流媒体前端模板”之所以能快速落地、反复复用,根本原因不是技术多炫酷,而是它精准卡在了工程落地的“甜点区”:不碰后端协议解析(EasyMedia已做好FLV/RTMP/HLS封装),不挑战浏览器原生限制(RTSP不直播,走EasyMedia的HTTP-FLV代理),不做过度设计(没上WebSocket心跳、没搞WebRTC自研适配),就专注把“拉流→解码→渲染→布局切换”这条链路打磨得丝滑、健壮、可维护。
你可能正面临类似场景:后台已有EasyMedia,但前端要么太简陋,要么太重(比如硬塞进Vue3+Pinia+Vite+微前端架构里,结果光配置就调两天)。这套模板就是为“今天下午就要上线三路监控画面”的真实需求而生的。它支持RTSP协议拉流?准确说是通过EasyMedia的HTTP-FLV接口间接支持RTSP源——这是关键前提,必须厘清,否则你会在Chrome控制台看到一堆跨域或MIME错误;它说“开箱即用”,是指npm install && npm run serve之后,改两行配置就能看到真实摄像头画面,而不是还要去部署一个Mock Server;它标榜“单屏至十六屏自由切换”,背后是一套基于CSS Grid动态生成容器、配合播放器实例生命周期管理的轻量方案,不是靠v-for暴力渲染16个video标签然后祈祷内存不爆。
关键词里“Vue流媒体前端”是定位,“RTSP播放器”是能力入口,“FLV播放”是核心传输载体,“多分屏显示”是交互形态,“EasyMedia集成”是落地前提——这五个词串起来,就是整套方案的DNA。接下来我会带你一层层拆开它:为什么选HTTP-FLV而非WebRTC?为什么布局不用Flex而用Grid?为什么CusPlayer.vue要自己封装而不直接用flv.js的FlvPlayer?这些选择背后,全是我在十多个现场踩坑后筛出来的最优解。
2. 整体架构与设计思路:不造轮子,只搭桥
2.1 核心逻辑链:EasyMedia如何成为前端的“协议翻译官”
很多新手第一反应是:“Vue怎么直接播RTSP?” 答案是:它根本不播RTSP。RTSP是TCP长连接信令协议,浏览器原生不支持,强行用WebAssembly解析是给自己挖坑。这套模板的聪明之处,在于彻底放弃“前端直连RTSP”的幻想,转而信任EasyMedia作为中间层的能力。
EasyMedia在这里扮演三个关键角色:
- 协议转换器:将设备端的RTSP流(如 rtsp://192.168.1.100:554/stream1)转成浏览器友好的HTTP-FLV流(如 http://em-server:8080/live/test.flv);
- 状态路由器:提供标准HTTP接口查询流状态(GET /api/v1/streams)、启停流(POST /api/v1/streams/start)、获取流参数(GET /api/v1/streams/{id}/info);
- 跨域代理:前端所有请求都打向EasyMedia的API端口(默认8080),规避了传统流媒体前端最头疼的跨域问题。
所以整个数据流向是:
IPC设备 → EasyMedia(RTSP接入+FLV转推) → Vue前端(HTTP请求拉取FLV流 + flv.js解码渲染)
这个设计规避了三大雷区:
- 不需要在前端处理RTSP的DESCRIBE/SETUP/PLAY等复杂信令;
- 不依赖WebSocket或SSE维持长连接,降低前端内存泄漏风险;
- 所有流控制逻辑(启停、重连、鉴权)由EasyMedia统一管理,前端只做状态同步。
提示:如果你的EasyMedia未开启HTTP-FLV服务,请检查其配置文件中的
http_flv段落,确保enable为true,且port与前端请求端口一致(默认8080)。常见错误是EasyMedia开了RTMP但忘了开FLV,导致前端请求/live/xxx.flv返回404。
2.2 技术栈选型:为什么是Vue2 + flv.js + CSS Grid?
这套模板锁定Vue 2.6.x(非Vue3),是有明确工程考量的:
- 兼容性优先:大量政企客户仍在用IE11兼容模式(哪怕只是内网),Vue3的Proxy无法降级,而Vue2的Object.defineProperty在旧浏览器中更可控;
- 生态成熟度:flv.js官方明确支持Vue2的ref和v-if生命周期钩子,Vue3的Composition API需额外封装onBeforeUnmount处理销毁逻辑,增加出错概率;
- 团队成本低:现有维护人员熟悉Vue2,无需重新培训。
flv.js的选择更是经过血泪验证:
我们曾对比过hls.js、dash.js、video.js插件方案。HLS延迟普遍在10秒以上,对安防监控是致命伤;dash.js配置复杂,一次m3u8解析失败就卡死;video.js的RTSP插件实测在Chrome 90+版本频繁崩溃。而flv.js在HTTP-FLV场景下,实测端到端延迟稳定在1~1.5秒(局域网环境),且内存占用比同类方案低35%。关键它提供了createPlayer、load()、play()、pause()等清晰API,与Vue组件生命周期天然契合。
至于布局引擎为何弃Flex选CSS Grid?看一个真实案例:客户要求九分屏(3×3)下,点击任意小窗可“升格”为全屏,再点一次恢复原位。用Flex实现需计算每个item的flex-basis、order、align-self,动画过渡极难控制;而Grid只需动态修改grid-template-areas或grid-column/grid-row,配合transition: grid-column 0.3s即可实现丝滑缩放。十六分屏同理——Grid的行列定义让“4×4矩阵”变成声明式配置,而非命令式计算。
2.3 项目结构解耦:每个目录解决一个具体问题
目录结构不是为了好看,而是为了解决四类高频协作问题:
| 目录 | 解决的问题 | 实操价值 |
|---|---|---|
router/ |
多视图路由隔离 | /stream主播放页、/preview单路预览页、/config流管理页(预留)互不干扰,避免路由守卫混乱 |
store/ |
全局状态收敛 | 所有流地址、当前布局模式(1/4/9/16)、播放状态(playing/paused/error)集中管理,组件间通信不再靠EventBus或props钻透 |
http/ |
请求统一治理 | 封装了EasyMedia API的基地址、超时时间(3000ms)、错误拦截(404自动跳转404页,503提示服务不可用),避免每个组件重复写axios.create() |
components/CusPlayer.vue |
播放器逻辑复用 | 将flv.js初始化、错误重试、静音控制、分辨率自适应等逻辑封装成独立组件,<CusPlayer :src="streamUrl" />一行调用,杜绝各处复制粘贴 |
特别说明views/Stream.vue的设计哲学:它不负责拉流,只负责“调度”。它从store读取当前激活的流列表,遍历渲染CusPlayer组件,并将布局指令(如layout="4")透传给子组件。这种“瘦视图、胖组件”模式,让Stream.vue代码量始终控制在300行以内,极大降低维护成本。
3. 核心模块详解与实操要点
3.1 CusPlayer.vue:不只是播放器,更是流状态管家
这个组件是整套方案的“心脏”,它的设计直接决定了多路播放的稳定性。我们先看它的核心结构:
<template>
<div class="player-container" :class="{ 'is-fullscreen': isFullscreen }">
<video
ref="videoEl"
class="player-video"
muted
autoplay
@error="handleVideoError"
@canplay="handleCanPlay"
@timeupdate="handleTimeUpdate"
/>
<div v-if="loading" class="player-loading">加载中...</div>
<div v-if="error" class="player-error">{{ error }}</div>
</div>
</template>
<script>
import flvjs from 'flv.js'
export default {
name: 'CusPlayer',
props: {
src: { type: String, required: true }, // EasyMedia的FLV流地址,如 http://192.168.1.100:8080/live/cam1.flv
isMuted: { type: Boolean, default: true },
autoPlay: { type: Boolean, default: true }
},
data() {
return {
player: null,
loading: false,
error: '',
isFullscreen: false
}
},
watch: {
src: {
handler(newSrc) {
this.destroyPlayer()
if (newSrc) this.initPlayer(newSrc)
},
immediate: true
}
},
mounted() {
// 防止父组件v-if切换导致video元素被销毁后无法重建
this.$nextTick(() => {
if (this.src) this.initPlayer(this.src)
})
},
beforeUnmount() {
this.destroyPlayer()
},
methods: {
initPlayer(url) {
if (!url || this.player) return
this.loading = true
this.error = ''
// 关键:flv.js配置必须匹配EasyMedia输出
this.player = flvjs.createPlayer({
type: 'flv',
url: url,
isLive: true,
enableWorker: true, // 启用Web Worker解码,减轻主线程压力
enableStallMonitor: true, // 开启卡顿监控
stashInitialSize: 128, // 初始缓冲区大小(KB),太小易卡顿,太大增延迟
reuseRedirectedURL: true, // 重定向URL复用,应对EasyMedia集群负载均衡
cors: true // 强制启用CORS,即使EasyMedia已配置Access-Control-Allow-Origin
})
this.player.on(flvjs.Events.ERROR, this.handleError)
this.player.on(flvjs.Events.STATISTICS_INFO, this.handleStats)
this.player.attachMediaElement(this.$refs.videoEl)
this.player.load()
if (this.autoPlay) this.player.play()
},
destroyPlayer() {
if (this.player) {
try {
this.player.unload()
this.player.detachMediaElement()
this.player.destroy()
} catch (e) {
console.warn('Player destroy error:', e)
}
this.player = null
}
this.loading = false
this.error = ''
},
handleError(type, detail, info) {
// 分级错误处理:网络错误自动重试,解码错误提示升级
if (type === 'NetworkError' && detail === 'NetConnection.Connect.Rejected') {
this.error = '连接被拒绝,请检查EasyMedia服务状态'
this.retryLoad()
} else if (type === 'NetworkError' && detail === 'NetStream.Play.StreamNotFound') {
this.error = '流不存在,请确认流ID是否正确'
} else if (type === 'OtherError') {
this.error = `播放异常:${info.code} - ${info.msg}`
}
},
retryLoad() {
if (this.retryCount < 3) {
this.retryCount++
setTimeout(() => {
if (this.player && this.src) {
this.player.load()
this.player.play()
}
}, 2000 * this.retryCount) // 指数退避重试
}
}
}
}
</script>
关键细节解析:
- stashInitialSize: 128 是经过压测确定的黄金值:设为64时局域网偶发卡顿;设为256时首帧延迟从800ms升至1400ms;128在流畅性与延迟间取得最佳平衡;
- reuseRedirectedURL: true 应对EasyMedia集群场景:当EasyMedia配置了Nginx反向代理或K8s Service时,flv.js首次请求可能被302重定向到实际节点IP,此选项确保后续请求直接打向该IP,避免二次重定向开销;
- 错误分级处理中,NetConnection.Connect.Rejected 通常意味着EasyMedia进程崩溃或端口被占,此时自动重试比弹窗提示更符合监控场景需求;而StreamNotFound属于配置错误,必须人工介入,故不重试;
- beforeUnmount 中的try/catch不是多余:某些Android WebView中player.destroy()会抛出TypeError: Cannot read property 'destroy' of null,捕获后保证组件卸载不中断。
注意:
CusPlayer.vue必须使用v-if而非v-show控制显隐。因为v-show仅改变CSSdisplay,video元素仍存在于DOM中,flv.js实例不会被销毁,导致内存持续增长。实测16路播放下,v-show2小时内存占用达1.2GB,v-if则稳定在380MB。
3.2 Stream.vue:多分屏布局的动态引擎
Stream.vue 的核心任务是:根据store.state.layoutMode(1/4/9/16)动态生成对应数量的CusPlayer,并分配流地址。它的布局逻辑完全基于CSS Grid:
<template>
<div class="stream-layout" :class="`layout-${layoutMode}`">
<CusPlayer
v-for="(stream, index) in activeStreams"
:key="stream.id"
:src="getFlvUrl(stream)"
class="player-item"
:style="getPlayerStyle(index)"
/>
</div>
</template>
<script>
export default {
computed: {
layoutMode() {
return this.$store.state.layoutMode // 从Vuex读取当前布局模式
},
activeStreams() {
// 从store读取已激活的流列表,按顺序截取前N个(N=layoutMode)
const streams = this.$store.state.activeStreams
return streams.slice(0, this.layoutMode)
}
},
methods: {
getFlvUrl(stream) {
// 构建EasyMedia FLV流地址:http://[em-host]:[em-port]/live/[stream-id].flv
const host = process.env.VUE_APP_EM_HOST || 'localhost'
const port = process.env.VUE_APP_EM_PORT || '8080'
return `http://${host}:${port}/live/${stream.id}.flv`
},
getPlayerStyle(index) {
// 为Grid布局计算行列位置,避免JS计算,全部交由CSS控制
return {}
}
}
}
</script>
<style scoped>
.stream-layout {
display: grid;
gap: 8px;
height: 100vh;
padding: 8px;
}
.layout-1 {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.layout-4 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
.layout-9 {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
}
.layout-16 {
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
}
.player-item {
border-radius: 4px;
overflow: hidden;
background: #000;
}
/* 响应式优化:小屏设备自动缩小间隙 */
@media (max-width: 768px) {
.stream-layout {
gap: 4px;
padding: 4px;
}
.layout-16 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(8, 1fr);
}
}
</style>
为什么用CSS Grid而非JavaScript计算?
早期版本尝试过用v-for生成16个div,再通过JS设置style.gridColumn/gridRow,结果在Chrome 110+出现严重性能抖动。根源在于:每帧渲染都要触发JS计算+DOM重排。而纯CSS Grid方案,浏览器在样式计算阶段就完成布局,渲染帧率稳定在60fps。
实操心得:
- gap: 8px 和 padding: 8px 是精心设计的“呼吸感”参数:小于6px视觉拥挤,大于12px浪费屏幕空间;
- 移动端适配中,layout-16在手机上强制改为2列8行,而非4列4行,是因为4列会导致单列宽度不足200px,video标签挤压变形,文字标签难以阅读;
- border-radius: 4px 不是装饰,而是解决Chrome下video元素边缘锯齿问题的hack——加了圆角后GPU渲染更平滑。
3.3 状态管理(store):让16路流“活”起来
store/index.js 是整套方案的“中枢神经”,它管理三类核心状态:
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// 当前布局模式:1(单屏)、4(四分屏)、9(九分屏)、16(十六分屏)
layoutMode: 4,
// 已激活的流列表,每个对象包含id、name、status等
activeStreams: [
{ id: 'cam1', name: '东门入口', status: 'online', lastActive: Date.now() },
{ id: 'cam2', name: '停车场A区', status: 'online', lastActive: Date.now() }
],
// 全局播放控制:true为全部播放,false为全部暂停
globalPlayState: true,
// 流状态映射表:key为streamId,value为'playing'/'paused'/'error'
streamStates: {}
},
mutations: {
SET_LAYOUT_MODE(state, mode) {
state.layoutMode = mode
// 切换布局时,自动暂停超出数量的流,避免资源浪费
const maxStreams = Math.min(mode, state.activeStreams.length)
for (let i = maxStreams; i < state.activeStreams.length; i++) {
const streamId = state.activeStreams[i].id
Vue.set(state.streamStates, streamId, 'paused')
}
},
ADD_STREAM(state, stream) {
// 添加流时,自动插入到activeStreams末尾,并初始化状态
state.activeStreams.push(stream)
Vue.set(state.streamStates, stream.id, 'paused')
},
UPDATE_STREAM_STATUS(state, { id, status }) {
// 更新单个流状态,用于心跳检测
const stream = state.activeStreams.find(s => s.id === id)
if (stream) {
stream.status = status
stream.lastActive = Date.now()
}
}
},
actions: {
// 异步获取流列表,对接EasyMedia API
async fetchStreams({ commit }) {
try {
const res = await this.$http.get('/api/v1/streams')
const streams = res.data.data.map(item => ({
id: item.app + '/' + item.stream, // EasyMedia返回的app/stream格式,转为唯一id
name: item.name || item.app + '-' + item.stream,
status: item.status
}))
commit('SET_ACTIVE_STREAMS', streams)
} catch (e) {
console.error('Fetch streams failed:', e)
}
}
}
})
关键设计点:
- SET_LAYOUT_MODE mutation中,切换布局时主动暂停超出数量的流,而非任其后台播放。实测16路全开时,Chrome内存占用峰值达2.1GB,而仅播放4路时为680MB。暂停策略让资源消耗与实际观看需求严格对齐;
- streamStates 使用Vue.set而非直接赋值,确保响应式更新——这是Vuex中容易忽略的坑,直接state.streamStates[id] = 'playing'会导致视图不更新;
- id 字段拼接为app/stream(如live/cam1),是因为EasyMedia的HTTP-FLV地址规则要求/live/cam1.flv,这样getFlvUrl()方法可直接拼接,避免额外映射。
3.4 HTTP请求封装(http.js):让API调用像呼吸一样自然
http.js 封装了Axios实例,但它真正的价值在于错误语义化和环境隔离:
// http.js
import axios from 'axios'
// 创建axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_EM_API_BASE || '/api', // 默认走Vue CLI代理
timeout: 3000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器:添加token(若EasyMedia启用了鉴权)
service.interceptors.request.use(
config => {
const token = localStorage.getItem('em_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
// 响应拦截器:统一错误处理
service.interceptors.response.use(
response => {
// EasyMedia API约定:data字段为业务数据,code为状态码
if (response.data.code === 200) {
return response.data.data
} else {
// 抛出业务错误,供组件catch
return Promise.reject(new Error(response.data.message || '请求失败'))
}
},
error => {
if (error.response) {
switch (error.response.status) {
case 401:
// token过期,跳转登录页
localStorage.removeItem('em_token')
window.location.href = '/login'
break
case 404:
// API不存在,提示检查EasyMedia服务
alert('EasyMedia服务不可达,请检查服务器状态')
break
case 503:
// 服务繁忙,提示稍后重试
alert('EasyMedia服务暂时繁忙,请稍后重试')
break
}
}
return Promise.reject(error)
}
)
export default service
为什么需要环境变量隔离?
开发时,我们通过vue.config.js配置代理将/api转发到http://localhost:8080;生产时,EasyMedia可能部署在http://em.internal:8080,此时需在构建时注入VUE_APP_EM_API_BASE。若不隔离,上线后所有API请求都会打向/api(404),而开发者本地却一切正常,排查难度陡增。
提示:EasyMedia默认关闭API鉴权,若你启用了JWT认证,请确保
VUE_APP_EM_API_BASE指向正确的API端口(非FLV端口),并在localStorage中存入有效token。
4. 实操部署与配置指南
4.1 五分钟快速启动:从零到看到画面
步骤1:准备EasyMedia服务
确保EasyMedia已启动,且以下配置生效(以config.yml为例):
http_flv:
enable: true
port: 8080
# 若需HTTPS,配置ssl证书路径
# ssl: true
# cert: ./cert.pem
# key: ./key.pem
api:
enable: true
port: 8080 # API端口与FLV端口可相同
# 若启用了鉴权,配置secret
# jwt_secret: "your-secret-key"
启动命令:./EasyMedia -c config.yml
步骤2:克隆并安装前端
git clone https://github.com/your-repo/vue-easymedia-template.git
cd vue-easymedia-template
npm install
步骤3:配置EasyMedia地址
修改.env.development和.env.production:
# 开发环境:代理到本地EasyMedia
VUE_APP_EM_HOST=localhost
VUE_APP_EM_PORT=8080
VUE_APP_EM_API_BASE=/api
# 生产环境:直连EasyMedia服务器
VUE_APP_EM_HOST=192.168.1.100
VUE_APP_EM_PORT=8080
VUE_APP_EM_API_BASE=http://192.168.1.100:8080/api
步骤4:启动开发服务
npm run serve
此时访问 http://localhost:8080,你应该看到四分屏界面。若无画面,请按以下顺序排查:
- 打开浏览器开发者工具 → Network标签页,过滤
flv,查看是否发出http://localhost:8080/live/cam1.flv请求; - 若请求404,检查EasyMedia的
http_flv.enable是否为true,端口是否匹配; - 若请求200但无画面,检查Console是否有
flv.js报错,大概率是EasyMedia未成功拉取RTSP流(用ffplay rtsp://...测试设备流是否正常); - 若出现跨域错误(CORS),检查EasyMedia配置中
http_flv.cors是否为true(EasyMedia 2.0+默认开启)。
步骤5:添加你的摄像头流
EasyMedia提供REST API添加流:
curl -X POST "http://localhost:8080/api/v1/streams" \
-H "Content-Type: application/json" \
-d '{"app":"live","stream":"my-cam","url":"rtsp://admin:password@192.168.1.200:554/stream1"}'
执行后刷新前端,新流将自动出现在/stream页面。
4.2 构建生产包:适配不同部署场景
npm run build 生成的dist/目录可直接部署到Nginx、Apache或任何静态服务器。关键配置如下:
Nginx配置示例(/etc/nginx/conf.d/easymedia.conf):
server {
listen 80;
server_name em.example.com;
location / {
root /var/www/easymedia/dist;
try_files $uri $uri/ /index.html;
}
# 代理EasyMedia API和FLV流,避免跨域
location /api/ {
proxy_pass http://192.168.1.100:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /live/ {
proxy_pass http://192.168.1.100:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 关键:启用长连接,避免FLV流频繁断开
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
为什么推荐Nginx代理而非直连?
- 安全:隐藏EasyMedia真实IP和端口;
- 性能:Nginx的proxy_buffering off可优化FLV流传输;
- 可靠:proxy_next_upstream error timeout http_500实现EasyMedia集群故障转移。
4.3 自定义主题与品牌化:三步替换Logo
品牌化改造极其简单,只需三步:
- 替换
public/logo.png为你的公司Logo(建议尺寸120×120px,PNG透明背景); - 替换
public/favicon.ico为你的网站图标(生成工具:https://realfavicongenerator.net/); - 修改
src/App.vue中的标题:
```vue
```
注意:
logo.png会被index.html中的<link rel="icon" href="<%= BASE_URL %>logo.png">引用,favicon.ico则由浏览器自动加载。无需修改任何代码,替换文件即可生效。
5. 常见问题与排查技巧实录
5.1 十六分屏下部分画面黑屏或卡顿
现象: 布局切换到16后,右下角4个窗口黑屏,控制台无报错。
排查思路:
- 首先确认是否所有流地址都正确:在Stream.vue中console.log(this.activeStreams),检查16个流的id是否都存在;
- 若地址正确,检查EasyMedia日志:tail -f logs/easymedia.log,搜索flv关键字,看是否有write flv header failed等错误;
- 最常见原因是EasyMedia服务器CPU或带宽瓶颈。16路1080p FLV流约占用80Mbps带宽和45% CPU(Intel i5-8500),用htop和iftop实时监控;
解决方案:
- 在EasyMedia配置中为高负载流启用transcode(转码):yaml streams: - app: live stream: cam1 url: rtsp://... transcode: true video_bitrate: 1000 # 降低码率至1Mbps video_width: 640 # 分辨率降至640x480
- 前端层面,在CusPlayer.vue中为低优先级流添加preload="none"属性,延迟加载:
```vue
<!-- ❌ 错误:key用index -->
<CusPlayer v-for="(stream, index) in activeStreams" :key="index" :src="stream.url" />
<!-- ✅ 正确:key用唯一id -->
<CusPlayer v-for="stream in activeStreams" :key="stream.id" :src="stream.url" />
`stream.id`必须是全局唯一标识(如`cam1`、`parking-a`),不能是数组索引。这是Vue列表渲染的黄金法则,也是多分屏场景下最易踩的坑。 ### 5.3 移动端Safari无法播放,显示“不支持的格式” **现象:** iOS Safari打开页面,video区域空白,控制台报`Unhandled Promise Rejection: NotSupportedError: The element has no supported sources.` **原因:** Safari对HTTP-FLV支持极差,且禁用Web Worker(`enableWorker: true`会失效)。 **解决方案:** 1. 在`CusPlayer.vue`中检测Safari: ```javascript methods: { initPlayer(url) { // Safari特殊处理 const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) if (isSafari) { // 回退到HLS方案(需EasyMedia开启HLS) const hlsUrl = url.replace('.flv', '.m3u8') this.player = hlsjs.createPlayer({ source: hlsUrl }) } else { // 正常flv.js流程 } } } ``` 2. 确保EasyMedia配置中`hls.enable: true`,并重启服务。 > 注意:HLS在Safari上延迟约8~12秒,无法满足实时监控需求。若业务强依赖低延迟,建议引导用户使用Chrome for iOS(支持FLV)。 ### 5.4 多人同时操作时,布局模式不同步 **现象:** A用户切换到九分屏,B用户仍是四分屏,两人看到的界面不一致。 **本质:** Vuex状态是前端局部状态,未与后端同步。 **扩展方案(非默认功能,需自行开发):** - 在EasyMedia中新增`/api/v1/layout`接口,存储全局布局模式; - 前端`store`中添加`syncLayout` action,定时(如30秒)GET该接口,更新`layoutMode`; - 或使用WebSocket监听布局变更事件(EasyMedia需扩展WebSocket服务)。 **简易替代方案:** 在`store/index.js`中添加持久化:
mutations: {
SET_LAYOUT_MODE(state, mode) {
state.layoutMode = mode
localStorage.setItem('em_layout_mode', mode) // 保存到本地存储
}
},
actions: {
initLayout({ commit }) {
const saved = localStorage.getItem('em_layout_mode')
if (saved) commit('SET_LAYOUT_MODE', Number(saved))
}
}
然后在`main.js`中:
store.dispatch('initLayout')
这样同一浏览器的多次访问能记住上次布局,虽非实时同步,但已覆盖80%场景。 ## 6. 进阶扩展与定制建议 ### 6.1 集成语音对讲(Two-way Audio) EasyMedia支持RTMP音频推流,前端可通过WebRTC实现双向语音。关键步骤: 1. EasyMedia配置中启用`rtmp_audio`; 2. 前端使用`adapter.js`(WebRTC适配库)和`simple-peer`建立P2P连接; 3. 将`
更多推荐


所有评论(0)