uniapp 实现直播功能的完整方案与实战指南

随着电商带货、在线教育、秀场社交等业务爆发式增长,直播已经从"锦上添花"变成"业务刚需"。对前端团队来说,用 uniapp 一套代码覆盖 H5、微信小程序、iOS、Android 是性价比最高的选择。但直播涉及音视频采集、编解码、实时传输、CDN 分发等复杂环节,从零自建难度极高。本文从方案选型到代码落地,完整讲清楚 uniapp 实现直播的工程化路径,并以即构科技的 ZEGO Express SDK 为例给出实战代码。

一、uniapp 直播的技术方案对比

uniapp 实现直播的技术方案一般有三种,如下:

  • 方案一:基于小程序原生组件 live-pusher / live-player;
  • 方案二:第三方 RTC SDK,如即构科技(ZEGO) 、腾讯云/阿里云等;
  • 方案三:WebRTC / HLS 自建。

选型阶段先搞清楚方案的差异,避免踩坑后再返工。

1.1 主流方案全景对比

对比维度 方案一:原生组件 live-pusher / live-player 方案二:第三方 RTC SDK / 即构科技(ZEGO) 、腾讯云等 方案三:WebRTC / HLS 自建
技术原理 基于小程序宿主能力,RTMP 推流 + FLV/RTMP 拉流 商用 RTC 引擎,底层自研协议 + CDN 原生 WebRTC + 自建信令 + CDN
支持平台 仅微信小程序、QQ 小程序、部分 App 端 全端(H5 / 小程序 / App / Web) H5 / App(需自行适配)
延迟 1-3 秒(RTMP) 200-400ms(RTC)、1-3 秒(CDN 直播) 300ms-2s(依实现而定)
连麦互动 仅支持主播-观众单向,不支持多人连麦 支持一对一、多人连麦、PK 连麦 需自行实现混流、信令
接入成本 低,直接用 uni 内置标签 中,引入 SDK + 调用 API 高,需音视频团队
并发能力 依赖小程序平台限制 商用级,百万级并发 取决于自建规模
美颜 / 滤镜 / 特效 基础美颜 SDK 内置 + 可扩展 AI 特效 需自行集成
鉴权与安全 需自行搭建 RTMP 鉴权 官方 Token 鉴权、房间隔离 自行设计鉴权体系
弱网优化 依赖基础协议,较弱 有专门抗弱网算法(NACK / FEC / 自适应码率) 取决于实现
费用 免费(仅用带宽) 按用量计费(分钟数 / 流量) 带宽 + 服务器 + 人力
上线周期 1-3 天 1-2 周 1-3 个月
适用场景 简单秀场直播、只在微信内使用 电商带货、在线教育、互动直播、连麦 PK 有特殊定制需求的大厂

1.2 选型决策流程

复制代码
只在微信小程序内使用?
      │
 ┌────┴────┐
 是         否
 │          │
需要连麦?  需要低延迟互动?
 │           │
是 / 否     是 / 否
 │           │
方案二 / 方案一   方案二(RTC) / 方案二(CDN)

结论:绝大多数"需要跨端 + 互动直播"的业务,方案二是最合理的选择。下面进入实战。

二、核心概念速览

  • 推流(Publish):主播将本地采集的音视频上传到服务器
  • 拉流(Play):观众从服务器下载并播放音视频流
  • 房间(Room):一次直播的逻辑容器,主播和观众通过 roomID 关联
  • RTC vs CDN 直播:RTC 走实时协议,延迟 400ms 内,支持双向互动;CDN 直播走 HLS/FLV,延迟 1-3 秒,成本更低
  • Token:登录房间的鉴权凭证,必须由服务端生成

三、实战:基于 ZEGO Express SDK 在 uniapp 中实现直播

ZEGO Express SDK 是即构科技(ZEGO)开发的一款实时音视频互动服务产品,能够为开发者提供便捷接入、高可靠、多平台互通的音视频服务。通过低至 200 ms 的端到端平均时延,业内领先的保障弱网质量的 QoS 策略,并结合强大的 3A 处理能力,完美支持一对多、多对多的实时音视频通话、直播、会议等场景。

本节给出一个可跑通的最小直播 Demo:主播开播 + 观众观看,覆盖 Web / 小程序 / App 三端。

3.1 环境准备

版本要求:

  • HBuilderX 3.8.0+
  • Vue 3(推荐)或 Vue 2
  • uniapp 最新稳定版

控制台申请:

  1. 登录 ZEGO 控制台创建项目,获取 AppIDServerSecret
  2. 开通"实时音视频"服务
  3. ServerSecret 只在服务端使用,不要放在客户端

3.2 引入 SDK

uniapp 的多端特性决定了 SDK 引入方式要按端区分:

复制代码
# Web 端
npm i zego-express-engine-webrtm

# 微信小程序端
npm i zego-express-engine-miniprogram

# App 端(原生插件)
# 在 HBuilderX 中:manifest.json → App 原生插件配置 → 选择 ZEGO Express 插件

建议封装一个统一的引擎入口,用条件编译分发:

复制代码
// utils/zego-engine.js
// #ifdef H5
import ZegoExpressEngine from 'zego-express-engine-webrtm'
// #endif

// #ifdef MP-WEIXIN
import ZegoExpressEngine from 'zego-express-engine-miniprogram'
// #endif

// #ifdef APP-PLUS
const ZegoExpressEngine = uni.requireNativePlugin('ZegoExpressUniPlugin')
// #endif

export default ZegoExpressEngine

3.3 权限与基础配置

manifest.json 中开启必要权限:

复制代码
{
  "app-plus": {
    "distribute": {
      "android": {
        "permissions": [
          "<uses-permission android:name=\"android.permission.CAMERA\"/>",
          "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
          "<uses-permission android:name=\"android.permission.INTERNET\"/>",
          "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>"
        ]
      },
      "ios": {
        "privacyDescription": {
          "NSCameraUsageDescription": "用于直播画面采集",
          "NSMicrophoneUsageDescription": "用于直播声音采集"
        }
      }
    }
  },
  "mp-weixin": {
    "requiredBackgroundModes": ["audio"],
    "requiredPrivateInfos": ["getLocation"]
  }
}

小程序后台还要在"服务器域名"中配置 ZEGO 的 wss 与 https 白名单。

3.4 封装 ZEGO Engine 单例

复制代码
// services/zegoService.js
import ZegoExpressEngine from '@/utils/zego-engine.js'

class ZegoService {
  constructor() {
    this.engine = null
    this.appID = 1234567890 // 替换为你的 AppID
    this.server = 'wss://webliveroom-api.zego.im/ws'
  }

  async init() {
    if (this.engine) return this.engine
    this.engine = new ZegoExpressEngine(this.appID, this.server)
    this._bindEvents()
    return this.engine
  }

  _bindEvents() {
    this.engine.on('roomStateUpdate', (roomID, state, errorCode) => {
      console.log('房间状态:', state, errorCode)
    })

    this.engine.on('roomStreamUpdate', (roomID, updateType, streamList) => {
      if (updateType === 'ADD') {
        uni.$emit('stream-add', streamList)
      } else {
        uni.$emit('stream-delete', streamList)
      }
    })

    this.engine.on('publisherStateUpdate', (result) => {
      console.log('推流状态:', result)
    })
  }

  async loginRoom(roomID, user, token) {
    return this.engine.loginRoom(roomID, token, {
      userID: user.userID,
      userName: user.userName
    })
  }

  logoutRoom(roomID) {
    return this.engine.logoutRoom(roomID)
  }

  destroy() {
    if (this.engine) {
      this.engine.destroyEngine?.()
      this.engine = null
    }
  }
}

export default new ZegoService()

3.5 主播端:创建直播间并推流

复制代码
<!-- pages/anchor/index.vue -->
<template>
  <view class="anchor-page">
    <!-- #ifdef H5 || MP-WEIXIN -->
    <view id="local-preview" class="video-box"></view>
    <!-- #endif -->

    <!-- #ifdef APP-PLUS -->
    <ZegoView
      class="video-box"
      :streamID="localStreamID"
      :viewMode="0"
    />
    <!-- #endif -->

    <view class="controls">
      <button @click="startLive" v-if="!isLiving">开始直播</button>
      <button @click="stopLive" v-else>结束直播</button>
    </view>
  </view>
</template>

<script setup>
import { ref, onUnmounted } from 'vue'
import zegoService from '@/services/zegoService.js'
import { fetchZegoToken } from '@/api/auth.js'

const isLiving = ref(false)
const localStreamID = ref('')
const roomID = 'live_room_001'
const user = { userID: 'anchor_001', userName: '主播A' }

async function startLive() {
  await zegoService.init()

  // 1. 从服务端获取 Token
  const token = await fetchZegoToken(user.userID)

  // 2. 登录房间
  await zegoService.loginRoom(roomID, user, token)

  // 3. 开启本地预览
  const streamID = `stream_${user.userID}_${Date.now()}`
  localStreamID.value = streamID

  // #ifdef H5 || MP-WEIXIN
  await zegoService.engine.startPreview('local-preview')
  // #endif

  // 4. 推流
  await zegoService.engine.startPublishingStream(streamID)
  isLiving.value = true
}

async function stopLive() {
  await zegoService.engine.stopPublishingStream()
  await zegoService.engine.stopPreview?.()
  await zegoService.logoutRoom(roomID)
  isLiving.value = false
}

onUnmounted(() => {
  if (isLiving.value) stopLive()
})
</script>

<style>
.video-box { width: 750rpx; height: 1000rpx; background: #000; }
</style>

3.6 观众端:进入直播间并拉流

复制代码
<!-- pages/audience/index.vue -->
<template>
  <view class="audience-page">
    <!-- #ifdef H5 || MP-WEIXIN -->
    <view id="remote-view" class="video-box"></view>
    <!-- #endif -->

    <!-- #ifdef APP-PLUS -->
    <ZegoView class="video-box" :streamID="remoteStreamID" />
    <!-- #endif -->

    <view class="status">{{ statusText }}</view>
  </view>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import zegoService from '@/services/zegoService.js'
import { fetchZegoToken } from '@/api/auth.js'

const remoteStreamID = ref('')
const statusText = ref('连接中...')
const roomID = 'live_room_001'
const user = { userID: 'viewer_' + Date.now(), userName: '观众' }

onMounted(async () => {
  await zegoService.init()
  const token = await fetchZegoToken(user.userID)
  await zegoService.loginRoom(roomID, user, token)

  uni.$on('stream-add', onStreamAdd)
  uni.$on('stream-delete', onStreamDelete)
})

async function onStreamAdd(streamList) {
  const stream = streamList[0]
  remoteStreamID.value = stream.streamID

  // #ifdef H5 || MP-WEIXIN
  await zegoService.engine.startPlayingStream(stream.streamID, 'remote-view')
  // #endif

  statusText.value = '直播中'
}

async function onStreamDelete(streamList) {
  await zegoService.engine.stopPlayingStream(streamList[0].streamID)
  statusText.value = '主播已离开'
}

onUnmounted(() => {
  uni.$off('stream-add', onStreamAdd)
  uni.$off('stream-delete', onStreamDelete)
  zegoService.logoutRoom(roomID)
})
</script>

3.7 互动能力扩展思路

  • 连麦 :观众调用 startPublishingStream,主播订阅 stream-add 事件拉对方流即可
  • 弹幕 / 礼物:引入 ZIM SDK 做消息通道,直播房间内广播 IM 消息
  • 美颜 / 滤镜:App 端通过 SDK 的美颜模块或第三方插件(FaceUnity、商汤)集成
  • 混流转 CDN :主播侧开启混流任务,观众端通过 <video> 或 live-player 播放 FLV / HLS 拉流地址,降低大并发成本

3.8 多端差异清单

差异点 Web 小程序 App
视频渲染容器 <view id> <view id> 原生 ZegoView 组件
权限申请时机 浏览器首次推流时 打开 Camera 组件时 首次初始化时
后台策略 Tab 切走自动暂停 小程序后台 5 分钟断连 需申请后台音频能力
包体积影响 约 300KB 约 400KB 原生插件需云打包

四、常见问题排查

1. 推流黑屏 / 无声

  • 先确认权限是否授予(摄像头、麦克风)
  • 检查 startPreview 传入的 DOM 节点是否已渲染
  • 小程序端检查是否在真机调试,模拟器不支持音视频采集

2. 小程序提示"不在以下合法域名列表中"

  • 微信后台 → 开发 → 服务器域名 → 加入 ZEGO 提供的 wss / https 域名

3. iOS App 端后台推流中断

  • manifest.json 中配置 UIBackgroundModes: audio
  • 提示用户回到前台,移动端系统级限制无法完全规避

4. 延迟高、画面卡顿

  • 降低分辨率(从 720p 降到 540p)
  • 打开自适应码率
  • 检查上行带宽,优先保证推流端网络

5. Token 过期导致踢出房间

  • 监听 roomTokenWillExpire 事件,提前 30 秒从服务端刷新 Token 并调用 renewToken

五、性能与上线建议

5.1 码率分辨率推荐

场景 推荐值
视频通话 分辨率 360 × 640、帧率 15 fps、码率 600 Kbps
直播 分辨率 540 x 960、帧率 24 fps、码率 1500 Kbps

5.2 安全与合规

  • Token 必须服务端签发 :客户端只拿到短时效 Token,ServerSecret 绝不外泄
  • 接入内容审核:开播鉴黄、实时截图审核,避免违规封号
  • 隐私合规:App 首次启动前完成隐私协议弹窗,再初始化 SDK

5.3 上线前 Checklist

  • AppID / Token 区分测试和正式环境
  • 三端真机各跑通一遍开播、观看、退出
  • 弱网场景测试(4G / 模拟 200ms 丢包 5%)
  • 监控告警:推流失败、拉流失败、房间登录失败
  • 配置内容审核和录制留存

六、结语

uniapp 做直播的核心挑战不在"能不能跑起来",而在"多端一致的稳定性"和"弱网下的体验"。选一个成熟的商用 RTC SDK,把复杂的音视频栈交给专业方案,团队就能专注在业务互动玩法上。

本文演示的最小直播链路已经能覆盖 Web、小程序、App 三端。下一步如果要做连麦 PK、直播带货、AI 数字人主播这类高阶玩法,可以在这个骨架上继续扩展。

完整的 API 参数、事件回调、错误码,建议对照 ZEGO 官方文档进行查阅。

相关推荐
wuxianda10302 小时前
Object-C/Swift/UniApp项目苹果商店上架3天极速解决方案汇报总结
ios·uni-app·objective-c·cocoa·苹果上架
WKK_3 小时前
uniapp 微信小程序使用TextEncoder,arrayBufferToBase64
微信小程序·小程序·uni-app
喜崽4 小时前
uniapp消息会话界面【消息组件一左一右】-01
uni-app
一渊之隔4 小时前
uniapp蓝牙搜索连接展示蓝牙设备包含信号显示
前端·网络·uni-app·bluetooth
喜崽5 小时前
uniapp消息会话界面【消息滚动到底部】-02
uni-app
Geek_Vison7 小时前
三款小程序容器技术选型对比分析——融媒新闻APP如何进行技术选型~
小程序·uni-app·app开发·finclip·小程序开发平台·跨端开发·小程序容器
DK1858383225217 小时前
知识付费会员小程序/付费圈子系统——课程兑换码+会员体系完整实战,开源运营级方案
小程序·uni-app·开源·php
陈龙龙的陈龙龙1 天前
uni-app中获取参数的几个方法
uni-app
果壳~1 天前
【Uniapp】【rich-text】富文本展示以及图片预览功能解决方案
前端·javascript·uni-app