vue实现后端传输逐帧图像数据

在 Vue 中实现后端传输逐帧图像数据,通常涉及 WebSocket 或 Server-Sent Events (SSE) 技术。这里我将提供一个完整的解决方案,包括前后端实现。

方案一:使用 WebSocket (推荐)

前端实现 (Vue 3)

复制代码
<template>
  <div>
    <div>
      <button @click="startStream">开始接收视频流</button>
      <button @click="stopStream">停止接收</button>
    </div>
    <img :src="currentFrame" alt="实时视频帧" ref="videoFrame" />
  </div>
</template>

<script setup>
import { ref, onUnmounted } from 'vue'

const currentFrame = ref('')
const ws = ref(null)
const videoFrame = ref(null)
const isStreaming = ref(false)

// 开始接收视频流
const startStream = () => {
  if (isStreaming.value) return
  
  // 连接到 WebSocket 服务器
  ws.value = new WebSocket('ws://localhost:8080/ws/video')
  
  ws.value.onopen = () => {
    console.log('WebSocket 连接已建立')
    isStreaming.value = true
  }
  
  ws.value.onmessage = (event) => {
    if (typeof event.data === 'string') {
      const data = JSON.parse(event.data)
      if (data.type === 'frame') {
        // 将 base64 数据转换为图片
        currentFrame.value = `data:image/jpeg;base64,${data.frame}`
      }
    } else if (event.data instanceof Blob) {
      // 如果传输的是二进制数据
      const reader = new FileReader()
      reader.onload = () => {
        currentFrame.value = reader.result
      }
      reader.readAsDataURL(event.data)
    }
  }
  
  ws.value.onerror = (error) => {
    console.error('WebSocket 错误:', error)
    isStreaming.value = false
  }
  
  ws.value.onclose = () => {
    console.log('WebSocket 连接已关闭')
    isStreaming.value = false
  }
}

// 停止接收
const stopStream = () => {
  if (ws.value) {
    ws.value.close()
  }
  isStreaming.value = false
}

// 组件卸载时关闭连接
onUnmounted(() => {
  stopStream()
})
</script>

<style scoped>
img {
  max-width: 100%;
  border: 1px solid #ddd;
  margin-top: 20px;
}
</style>

方案二:使用 Server-Sent Events (SSE)

复制代码
<template>
  <div>
    <div>
      <button @click="startSSEStream">开始SSE流</button>
      <button @click="stopSSEStream">停止SSE流</button>
    </div>
    <img :src="currentFrame" alt="实时视频帧" />
  </div>
</template>

<script setup>
import { ref, onUnmounted } from 'vue'

const currentFrame = ref('')
const eventSource = ref(null)
const isSSEConnected = ref(false)

// 开始 SSE 连接
const startSSEStream = async () => {
  if (isSSEConnected.value) return
  
  try {
    // 首先请求流式连接
    const response = await fetch('http://localhost:3000/api/start-stream', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ clientId: 'vue-client' })
    })
    
    const data = await response.json()
    
    // 建立 SSE 连接
    eventSource.value = new EventSource(`http://localhost:3000/api/video-stream/${data.streamId}`)
    
    eventSource.value.onmessage = (event) => {
      const data = JSON.parse(event.data)
      if (data.type === 'frame') {
        currentFrame.value = `data:image/${data.format || 'jpeg'};base64,${data.frame}`
      }
    }
    
    eventSource.value.onerror = (error) => {
      console.error('SSE 连接错误:', error)
      stopSSEStream()
    }
    
    isSSEConnected.value = true
  } catch (error) {
    console.error('启动流失败:', error)
  }
}

// 停止 SSE 连接
const stopSSEStream = async () => {
  if (eventSource.value) {
    eventSource.value.close()
    eventSource.value = null
  }
  
  // 通知服务器停止发送
  await fetch('http://localhost:3000/api/stop-stream', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ clientId: 'vue-client' })
  })
  
  isSSEConnected.value = false
}

onUnmounted(() => {
  stopSSEStream()
})
</script>

后端实现示例 (Node.js + Express)

WebSocket 服务器

复制代码
// server/websocket-server.js
const WebSocket = require('ws')
const { createCanvas } = require('canvas')

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

wss.on('connection', (ws) => {
  console.log('新的 WebSocket 连接')
  
  // 模拟生成视频帧
  let frameInterval = null
  
  const sendFrame = () => {
    // 创建测试帧
    const canvas = createCanvas(640, 480)
    const ctx = canvas.getContext('2d')
    
    // 绘制动态内容
    ctx.fillStyle = `rgb(${Math.random()*255}, ${Math.random()*255}, ${Math.random()*255})`
    ctx.fillRect(0, 0, 640, 480)
    
    // 绘制文本
    ctx.fillStyle = 'white'
    ctx.font = '30px Arial'
    ctx.fillText(`时间: ${new Date().toLocaleTimeString()}`, 50, 100)
    
    // 转换为 base64
    const base64Image = canvas.toDataURL('image/jpeg').split(',')[1]
    
    // 发送帧数据
    const frameData = {
      type: 'frame',
      frame: base64Image,
      timestamp: Date.now(),
      width: 640,
      height: 480
    }
    
    ws.send(JSON.stringify(frameData))
  }
  
  // 每秒发送 30 帧
  frameInterval = setInterval(sendFrame, 1000 / 30)
  
  ws.on('message', (message) => {
    console.log('收到消息:', message.toString())
  })
  
  ws.on('close', () => {
    console.log('连接关闭')
    if (frameInterval) {
      clearInterval(frameInterval)
    }
  })
  
  ws.on('error', (error) => {
    console.error('WebSocket 错误:', error)
  })
})

console.log('WebSocket 服务器运行在 ws://localhost:8080')

使用 SSE 的后端

复制代码
// server/sse-server.js
const express = require('express')
const { createCanvas } = require('canvas')
const cors = require('cors')

const app = express()
app.use(cors())
app.use(express.json())

const clients = new Map()

// 启动视频流
app.post('/api/start-stream', (req, res) => {
  const { clientId } = req.body
  const streamId = `stream_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
  
  clients.set(streamId, {
    clientId,
    active: true
  })
  
  res.json({ streamId, message: 'Stream started' })
})

// SSE 视频流端点
app.get('/api/video-stream/:streamId', (req, res) => {
  const { streamId } = req.params
  
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Access-Control-Allow-Origin': '*'
  })
  
  const clientInfo = clients.get(streamId)
  if (!clientInfo) {
    res.write('data: {"error": "Invalid stream ID"}\n\n')
    res.end()
    return
  }
  
  let frameCount = 0
  const sendInterval = setInterval(() => {
    if (!clientInfo.active) {
      clearInterval(sendInterval)
      res.end()
      return
    }
    
    // 创建测试帧
    const canvas = createCanvas(640, 480)
    const ctx = canvas.getContext('2d')
    
    ctx.fillStyle = `rgb(${Math.random()*255}, ${Math.random()*255}, ${Math.random()*255})`
    ctx.fillRect(0, 0, 640, 480)
    
    ctx.fillStyle = 'white'
    ctx.font = '30px Arial'
    ctx.fillText(`帧: ${++frameCount}`, 50, 100)
    ctx.fillText(`流ID: ${streamId}`, 50, 150)
    
    const base64Image = canvas.toDataURL('image/jpeg').split(',')[1]
    
    const frameData = {
      type: 'frame',
      frame: base64Image,
      frameNumber: frameCount,
      timestamp: Date.now()
    }
    
    res.write(`data: ${JSON.stringify(frameData)}\n\n`)
  }, 1000 / 30) // 30 FPS
  
  req.on('close', () => {
    clearInterval(sendInterval)
    clientInfo.active = false
  })
})

// 停止流
app.post('/api/stop-stream', (req, res) => {
  const { clientId } = req.body
  
  for (const [streamId, info] of clients) {
    if (info.clientId === clientId) {
      clients.delete(streamId)
    }
  }
  
  res.json({ message: 'Stream stopped' })
})

app.listen(3000, () => {
  console.log('SSE 服务器运行在 http://localhost:3000')
})

高级功能:带控制的视频流

复制代码
<template>
  <div>
    <div class="controls">
      <button @click="connect" :disabled="isConnected">连接</button>
      <button @click="disconnect" :disabled="!isConnected">断开</button>
      
      <div class="fps-control">
        <label>帧率控制:</label>
        <input 
          type="range" 
          min="1" 
          max="60" 
          v-model="targetFPS"
          @change="adjustFPS"
        >
        <span>{{ targetFPS }} FPS</span>
      </div>
      
      <div class="quality-control">
        <label>质量:</label>
        <select v-model="quality" @change="adjustQuality">
          <option value="high">高质量</option>
          <option value="medium">中等</option>
          <option value="low">低质量</option>
        </select>
      </div>
    </div>
    
    <div class="stats">
      <p>连接状态: {{ connectionStatus }}</p>
      <p>帧率: {{ actualFPS.toFixed(1) }} FPS</p>
      <p>延迟: {{ latency }}ms</p>
      <p>已接收帧数: {{ frameCount }}</p>
    </div>
    
    <img 
      :src="currentFrame" 
      alt="视频流" 
      class="video-frame"
      :style="{ filter: imageFilter }"
    />
  </div>
</template>

<script setup>
import { ref, computed, onUnmounted } from 'vue'

const ws = ref(null)
const currentFrame = ref('')
const isConnected = ref(false)
const targetFPS = ref(30)
const quality = ref('medium')
const frameTimes = ref([])
const frameCount = ref(0)
const lastFrameTime = ref(0)
const imageFilter = ref('')

const connectionStatus = computed(() => {
  if (!ws.value) return '未连接'
  switch (ws.value.readyState) {
    case WebSocket.CONNECTING: return '连接中...'
    case WebSocket.OPEN: return '已连接'
    case WebSocket.CLOSING: return '关闭中...'
    case WebSocket.CLOSED: return '已断开'
    default: return '未知'
  }
})

const actualFPS = computed(() => {
  if (frameTimes.value.length < 2) return 0
  const recentTimes = frameTimes.value.slice(-10)
  const avgInterval = recentTimes.reduce((a, b) => a + b, 0) / (recentTimes.length - 1)
  return 1000 / avgInterval
})

const latency = ref(0)

const connect = () => {
  ws.value = new WebSocket(`ws://localhost:8080/ws/video?fps=${targetFPS.value}&quality=${quality.value}`)
  
  ws.value.onopen = () => {
    isConnected.value = true
    console.log('连接成功')
  }
  
  ws.value.onmessage = (event) => {
    const now = Date.now()
    const data = JSON.parse(event.data)
    
    if (data.type === 'frame') {
      // 计算延迟
      if (data.timestamp) {
        latency.value = now - data.timestamp
      }
      
      // 更新帧率统计
      if (lastFrameTime.value > 0) {
        const interval = now - lastFrameTime.value
        frameTimes.value.push(interval)
        if (frameTimes.value.length > 100) {
          frameTimes.value.shift()
        }
      }
      lastFrameTime.value = now
      
      // 显示帧
      currentFrame.value = `data:image/jpeg;base64,${data.frame}`
      frameCount.value++
    }
  }
  
  ws.value.onclose = () => {
    isConnected.value = false
    console.log('连接关闭')
  }
  
  ws.value.onerror = (error) => {
    console.error('连接错误:', error)
    isConnected.value = false
  }
}

const disconnect = () => {
  if (ws.value) {
    ws.value.close()
  }
}

const adjustFPS = () => {
  if (ws.value && ws.value.readyState === WebSocket.OPEN) {
    ws.value.send(JSON.stringify({
      type: 'control',
      action: 'setFPS',
      value: targetFPS.value
    }))
  }
}

const adjustQuality = () => {
  if (ws.value && ws.value.readyState === WebSocket.OPEN) {
    ws.value.send(JSON.stringify({
      type: 'control',
      action: 'setQuality',
      value: quality.value
    }))
  }
  
  // 应用视觉滤镜
  switch (quality.value) {
    case 'high':
      imageFilter.value = 'none'
      break
    case 'medium':
      imageFilter.value = 'blur(0.5px)'
      break
    case 'low':
      imageFilter.value = 'blur(1px) grayscale(20%)'
      break
  }
}

onUnmounted(() => {
  disconnect()
})
</script>

<style scoped>
.controls {
  margin-bottom: 20px;
  padding: 15px;
  background: #f5f5f5;
  border-radius: 8px;
}

.controls button {
  margin-right: 10px;
  padding: 8px 16px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.controls button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.fps-control, .quality-control {
  margin-top: 10px;
  display: flex;
  align-items: center;
  gap: 10px;
}

.stats {
  margin: 15px 0;
  padding: 10px;
  background: #e9ecef;
  border-radius: 4px;
  font-family: monospace;
}

.video-frame {
  max-width: 100%;
  border: 2px solid #333;
  border-radius: 8px;
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
</style>

使用建议

  1. 性能优化

    • 使用 requestAnimationFrame进行渲染同步

    • 实现帧缓冲区防止丢帧

    • 使用 Web Workers 处理解码

  2. 错误处理

    • 添加重连机制

    • 监控网络状态

    • 实现优雅降级

  3. 扩展功能

    • 添加视频录制功能

    • 实现帧捕获和保存

    • 添加视频滤镜和特效

    • 支持多摄像头切换

相关推荐
YGY顾n凡2 小时前
我开源了一个项目:一句话创造一个AI世界!
前端·后端·aigc
qq_12084093712 小时前
Three.js 工程向:动画循环与时间步进稳定性实践
前端·javascript
旷世奇才李先生2 小时前
React18\+TypeScript实战: Hooks封装与企业级组件开发
前端·javascript·typescript
午安~婉2 小时前
Electron(续4)利用AI辅助完成配置功能
前端·javascript·electron·应用打包与发布
tERS ERTS2 小时前
头歌答案--爬虫实战
java·前端·爬虫
当时只道寻常2 小时前
Vue3 集成 NProgress 进度条:从入门到精通
前端·vue.js
kyriewen2 小时前
React性能优化:从“卡成狗”到“丝般顺滑”的5个秘诀
前端·react.js·性能优化
米丘2 小时前
Vue 3.x 单文件组件(SFC)模板编译过程解析
前端·vue.js·编译原理