uniapp微信小程序视频实时流+pc端预览方案

方案类型 技术实现 是否免费 优点 缺点 适用场景 延迟范围 开发复杂度
WebSocket+图片帧 定时拍照+Base64传输 ✅ 完全免费 无需服务器 纯前端实现 高延迟高流量 帧率极低 个人demo测试 超低频监控 500ms-2s ⭐⭐
RTMP推流 TRTC/即构SDK推流 ❌ 付费方案 (部分有免费额度) 专业直播方案 支持高并发 需流媒体服务器 SDK可能收费 中小型直播场景 1-3s ⭐⭐⭐⭐
开源WebRTC 自建coturn+mediasoup ✅ 开源免费 超低延迟 完全可控 需自建信令服务器 维护成本高 技术团队内网项目 200-500ms ⭐⭐⭐⭐⭐
商业WebRTC 腾讯TRTC/声网Agora ❌ 付费方案 (免费试用) 企业级服务 全球节点 按流量/时长计费 绑定厂商 商业视频通话应用 200-800ms ⭐⭐⭐⭐
HLS切片方案 FFmpeg切片+nginx ✅ 服务器可自建免费 兼容所有浏览器 支持CDN分发 延迟10秒以上 非实时录播场景 10s+ ⭐⭐⭐
UDP自定义协议 开发原生插件 ✅ 协议层免费 ❌ 人力成本高 完全自定义优化 需原生开发能力 过审风险 军工/工业特殊场景 200-500ms ⭐⭐⭐⭐⭐⭐

免费方案选择建议:

  1. 完全零成本​:

    • WebSocket图片帧(仅适合原型验证)
    • 开源WebRTC(需技术储备)
  2. 轻度付费​:

    • 腾讯云RTMP(免费10GB/月流量)
    • 阿里云直播(免费20GB/月流量)
  3. 企业级推荐​:

    • 声网Agora(首月赠送1万分钟)
    • 即构科技(首月免费)

下面我将介绍WebSocket+图片帧的实现方法:

WebSocket + 图片帧传输方案详解

该方案是 ​Uniapp微信小程序 + PC端视频实时预览 ​ 的一种 ​低成本、纯前端实现 ​ 的技术方案,适用于 ​低帧率、非严格实时​ 的场景。


🔹 方案原理

  1. 小程序端​:

    • 使用 <camera> 组件获取实时画面。
    • 通过 uni.createCameraContext().takePhoto() 定时拍照(如300ms/次)。
    • 将图片转为 Base64 格式,通过 WebSocket 发送到服务器。
  2. PC端​:

    • 建立 WebSocket 连接,接收 Base64 图片数据。
    • 使用 <img><canvas> 连续渲染图片,模拟视频流效果。

uniapp微信小程序端:

html 复制代码
<template>
  <view>
    <camera :device-position="devicePosition" :flash="flash" @error="error" style="width:100%; height:300px;"></camera>
    <button @click="startPushing">开始推流</button>
    <button @click="stopPushing">停止推流</button>
    <button @click="switchFlash">切换闪光灯</button>
    <button @click="flipCamera">翻转摄像头</button>
    <button style="font-size: 24rpx;">webscoket连接状态:{{pushState}}</button>
  </view>
</template>

<script>
export default {
  data() {
    return {
      pushState: "未连接",
      devicePosition: 'front',
      flash: 'off',
      timer: null,
      ws: null
    }
  },
  methods: {
    flipCamera() {
      this.devicePosition = this.devicePosition === 'back' ? 'front' : 'back';
    },

    switchFlash() {
      this.flash = this.flash === 'off' ? 'torch' : 'off';
    },

    startPushing() {
      // 如果已连接,则不再重复连接
      if (this.pushState === '连接成功') return;
      
      const randomToken = new Date().getTime();
      const url = 'ws://192.168.1.34:7097/liveWebSocket?linkInfo=a-' + randomToken;

      this.ws = uni.connectSocket({
        url,
        success: () => {
          console.log('正在尝试连接WebSocket', url);
        }
      });

      this.ws.onOpen(() => {
        uni.showToast({ title: '连接成功' });
        this.pushState = '连接成功';
        this.startCapture();
      });

      this.ws.onError((err) => {
        uni.showToast({ title: '连接异常', icon: 'none' });
        this.pushState = '连接异常';
        this.stopPushing();
      });

      this.ws.onClose(() => {
        this.pushState = '已关闭';
        this.stopPushing();
      });
    },

    stopPushing() {
      if (this.timer) {
        clearInterval(this.timer);
        this.timer = null;
      }

      if (this.ws) {
        this.ws.close();
        this.ws = null;
        this.pushState = "未连接";
      }
    },

    startCapture() {
      const context = uni.createCameraContext(this);
      // 调整为300ms间隔,减轻设备压力
      this.timer = setInterval(() => {
        context.takePhoto({
          quality: 'low',
          success: (res) => {
            this.processAndSendImage(res.tempImagePath);
          },
          fail: (err) => {
            console.error('拍照失败:', err);
          }
        });
      }, 300);
    },

    processAndSendImage(tempImagePath) {
      uni.getFileSystemManager().readFile({
        filePath: tempImagePath,
        encoding: 'base64',
        success: (res) => {
          const base64Image = `data:image/jpeg;base64,${res.data}`;
          if (this.ws) {
            this.ws.send({
              data: base64Image,
              success: () => {
                console.log('图片发送成功');
                this.cleanTempFile(tempImagePath);
              },
              fail: (err) => {
                console.warn('图片发送失败:', err);
              }
            });
          }
        },
        fail: (err) => {
          console.warn('读取图片失败:', err);
        }
      });
    },

    cleanTempFile(filePath) {
      setTimeout(() => {
        uni.getFileSystemManager().removeSavedFile({
          filePath,
          success: () => {
            console.log('临时文件已删除');
          },
          fail: (err) => {
            console.warn('删除临时文件失败:', err);
          }
        });
      }, 2000);
    },

    error(e) {
      console.error('摄像头错误:', e);
    }
  },

  onUnload() {
    this.stopPushing();
  }
}
</script>

pc端预览:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>管理员监控页面</title>
    <script src="./vue2.js"></script>
</head>

<body>
    <div id="app">
        <button @click="toSend">开始请求</button>
        <div v-if="videos && videos.length> 0" style="display: flex;">
            <div v-for="item in videos" :key="item.sessionId" style="margin: 10px;display: flex;flex-flow: column;"
                :id="item.sessionId">
                状态:{{item.status}}
                <img :src="item.videoSrc" style="width: 200px; height: 200px; border: 1px solid red;" alt="">
            </div>
        </div>
        <div style="background-color: green;margin: 20px 0;display: flex;width: 50%;word-wrap: break-word">
            接口数据:
            <div v-html="datas"></div>
        </div>

        <div style="background-color: red;width: 50%">
            视频列表:
            <template v-if="videos && videos.length> 0">
                <p v-for="item2 in videos">{{item2}}</p>
            </template>
        </div>
    </div>

    <script>
        new Vue({
            el: '#app',
            data: {
                datas: "",
                videos: [
                    // {
                    //     sessionId: '1',
                    //     status: '未连接',
                    //     videoSrc: '' //图片帧
                    // }
                ]
            },
            mounted() {

            },
            methods: {
                // 开始请求
                toSend() {
                    //断开所有webscoket连接
                    if (this.videos && this.videos.length > 0) {
                        this.videos.forEach(item => {
                            if (item.ws) {
                                item.ws.close();
                            }
                        });
                    }
                    this.datas = "";
                    this.videos = [];


                    // 请求直播人员列表
                    fetch('http://192.168.1.34:7097/liveWebStock/getAcceptList')
                        .then(response => response.json())
                        .then(data => {
                            if (data.code == 200) {
                                // console.log(6666, data.data); 
                                this.datas = data.data;
                                // 初始化每个视频流对象并建立 WebSocket 
                                this.videos = data.data.map(item => ({
                                    ...item,
                                    status: '未连接',
                                    videoSrc: '',
                                    ws: null
                                }));

                                // 建立 WebSocket 连接
                                this.videos.forEach(item => {
                                    this.initWebSocket(item.sessionId);
                                });
                            }
                        })
                        .catch(error => {
                            console.error('请求直播人员列表失败:', error);
                        });
                },
                initWebSocket(sessionId) {
                    if (!sessionId) return;
                    const wsUrl = `ws://192.168.1.34:7097/liveWebSocket?linkInfo=b-${sessionId}`;
                    const index = this.videos.findIndex(v => v.sessionId === sessionId);
                    if (index === -1) return;

                    const ws = new WebSocket(wsUrl);

                    ws.onopen = () => {
                        this.$set(this.videos, index, {
                            ...this.videos[index],
                            status: '已连接到服务器',
                            ws
                        });
                    };

                    //  处理接收到的数据
                    ws.onmessage = (event) => {
                        console.log("接收到base64图片", event);
                        // 假设是 base64 数据
                        const base64Data = event.data;
                        const url = base64Data;
                        this.$set(this.videos, index, {
                            ...this.videos[index],
                            videoSrc: url
                        });
                    };

                    ws.onerror = (error) => {
                        this.$set(this.videos, index, {
                            ...this.videos[index],
                            status: `WebSocket 错误: ${error.message}`
                        });
                        console.error(`WebSocket 错误 (${sessionId}):`, error);
                    };

                    ws.onclose = () => {
                        this.$set(this.videos, index, {
                            ...this.videos[index],
                            status: 'WebSocket 连接已关闭'
                        });
                    };


                }
            }
        });
    </script>
</body>

</html>
相关推荐
轩1154 小时前
实现仿中国婚博会微信小程序
微信小程序·小程序
武子康4 小时前
AI炼丹日志-28 - Audiblez 将你的电子书epub转换为音频mp3 做有声书
人工智能·爬虫·gpt·算法·机器学习·ai·音视频
八月林城7 小时前
echarts在uniapp中使用安卓真机运行时无法显示的问题
android·uni-app·echarts
哈贝#7 小时前
vue和uniapp聊天页面右侧滚动条自动到底部
javascript·vue.js·uni-app
知否技术8 小时前
2025微信小程序开发实战教程(一)
前端·微信小程序
iOS阿玮9 小时前
苹果2024透明报告看似更加严格的背后是利好!
uni-app·app·apple
喝牛奶的小蜜蜂10 小时前
个人小程序:不懂后台,如何做数据交互
前端·微信小程序·小程序·云开发
gomogomono10 小时前
【面试】音视频面试
音视频
Likeadust11 小时前
视频汇聚平台EasyCVR“明厨亮灶”方案筑牢旅游景区餐饮安全品质防线
网络·人工智能·音视频
2501_9189410511 小时前
旅游微信小程序制作指南
微信小程序·小程序·旅游