关于vue2中对接海康摄像头以及直播流rtsp或rtmp,后台ffmpeg转码后通过ws实现

发布于:2025-08-08 ⋅ 阅读:(17) ⋅ 点赞:(0)

最近项目中需要对接摄像头监控,海康摄像头为rtsp流格式

有一个软件VLC media player,可以在线进行rtsp或者rtmp流播放,可用来测试流地址是否可用

功能实现思路为后台通过fmpeg把rtsp流进行转码,然后通过ws方式进行一帧一帧推送。(还有一种时后台通过fmpeg转为hls格式,但是这样会在项目中生产一张张图片,所以我们没有考虑)

首先后台我先使用node进行了一个测试dme,网上找了一个测试的rtmp地址:

rtmp://liteavapp.qcloud.com/live/liteavdemoplayerstreamid  //好像是个游戏直播

const { spawn } = require('child_process');
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 9999 });

wss.on('connection', (ws) => {
  console.log('WebSocket 客户端连接');

  // 关键修改1:使用完整FFmpeg路径(避免环境变量问题)
  const ffmpegPath = 'D:\\rj\\ffmpeg\\bin\\ffmpeg.exe'; // 自己电脑的ffmpeg地址,自行更换确保路径存在

  // 关键修改2:简化参数(移除可能冲突的选项)
  const args = [
    '-i', 'rtmp://liteavapp.qcloud.com/live/liteavdemoplayerstreamid', //网上的游戏直播地址
    '-f', 'mpegts',
    '-codec:v', 'mpeg1video',
    
    '-'
  ];

//   // rtsp的配置,和rtp略有区别
//  const args= [
//     '-rtsp_transport', 'tcp',       // 强制TCP传输
//     '-timeout', '5000000',          // 5秒超时
//     '-re',                          // 按原始速率读取
//     '-i', 'rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mp4',
//  '-f', 'mpegts',
//   '-codec:v', 'mpeg1video',       // JSMpeg兼容编码
//   '-b:v', '800k',                 // 视频比特率
//   '-r', '25',                     // 帧率
//   '-vf', 'scale=640:480',         // 分辨率缩放
//   '-preset', 'ultrafast',         // 最快编码预设
//   '-fflags', 'nobuffer',          // 减少缓冲
//   '-'
//   ]

  // 关键修改3:显式传递环境变量
  const env = { ...process.env, PATH: process.env.PATH }; // 继承当前环境

  const ffmpeg = spawn(ffmpegPath, args, { 
    env: env,
    stdio: ['ignore', 'pipe', 'pipe'] // 忽略stdin,捕获stdout/stderr
  });

  // 打印FFmpeg日志(调试用)
  ffmpeg.stderr.on('data', (data) => {
    console.log('[FFmpeg]', data.toString());
  });

  // 转发数据到WebSocket
  ffmpeg.stdout.on('data', (data) => {
    if (ws.readyState === ws.OPEN) {
      ws.send(data, { binary: true });
    }
  });

  ws.on('close', () => {
    ffmpeg.kill('SIGTERM');
  });
});

代码写完后通过node运行

前端在vue中采用   @cycjimmy/jsmpeg-player 或者 vue-jsmpeg-player,关于这点,因为vue-jsmpeg-player必须要求vue 2.6.12 以上,版本地低的话有问题,我这边采用@cycjimmy/jsmpeg-player

1.使用@cycjimmy/jsmpeg-player

npm install @cycjimmy/jsmpeg-player

npm下载安装后新建一个播放直播流的vue组件,代码如下

<template>
  <div class="video-player">
    <!-- 视频画布容器 -->
    <div class="video-container" ref="videoContainer">
      <canvas ref="videoCanvas" class="video-canvas"></canvas>
      
      <!-- 加载状态 -->
      <div v-if="isLoading" class="loading-overlay">
        <div class="spinner"></div>
        <p class="loading-text">加载中...</p>
      </div>
      
      <!-- 错误提示 -->
      <div v-if="hasError" class="error-overlay">
        <p class="error-text">无法加载视频流,请检查连接</p>
        <button @click="reloadPlayer" class="reload-btn">重新加载</button>
      </div>
    </div>
    
    <!-- 控制栏 -->
    <div class="controls-bar">
      <div class="controls-group">
        <!-- 播放/暂停按钮 -->
        <button 
          class="control-btn" 
          @click="togglePlay"
          :title="isPlaying ? '暂停' : '播放'"
        >
          <i class="fas" :class="isPlaying ? 'fa-pause' : 'fa-play'"></i>
        </button>
        
        <!-- 音量控制 -->
        <div class="volume-control">
          <button 
            class="control-btn volume-btn"
            @click="toggleMute"
            :title="isMuted ? '取消静音' : '静音'"
          >
            <i class="fas" :class="isMuted ? 'fa-volume-mute' : 'fa-volume-up'"></i>
          </button>
          <input
            type="range"
            min="0"
            max="100"
            v-model="volume"
            @input="setVolume"
            class="volume-slider"
            :title="`音量: ${volume}%`"
          >
        </div>
      </div>
      
      <div class="controls-group">
        <!-- 全屏按钮 -->
        <button 
          class="control-btn" 
          @click="toggleFullscreen"
          :title="isFullscreen ? '退出全屏' : '进入全屏'"
        >
          <i class="fas" :class="isFullscreen ? 'fa-compress' : 'fa-expand'"></i>
        </button>
      </div>
    </div>
  </div>
</template>

<script>
import JSMpeg from '@cycjimmy/jsmpeg-player';

export default {
  name: 'VideoPlayer',
  props: {
    // 视频流地址
    streamUrl: {
      type: String,
      required: true,
      default: 'ws://localhost:9999' //node后台1的ws地址
    },
    // 封面图
    poster: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      player: null,
      isPlaying: false,
      volume: 80,
      isMuted: false,
      isFullscreen: false,
      isLoading: true,
      hasError: false
    };
  },
  mounted() {
    this.initPlayer();
    this.addEventListeners();
  },
  beforeUnmount() {
    this.destroyPlayer();
    this.removeEventListeners();
  },
  methods: {
    // 初始化播放器
    initPlayer() {
      this.isLoading = true;
      this.hasError = false;
      
      try {
        this.player = new JSMpeg.Player(this.streamUrl, {
          canvas: this.$refs.videoCanvas,
          autoplay: false,
          loop: false,
          controls: false,
          poster: this.poster,
          decodeFirstFrame: true,
          volume: this.volume / 100,
          
          // 事件回调
          onPlay: () => {
            this.isPlaying = true;
            this.isLoading = false;
          },
          onPause: () => {
            this.isPlaying = false;
          },
          onEnded: () => {
            this.isPlaying = false;
          },
          onError: () => {
            this.hasError = true;
            this.isLoading = false;
            this.isPlaying = false;
          }
        });
      } catch (error) {
        console.error('播放器初始化失败:', error);
        this.hasError = true;
        this.isLoading = false;
      }
    },
    
    // 销毁播放器
    destroyPlayer() {
      if (this.player) {
        this.player.destroy();
        this.player = null;
      }
    },
    
    // 重新加载播放器
    reloadPlayer() {
      this.destroyPlayer();
      this.initPlayer();
    },
    
    // 切换播放/暂停
    togglePlay() {
      if (!this.player) return;
      
      if (this.isPlaying) {
        this.player.pause();
      } else {
        // 处理浏览器自动播放限制
        this.player.play().catch(err => {
          console.warn('自动播放失败,需要用户交互:', err);
          this.showPlayPrompt();
        });
      }
    },
    
    // 显示播放提示(用于自动播放限制)
    showPlayPrompt() {
      const container = this.$refs.videoContainer;
      const prompt = document.createElement('div');
      prompt.className = 'play-prompt';
      prompt.innerHTML = '<i class="fas fa-play"></i><p>点击播放</p>';
      container.appendChild(prompt);
      
      prompt.addEventListener('click', () => {
        this.player.play();
        container.removeChild(prompt);
      }, { once: true });
    },
    
    // 切换静音
    toggleMute() {
      if (!this.player) return;
      
      this.isMuted = !this.isMuted;
      this.player.volume = this.isMuted ? 0 : this.volume / 100;
    },
    
    // 设置音量
    setVolume() {
      if (!this.player) return;
      
      this.isMuted = this.volume === 0;
      this.player.volume = this.volume / 100;
    },
    
    // 切换全屏
    toggleFullscreen() {
      const container = this.$refs.videoContainer;
      
      if (!document.fullscreenElement) {
        container.requestFullscreen().catch(err => {
          console.error(`全屏错误: ${err.message}`);
        });
        this.isFullscreen = true;
      } else {
        if (document.exitFullscreen) {
          document.exitFullscreen();
          this.isFullscreen = false;
        }
      }
    },
    
    // 监听全屏状态变化
    handleFullscreenChange() {
      this.isFullscreen = !!document.fullscreenElement;
    },
    
    // 添加事件监听器
    addEventListeners() {
      document.addEventListener('fullscreenchange', this.handleFullscreenChange);
    },
    
    // 移除事件监听器
    removeEventListeners() {
      document.removeEventListener('fullscreenchange', this.handleFullscreenChange);
    }
  }
};
</script>

<style scoped>
.video-player {
  position: relative;
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.video-container {
  position: relative;
  width: 100%;
  background-color: #000;
  aspect-ratio: 16 / 9; /* 保持视频比例 */
}

.video-canvas {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* 加载状态 */
.loading-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: rgba(0, 0, 0, 0.5);
  color: white;
  z-index: 10;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid rgba(255, 255, 255, 0.3);
  border-radius: 50%;
  border-top: 4px solid white;
  animation: spin 1s linear infinite;
  margin-bottom: 16px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.loading-text {
  font-size: 16px;
  font-weight: 500;
}

/* 错误提示 */
.error-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: rgba(0, 0, 0, 0.7);
  color: white;
  z-index: 10;
  padding: 20px;
  text-align: center;
}

.error-text {
  font-size: 16px;
  margin-bottom: 20px;
  max-width: 400px;
}

.reload-btn {
  background-color: #42b983;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s;
}

.reload-btn:hover {
  background-color: #359e75;
}

/* 播放提示 */
::v-deep .play-prompt {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: rgba(0, 0, 0, 0.5);
  color: white;
  z-index: 9;
  cursor: pointer;
}

::v-deep .play-prompt i {
  font-size: 48px;
  margin-bottom: 16px;
  transition: transform 0.2s;
}

::v-deep .play-prompt:hover i {
  transform: scale(1.1);
}

/* 控制栏 */
.controls-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 20px;
  background-color: #1a1a1a;
  color: white;
}

.controls-group {
  display: flex;
  align-items: center;
  gap: 16px;
}

.control-btn {
  background: none;
  border: none;
  color: white;
  font-size: 18px;
  cursor: pointer;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.2s;
}

.control-btn:hover {
  background-color: rgba(255, 255, 255, 0.15);
}

/* 音量控制 */
.volume-control {
  display: flex;
  align-items: center;
  gap: 8px;
}

.volume-slider {
  width: 100px;
  height: 4px;
  -webkit-appearance: none;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 2px;
  outline: none;
}

.volume-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: white;
  cursor: pointer;
  transition: transform 0.1s;
}

.volume-slider::-webkit-slider-thumb:hover {
  transform: scale(1.2);
}

/* 响应式调整 */
@media (max-width: 768px) {
  .controls-bar {
    padding: 8px 12px;
  }
  
  .controls-group {
    gap: 8px;
  }
  
  .control-btn {
    font-size: 16px;
    width: 32px;
    height: 32px;
  }
  
  .volume-slider {
    width: 80px;
  }
}
</style>

然后在父组件中引入使用就可以了。

2.使用 vue-jsmpeg-player

npm i vue-jsmpeg-player@1.1.0-beta

安装后在main.js全局引入:


import JSMpegPlayer from 'vue-jsmpeg-player';
import 'vue-jsmpeg-player/dist/jsmpeg-player.css';

Vue.use(JSMpegPlayer)

然后新建vue组件:

<template>
  <div>
    <jsmpeg-player :url="url" />
  </div>
</template>

<script>
export default {
  components: {},

  data() {
    return {
      //后台转发的ws地址
      url: "ws://10.10.10.113:9999", //后台ws地址
    };
  },
  computed: {},

  mounted() {
    // jsmpeg-player组件的使用说明
    //     url	string	视频流地址(推荐websocket,实际上jsmpeg.js原生也支持http方式,但没有经过测试)
    // title	string	播放器标题
    // no-signal-text	string	无信号时的显示文本
    // options	object	jsmpeg原生选项,直接透传,详见下表
    // closeable	boolean	是否可关闭(单击关闭按钮,仅抛出事件)
    // in-background	boolean	是否处于后台,如el-tabs的切换,路由的切换等,支持.sync修饰符
    // show-duration	boolean	是否现实持续播放时间
    // default-mute	boolean	默认静音
    // with-toolbar	boolean	是否需要工具栏
    // loading-text	boolean	加载时的文本,默认为:拼命加载中
  },
  beforeDestroy() {},
  methods: {},
};
</script>

<style lang="scss">
</style>


网站公告

今日签到

点亮在社区的每一天
去签到