Web项目实现播放rtsp视频流

由于浏览器并不支持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);
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值