由于浏览器并不支持rtsp协议的视频,所以需要额外处理
这里以vue + node 为例子实现
安装依赖
前后端项目安装rtsp-relay
npm i rtsp-relay
系统安装ffmpeg
地址:https://ffmpeg.org/download.html
前端组件
创建rtsp视频流播放组件
<template>
<div class="container">
<div
id="video-player-content"
v-loading="loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
style="width: 100%; height: 100%"
>
<canvas ref="canvas" id="canvas"
style="width: 100%; height: 100%"
></canvas>
</div>
<!-- <div class="play-icon" @click.stop="initPlayer" v-show="!playing">
<i class="el-icon-video-play"></i>
</div> -->
<!-- <div class="loading-icon" v-show="!loading">
<i class="loading"></i>
</div> -->
</div>
</template>
<script>
import { loadPlayer } from 'rtsp-relay/browser';
export default {
name: "WidgetVideoRtsp",
components: {},
props: {
value: Object,
},
data() {
return {
options: {},
lastFrameTime: performance.now(),
playing: false,
loading: false,
};
},
watch: {
value: {
handler(val) {
this.options = val;
console.log(val)
// this.playing = false;
},
deep: true
},
// 监听value的rtspUrl属性
'options.rtspUrl':{
async handler(newUrl, oldUrl) {
console.log("[RTSP] URL变更:", newUrl);
// 如果新旧URL相同,不进行处理
if (newUrl === oldUrl) {
return;
}
try {
// 清理旧的播放器资源
if (this.player) {
window.player = this.player;
console.log('[RTSP] 清理旧播放器资源');
// if (this.player._ws) {
// this.player._ws.close();
// }
this.player.destroy();
this.player = null;
// 重新创建canvas元素
const oldCanvas = this.$refs.canvas;
const parent = document.getElementById('video-player-content');
const newCanvas = document.createElement('canvas');
newCanvas.id = 'canvas';
newCanvas.style.width = '100%';
newCanvas.style.height = '100%';
console.log(oldCanvas, newCanvas)
if(oldCanvas){
parent.removeChild(oldCanvas);
}
parent.appendChild(newCanvas);
this.$refs.canvas = newCanvas;
await new Promise(resolve => setTimeout(resolve, 1500)); // 等待资源释放
}
// 只有在有新URL时才初始化新播放器
if (newUrl) {
console.log('[RTSP] 初始化新播放器');
await this.initPlayer();
}
} catch (error) {
console.error('[RTSP] URL变更处理错误:', error);
this.$message.error('视频流切换失败: ' + error.message);
}
},
immediate: true
},
},
mounted() {
this.options = this.value;
console.log(this.options);
// this.initPlayer();
},
beforeDestroy() {
// 清理资源
if (this.player) {
// if (this.player._ws) {
// this.player._ws.close();
// }
// if (this.configTimeout) {
// clearTimeout(this.configTimeout);
// }
// if (this.heartbeatInterval) {
// clearInterval(this.heartbeatInterval);
// }
this.player.destroy();
}
},
methods: {
async initPlayer() {
if (!this.options.rtspUrl) {
console.log('[RTSP] 无效的RTSP地址');
this.$message.warning('请输入RTSP地址');
return;
}
// 验证RTSP地址格式
const rtspRegex = /^rtsp:\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?$/;
if (!rtspRegex.test(this.options.rtspUrl)) {
console.log('[RTSP] RTSP地址格式不正确');
this.$message.error('RTSP地址格式不正确,请确保以rtsp://开头');
return;
}
// 确保在初始化新播放器前清理旧的播放器
// if (this.player) {
// console.log('[RTSP] 清理旧播放器实例');
// // if (this.player._ws) {
// // this.player._ws.close();
// // }
// this.player.destroy();
// this.player = null;
// // this.playing = false;
// }
console.log('[RTSP] 开始初始化播放器');
console.log('[RTSP] RTSP地址:', this.options.rtspUrl);
const wsUrl = `ws://${window.location.hostname}:3001/rtsp?url=${encodeURIComponent(this.options.rtspUrl)}`;
console.log('[RTSP Debug] WebSocket地址:', wsUrl);
console.log('[RTSP Debug] 当前Canvas元素:', this.$refs.canvas);
try {
this.loading = true;
// 确保在创建新实例前完全释放内存和WebSocket连接
await new Promise(resolve => setTimeout(resolve, 1000));
// 重置状态和清理DOM引用
this.playing = false;
this.player = null;
if (this.$refs.canvas) {
this.$refs.canvas.width = 0;
this.$refs.canvas.height = 0;
}
// 初始化新播放器
console.log('[RTSP Debug] 开始调用loadPlayer...');
this.playing = true;
this.player = await loadPlayer({
url: wsUrl,
canvas: this.$refs.canvas,
autoplay: true,
//disconnectThreshold: 3000,
transport: 'tcp',
loop:false,
// retry: {
// retries: 1,
// factor: 1.5,
// minTimeout: 2000,
// maxTimeout: 10000
// },
videoBufferSize: 2048 * 1024,
audioBufferSize: 512 * 1024,
timeout: 10000,
onDisconnect: () => {
console.log('[RTSP] 连接中断');
this.$message.warning('视频流连接中断');
},
onError: (err) => {
console.error('[RTSP] 播放错误:', err);
this.$message.error('视频流播放错误: ' + err.message);
},
onPlay: () => console.log('[RTSP] 开始播放'),
onPause: () => console.log('[RTSP] 暂停播放')
});
// 确保播放器配置正确设置
// this.player.muted = true;
// await this.player.play();
console.log('[RTSP Debug] loadPlayer初始化完成,player实例:', this.player);
} catch (error) {
console.error('[RTSP] 播放器初始化失败:', error);
this.$message.error('视频流连接失败: ' + error.message);
// 确保在错误时清理资源并重置状态
if (this.player) {
// if (this.player._ws) {
// this.player._ws.close();
// }
this.player.destroy();
this.player = null;
}
this.playing = false;
} finally {
this.loading = false;
}
},
// play() {
// if (this.player) {
// this.player.play();
// }
// }
}
};
</script>
<style scoped lang="scss">
.container {
width: 100%;
height: 100%;
position: relative;
background: #000;
border: 2px solid #409EFF;
}
video {
width: 100%;
height: 100%;
display: block;
}
canvas {
//width: 100% !important;
// height: 100%!important;
// background: #000;
}
.play-icon{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 120px;
cursor: pointer;
z-index: 10000;
}
</style>
node服务端
通过node进行转码
const express = require('express');
const cors = require('cors');
const app = express();
// 配置CORS中间件
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// 处理预检请求
// 增强预检请求处理
app.options('/rtsp', cors({
origin: '*',
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
preflightContinue: false,
optionsSuccessStatus: 204
}));
require('express-ws')(app);
const { proxy } = require('rtsp-relay')(app);
app.ws('/rtsp', (ws, req) => {
console.log('[RTSP] 收到新的WebSocket连接请求');
console.log('[RTSP] 请求URL:', req.url);
// 解析URL参数
const params = new URLSearchParams(req.url.split('?')[1] || '');
const rtspUrl = params.get('url');
console.log('[RTSP] 解析到的RTSP地址:', rtspUrl);
// 验证RTSP地址
if (!rtspUrl) {
console.error('[RTSP] 错误: 未提供RTSP地址');
ws.close(1008, 'RTSP地址不能为空');
return;
}
try {
const url = decodeURIComponent(rtspUrl);
console.log('[RTSP] 解码后的RTSP地址:', url);
// 验证RTSP协议和URL格式
if (!url.startsWith('rtsp://')) {
console.error('[RTSP] 错误: 无效的RTSP协议');
ws.close(1008, '无效的RTSP协议');
return;
}
// 验证URL格式
try {
new URL(url);
} catch (error) {
console.error('[RTSP] 错误: 无效的URL格式:', error.message);
ws.close(1008, '无效的URL格式');
return;
}
console.log('[RTSP] 开始代理RTSP流...');
console.log('[FFmpeg] 启动参数:', JSON.stringify({
input: url,
transport: 'tcp',
output: {
videoCodec: 'libx264',
preset: 'ultrafast',
tune: 'zerolatency',
scale: '1280:720',
framerate: 30,
format: 'rtsp'
}
}, null, 2));
proxy({
url: url,
verbose: true,
transport: 'tcp',
additionalFlags: [
// '-c:v', 'libx264',
'-c:v', 'mpeg1video', // 强制MPEG1编码
'-c:a', 'mp2', // 音频编码
'-preset', 'ultrafast',
'-tune', 'zerolatency',
// '-vf', 'scale=1280:720',
// '-vf', 'scale=1280:-2', // 确保分辨率宽度为偶数
'-r', '30',
'-timeout', '5000000',
'-loglevel', 'debug',
// '-f', 'mpegts' // 指定输出格式
],
onFfmpegStart: (command) => {
console.log('[FFmpeg] 启动命令:', command);
},
onFfmpegLog: (log) => {
console.log('[FFmpeg]', log);
// 检测关键错误信息
const errorPatterns = [
{ pattern: /Connection refused/i, message: '连接被拒绝' },
{ pattern: /Connection timed out/i, message: '连接超时' },
{ pattern: /Invalid data found/i, message: '无效的数据流' },
{ pattern: /Error opening input/i, message: '无法打开输入源' },
{ pattern: /Server returned 401/i, message: '认证失败' },
{ pattern: /Server returned 404/i, message: '资源不存在' }
];
for (const { pattern, message } of errorPatterns) {
if (pattern.test(log)) {
console.error(`[FFmpeg] ${message}:`, log);
ws.close(1011, message);
return;
}
}
},
onConnection: (connection) => {
console.log('[FFmpeg] 新建连接 进程ID:', connection.pid);
// 添加启动成功标志
let isStarted = false;
connection.on('exit', (code, signal) => {
console.log(`[FFmpeg] 进程退出 code:${code} signal:${signal}`);
if (code !== 0) {
console.error('[FFmpeg] 进程异常退出');
ws.close(1011, `FFmpeg进程异常退出: ${code}`);
}
});
connection.on('error', (err) => {
console.error('[FFmpeg] 进程错误:', err);
});
// 设置连接超时检测
const connectionTimeout = setTimeout(() => {
if (!isStarted) {
console.error('[FFmpeg] 连接超时');
ws.close(1011, '视频流连接超时');
connection.kill();
}
}, 10000);
connection.on('output', (data) => {
const output = data.toString();
console.log('[FFmpeg] 输出:', output);
// 检测FFmpeg启动成功标志
if (output.includes('Stream mapping:') || output.includes('Output #0')) {
isStarted = true;
clearTimeout(connectionTimeout);
console.log('[FFmpeg] 流媒体转发已启动');
}
// 检测常见错误
if (output.includes('Connection refused') || output.includes('Connection timed out')) {
console.error('[FFmpeg] 连接失败:', output);
ws.close(1011, '无法连接到视频源');
} else if (output.includes('Error') || output.includes('Failed')) {
console.error('[FFmpeg] 处理错误:', output);
}
});
let frameCount = 0;
let lastFrameTime = Date.now();
let frameInterval = 0;
connection.on('frame', (frame) => {
frameCount++;
const currentTime = Date.now();
frameInterval = currentTime - lastFrameTime;
lastFrameTime = currentTime;
// 每秒记录一次帧率统计
if (frameCount % 30 === 0) {
const fps = 1000 / frameInterval;
console.log('[FFmpeg] 当前帧率:', fps.toFixed(2), 'fps');
if (fps < 10) {
console.warn('[FFmpeg] 警告: 帧率过低');
}
}
console.log('[FFmpeg] 接收到视频帧 类型:', frame.type, '大小:', frame.data.length, '帧间隔:', frameInterval, 'ms');
});
}})(ws);
console.log('[RTSP] 代理设置完成');
} catch (error) {
console.error('[RTSP] 代理设置失败:', error);
ws.close(1011, '代理设置失败');
}
});
app.listen(3001, () => {
console.log('[Server] 服务器启动在端口 3001');
});
// 错误处理
process.on('uncaughtException', (err) => {
console.error('[Error] 未捕获的异常:', err);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('[Error] 未处理的Promise拒绝:', reason);
});