发布日期:2024年1月17日 | 作者:移动开发专家
一、直播应用架构概述
UniApp凭借其”一次开发,多端发布”的特性,成为构建跨平台直播应用的理想选择。本文将深入讲解如何基于UniApp开发一个功能完整的直播应用,涵盖推流、拉流、实时互动等核心功能。
技术架构图:
客户端(UniApp) → 信令服务器 → 媒体服务器 → CDN网络
↓ ↓ ↓ ↓
UI渲染 房间管理 流处理 内容分发
音视频采集 用户认证 转码录制 边缘加速
弹幕渲染 消息路由 连麦混流 全球分发
核心功能模块:
- 直播推流:基于live-pusher组件的实时视频采集与推送
- 直播观看:live-player组件的多协议流媒体播放
- 实时弹幕:WebSocket实现的即时消息系统
- 礼物系统:动画效果与实时交互的礼物功能
- 连麦互动:RTMP/WebRTC技术的实时音视频通话
二、开发环境搭建
1. 项目初始化
// 使用Vue3 + Vite创建项目
vue create -p dcloudio/uni-preset-vue#vite my-live-app
// 选择模板
? 请选择 uni-app 模板 (使用箭头键)
❯ 默认模板(TypeScript)
默认模板
自定义模板
// 安装直播相关依赖
npm install @dcloudio/uni-ui
npm install socket.io-client
npm install qiniu-js
2. 项目目录结构
src/
├── pages/
│ ├── index/
│ │ ├── index.vue # 直播列表页
│ │ └── components/
│ ├── live-room/
│ │ ├── live-room.vue # 直播间页
│ │ └── components/
│ └── profile/
│ └── profile.vue # 个人中心
├── static/
│ ├── images/
│ │ ├── gifts/ # 礼物图片
│ │ └── effects/ # 特效动画
│ └── icons/
├── store/
│ ├── live.js # 直播状态管理
│ └── user.js # 用户状态管理
├── utils/
│ ├── socket.js # WebSocket封装
│ ├── rtmp.js # RTMP工具类
│ └── gift-animation.js # 礼物动画
└── services/
├── live-service.js # 直播API服务
└── user-service.js # 用户API服务
3. 配置文件
// manifest.json 配置
{
"name": "LiveApp",
"appid": "__UNI__XXXXXX",
"description": "跨平台直播应用",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"modules": {
"LivePusher": {},
"LivePlayer": {}
},
"distribute": {
"android": {
"permissions": [
"",
""
]
},
"ios": {
"privacies": [
"NSCameraUsageDescription",
"NSMicrophoneUsageDescription"
]
}
}
}
}
三、直播核心功能实现
1. 直播推流功能
<template>
<view class="live-pusher-container">
<live-pusher
ref="livePusher"
:url="pushUrl"
mode="SD"
:muted="false"
:enable-camera="true"
:auto-focus="true"
:beauty="beautyLevel"
:whiteness="whitenessLevel"
:aspect="'3:4'"
@statechange="onPushStateChange"
@netstatus="onPushNetStatus"
class="live-pusher"
></live-pusher>
<view class="control-panel">
<button @tap="switchCamera">切换摄像头</button>
<slider :value="beautyLevel" @change="onBeautyChange" min="0" max="9" />
<button @tap="startPush" v-if="!isPushing">开始直播</button>
<button @tap="stopPush" v-else>结束直播</button>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
const pushUrl = ref('')
const isPushing = ref(false)
const beautyLevel = ref(5)
const whitenessLevel = ref(5)
const livePusher = ref(null)
// 生成推流地址
const generatePushUrl = async () => {
const roomId = await generateRoomId()
const timestamp = Date.now()
const token = await generatePushToken(roomId, timestamp)
pushUrl.value = `rtmp://your-push-server.com/live/${roomId}?token=${token}&t=${timestamp}`
}
// 开始推流
const startPush = () => {
if (!pushUrl.value) {
uni.showToast({ title: '推流地址生成失败', icon: 'none' })
return
}
livePusher.value.start()
isPushing.value = true
// 通知服务器直播开始
LiveService.notifyLiveStart({
roomId: getRoomIdFromUrl(pushUrl.value),
title: '我的直播间',
cover: 'default_cover.jpg'
})
}
// 推流状态监听
const onPushStateChange = (e) => {
console.log('推流状态:', e.detail.code, e.detail.message)
switch(e.detail.code) {
case 1001:
uni.showToast({ title: '连接成功', icon: 'success' })
break
case 1002:
uni.showToast({ title: '连接断开', icon: 'none' })
isPushing.value = false
break
case -1301:
uni.showToast({ title: '打开摄像头失败', icon: 'none' })
break
}
}
// 网络状态监听
const onPushNetStatus = (e) => {
const { videoBitrate, audioBitrate, netSpeed } = e.detail
console.log(`视频码率: ${videoBitrate}kbps, 音频码率: ${audioBitrate}kbps, 网速: ${netSpeed}kb/s`)
}
const switchCamera = () => {
livePusher.value.switchCamera()
}
const onBeautyChange = (e) => {
beautyLevel.value = e.detail.value
}
onMounted(() => {
generatePushUrl()
})
</script>
2. 直播观看功能
<template>
<view class="live-player-container">
<live-player
:src="playUrl"
mode="live"
autoplay
:muted="false"
:orientation="'vertical'"
:object-fit="'contain'"
@statechange="onPlayStateChange"
@fullscreenchange="onFullscreenChange"
class="live-player"
></live-player>
<view class="player-controls">
<button @tap="togglePlay">{{ isPlaying ? '暂停' : '播放' }}</button>
<button @tap="toggleFullscreen">全屏</button>
<button @tap="switchDefinition">清晰度</button>
</view>
<!-- 弹幕层 -->
<danmu-layer :messages="danmuList" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
const props = defineProps({
roomId: String
})
const playUrl = ref('')
const isPlaying = ref(true)
const danmuList = ref([])
const livePlayer = ref(null)
// 生成播放地址
const generatePlayUrl = () => {
// 支持多种格式
const formats = {
rtmp: `rtmp://your-play-server.com/live/${props.roomId}`,
flv: `https://your-cdn.com/live/${props.roomId}.flv`,
hls: `https://your-cdn.com/live/${props.roomId}.m3u8`
}
// 根据平台选择最优格式
playUrl.value = formats.hls // HLS兼容性最好
}
const togglePlay = () => {
if (isPlaying.value) {
livePlayer.value.pause()
} else {
livePlayer.value.resume()
}
isPlaying.value = !isPlaying.value
}
const toggleFullscreen = () => {
livePlayer.value.requestFullScreen({ direction: 0 })
}
const onPlayStateChange = (e) => {
console.log('播放状态:', e.detail.code)
switch(e.detail.code) {
case 2001:
uni.showToast({ title: '已经连接服务器', icon: 'none' })
break
case 2002:
uni.showToast({ title: '已经连接服务器,开始拉流', icon: 'none' })
break
case 2003:
uni.showToast({ title: '网络接收到首个视频数据包', icon: 'none' })
break
case 2004:
uni.showToast({ title: '视频播放开始', icon: 'success' })
break
case 2006:
uni.showToast({ title: '视频播放进度', icon: 'none' })
break
case -2301:
uni.showToast({ title: '网络断连,且经多次重连抢救无效', icon: 'none' })
break
}
}
onMounted(() => {
generatePlayUrl()
connectToRoom() // 连接直播间WebSocket
})
</script>
四、互动功能开发
1. 实时弹幕系统
// utils/socket.js - WebSocket封装
class LiveSocket {
constructor() {
this.socket = null
this.reconnectCount = 0
this.maxReconnect = 5
this.listeners = new Map()
}
connect(roomId, token) {
return new Promise((resolve, reject) => {
this.socket = uni.connectSocket({
url: `wss://your-websocket-server.com/ws?roomId=${roomId}&token=${token}`,
success: () => {
console.log('WebSocket连接成功')
this.setupEventListeners()
resolve()
},
fail: (err) => {
console.error('WebSocket连接失败:', err)
reject(err)
}
})
})
}
setupEventListeners() {
// 监听连接打开
this.socket.onOpen(() => {
console.log('WebSocket已连接')
this.reconnectCount = 0
})
// 监听消息接收
this.socket.onMessage((res) => {
const data = JSON.parse(res.data)
this.emit(data.type, data.payload)
})
// 监听连接关闭
this.socket.onClose(() => {
console.log('WebSocket连接关闭')
this.handleReconnect()
})
// 监听错误
this.socket.onError((err) => {
console.error('WebSocket错误:', err)
})
}
// 发送消息
send(type, payload) {
if (this.socket && this.socket.readyState === 1) {
this.socket.send({
data: JSON.stringify({ type, payload })
})
}
}
// 监听消息
on(type, callback) {
if (!this.listeners.has(type)) {
this.listeners.set(type, [])
}
this.listeners.get(type).push(callback)
}
// 触发消息
emit(type, data) {
const callbacks = this.listeners.get(type)
if (callbacks) {
callbacks.forEach(callback => callback(data))
}
}
// 重连机制
handleReconnect() {
if (this.reconnectCount {
this.connect()
}, 1000 * this.reconnectCount)
}
}
}
export default new LiveSocket()
2. 弹幕组件实现
<template>
<view class="danmu-layer">
<view
v-for="(item, index) in visibleMessages"
:key="item.id"
:class="['danmu-item', `danmu-${item.type}`]"
:style="getDanmuStyle(item, index)"
>
<image v-if="item.avatar" :src="item.avatar" class="avatar" />
<text class="username">{{ item.username }}:</text>
<text class="content">{{ item.content }}</text>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
const props = defineProps({
messages: Array
})
const visibleMessages = ref([])
const messagePool = ref([])
// 添加弹幕
const addDanmu = (message) => {
message.id = Date.now() + Math.random()
message.top = Math.random() * 70 + 10 // 随机顶部位置
message.speed = Math.random() * 2 + 1 // 随机速度
messagePool.value.push(message)
// 限制弹幕数量
if (messagePool.value.length > 50) {
messagePool.value.shift()
}
updateVisibleMessages()
}
// 更新可见弹幕
const updateVisibleMessages = () => {
visibleMessages.value = [...messagePool.value]
}
// 获取弹幕样式
const getDanmuStyle = (item, index) => {
return {
top: `${item.top}%`,
animationDuration: `${item.speed}s`,
zIndex: 1000 + index
}
}
// 监听弹幕消息
onMounted(() => {
LiveSocket.on('danmu', (data) => {
addDanmu(data)
})
LiveSocket.on('gift', (data) => {
addDanmu({
...data,
type: 'gift',
content: `送出了${data.giftName}`
})
})
})
onUnmounted(() => {
LiveSocket.off('danmu')
LiveSocket.off('gift')
})
</script>
3. 礼物系统与动画
// utils/gift-animation.js
class GiftAnimation {
constructor() {
this.animations = new Map()
this.initAnimations()
}
initAnimations() {
// 初始化礼物动画配置
this.animations.set('rose', {
duration: 3000,
effect: 'fadeInOut',
sound: 'gift_rose.mp3'
})
this.animations.set('rocket', {
duration: 5000,
effect: 'flyToMoon',
sound: 'gift_rocket.mp3'
})
}
// 播放礼物动画
play(giftType, options = {}) {
const config = this.animations.get(giftType)
if (!config) {
console.warn(`未找到礼物类型: ${giftType}的动画配置`)
return
}
this.createAnimationElement(giftType, config, options)
}
createAnimationElement(giftType, config, options) {
const animationId = `gift-${Date.now()}`
const animationNode = {
id: animationId,
type: giftType,
username: options.username,
giftName: options.giftName,
config: config
}
// 发送动画消息到页面
uni.$emit('giftAnimation', animationNode)
// 自动清理
setTimeout(() => {
uni.$emit('removeGiftAnimation', animationId)
}, config.duration)
}
}
export default new GiftAnimation()
五、性能优化与部署
1. 性能优化策略
// 直播流质量自适应
const adaptStreamQuality = (netStatus) => {
const { netSpeed, videoBitrate } = netStatus
if (netSpeed 2000) {
// 网络良好,切换为高清
switchToHighQuality()
} else {
// 网络一般,保持标清
switchToStandardQuality()
}
}
// 内存优化 - 清理历史消息
const cleanupMessageHistory = () => {
if (messagePool.value.length > 100) {
messagePool.value = messagePool.value.slice(-50)
}
}
// 图片懒加载优化
const lazyLoadImages = () => {
const images = document.querySelectorAll('img[data-src]')
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
observer.unobserve(img)
}
})
})
images.forEach(img => observer.observe(img))
}
2. 多平台适配
// 平台特定功能处理
const handlePlatformSpecific = () => {
// #ifdef APP-PLUS
setupAppSpecificFeatures()
// #endif
// #ifdef H5
setupH5SpecificFeatures()
// #endif
// #ifdef MP-WEIXIN
setupWechatSpecificFeatures()
// #endif
}
// 微信小程序特定配置
const setupWechatSpecificFeatures = () => {
// 微信小程序直播组件特殊处理
const livePlayerContext = uni.createLivePlayerContext('livePlayer')
const livePusherContext = uni.createLivePusherContext('livePusher')
// 微信小程序权限处理
uni.authorize({
scope: 'scope.camera',
success: () => {
console.log('摄像头权限已授权')
},
fail: () => {
uni.showModal({
title: '权限申请',
content: '需要摄像头权限才能进行直播',
success: (res) => {
if (res.confirm) {
uni.openSetting()
}
}
})
}
})
}
3. 发布部署配置
// package.json 构建脚本
{
"scripts": {
"build:app": "uni-build --app",
"build:h5": "uni-build --h5",
"build:mp-weixin": "uni-build --mp-weixin",
"build:all": "npm run build:app && npm run build:h5 && npm run build:mp-weixin"
}
}
// GitHub Actions 自动化部署
// .github/workflows/deploy.yml
name: Deploy Live App
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm install
- name: Build for all platforms
run: npm run build:all
- name: Deploy to CDN
uses: easingthemes/ssh-deploy@v2
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SOURCE: "dist/"
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
REMOTE_USER: ${{ secrets.REMOTE_USER }}
TARGET: ${{ secrets.REMOTE_TARGET }}

