UniApp 移动端直播流播放实战:打造符合鸿蒙设计风格的播放器
在移动互联网时代,直播已经成为一种主流的内容形式。本文将详细介绍如何使用 UniApp 框架开发一个优雅的直播流播放器,并融入鸿蒙系统的设计理念,实现一个既美观又实用的播放功能。
设计理念
在开发直播播放器之前,我们需要深入理解鸿蒙系统的设计哲学。鸿蒙系统强调"自然、统一、高效"的设计原则,这些特点将贯穿我们的整个功能实现过程:
- 自然:播放器的控制要符合用户的使用习惯
- 统一:视觉元素要保持一致性,符合系统设计规范
- 高效:确保流畅的播放体验和快速的加载速度
- 优雅:在各种状态下都保持良好的视觉表现
技术方案
在实现直播流播放功能时,我们需要考虑以下几个关键点:
- 播放器选型:使用原生组件
live-player
- 协议支持:RTMP、FLV、HLS 等主流直播协议
- 状态管理:加载中、播放中、错误等状态处理
- 性能优化:控制内存占用,优化渲染性能
- 交互设计:手势控制、全屏切换等
组件实现
1. 直播播放器组件
首先实现一个基础的直播播放器组件:
vue
<!-- components/harmony-live-player/harmony-live-player.vue -->
<template>
<view class="harmony-live-player" :class="{ 'fullscreen': isFullscreen }">
<live-player
class="player"
:src="src"
:mode="mode"
:autoplay="autoplay"
:muted="isMuted"
:orientation="orientation"
:object-fit="objectFit"
@statechange="handleStateChange"
@netstatus="handleNetStatus"
@error="handleError"
ref="livePlayer"
>
<!-- 播放器控制层 -->
<view class="player-controls" :class="{ 'controls-visible': showControls }">
<view class="top-bar">
<view class="left-area">
<text class="title">{{ title }}</text>
</view>
<view class="right-area">
<text class="online-count" v-if="onlineCount">
{{ formatNumber(onlineCount) }}人观看
</text>
</view>
</view>
<view class="bottom-bar">
<view class="control-btn" @click="togglePlay">
<text class="iconfont" :class="isPlaying ? 'icon-pause' : 'icon-play'"></text>
</view>
<view class="control-btn" @click="toggleMute">
<text class="iconfont" :class="isMuted ? 'icon-mute' : 'icon-volume'"></text>
</view>
<view class="quality-switch" v-if="qualities.length > 1">
<text
v-for="item in qualities"
:key="item.value"
:class="{ 'active': currentQuality === item.value }"
@click="switchQuality(item.value)"
>
{{ item.label }}
</text>
</view>
<view class="control-btn" @click="toggleFullscreen">
<text class="iconfont" :class="isFullscreen ? 'icon-fullscreen-exit' : 'icon-fullscreen'"></text>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-layer" v-if="isLoading">
<view class="loading-animation"></view>
<text class="loading-text">直播加载中...</text>
</view>
<!-- 错误状态 -->
<view class="error-layer" v-if="hasError">
<text class="error-icon iconfont icon-error"></text>
<text class="error-text">{{ errorMessage }}</text>
<view class="retry-btn" @click="retry">重试</view>
</view>
</live-player>
</view>
</template>
<script>
const CONTROL_HIDE_TIMEOUT = 3000 // 控制栏自动隐藏时间
export default {
name: 'HarmonyLivePlayer',
props: {
src: {
type: String,
required: true
},
title: {
type: String,
default: ''
},
mode: {
type: String,
default: 'RTC' // RTC模式延迟更低
},
autoplay: {
type: Boolean,
default: true
},
qualities: {
type: Array,
default: () => []
},
onlineCount: {
type: Number,
default: 0
}
},
data() {
return {
isPlaying: false,
isMuted: false,
isLoading: true,
hasError: false,
errorMessage: '',
isFullscreen: false,
showControls: true,
controlTimer: null,
currentQuality: '',
objectFit: 'contain',
orientation: 'vertical',
netStatus: {}
}
},
watch: {
src: {
handler(newVal) {
if (newVal) {
this.hasError = false
this.errorMessage = ''
this.isLoading = true
this.startPlay()
}
},
immediate: true
}
},
mounted() {
// 初始化播放器
this.initPlayer()
// 监听触摸事件以显示/隐藏控制栏
this.initTouchListeners()
},
beforeDestroy() {
this.clearControlTimer()
this.stopPlay()
},
methods: {
initPlayer() {
// 设置默认清晰度
if (this.qualities.length > 0) {
this.currentQuality = this.qualities[0].value
}
// 初始化播放状态
if (this.autoplay) {
this.startPlay()
}
},
initTouchListeners() {
const player = this.$refs.livePlayer
if (!player) return
player.$el.addEventListener('touchstart', () => {
this.toggleControls()
})
},
startPlay() {
const player = this.$refs.livePlayer
if (!player) return
player.play({
success: () => {
this.isPlaying = true
this.isLoading = false
this.startControlTimer()
},
fail: (err) => {
this.handleError(err)
}
})
},
stopPlay() {
const player = this.$refs.livePlayer
if (!player) return
player.stop()
this.isPlaying = false
},
togglePlay() {
if (this.isPlaying) {
this.stopPlay()
} else {
this.startPlay()
}
},
toggleMute() {
this.isMuted = !this.isMuted
},
toggleFullscreen() {
const player = this.$refs.livePlayer
if (!player) return
if (this.isFullscreen) {
player.exitFullScreen({
success: () => {
this.isFullscreen = false
this.orientation = 'vertical'
}
})
} else {
player.requestFullScreen({
direction: 0,
success: () => {
this.isFullscreen = true
this.orientation = 'horizontal'
}
})
}
},
switchQuality(quality) {
if (this.currentQuality === quality) return
this.currentQuality = quality
this.$emit('quality-change', quality)
},
handleStateChange(e) {
const state = e.detail.code
switch (state) {
case 2001: // 已经连接
this.isLoading = false
this.isPlaying = true
break
case 2002: // 已经开始播放
this.isLoading = false
break
case 2103: // 网络断连
this.handleError({ errMsg: '网络连接断开' })
break
}
},
handleNetStatus(e) {
this.netStatus = e.detail
this.$emit('net-status', e.detail)
},
handleError(e) {
this.hasError = true
this.isPlaying = false
this.isLoading = false
this.errorMessage = e.errMsg || '播放出错,请重试'
this.$emit('error', e)
},
retry() {
this.hasError = false
this.errorMessage = ''
this.isLoading = true
this.startPlay()
},
toggleControls() {
this.showControls = !this.showControls
if (this.showControls) {
this.startControlTimer()
}
},
startControlTimer() {
this.clearControlTimer()
this.controlTimer = setTimeout(() => {
this.showControls = false
}, CONTROL_HIDE_TIMEOUT)
},
clearControlTimer() {
if (this.controlTimer) {
clearTimeout(this.controlTimer)
this.controlTimer = null
}
},
formatNumber(num) {
if (num >= 10000) {
return (num / 10000).toFixed(1) + 'w'
}
return num.toString()
}
}
}
</script>
<style lang="scss">
.harmony-live-player {
position: relative;
width: 100%;
background-color: #000000;
&.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
}
.player {
width: 100%;
height: 100%;
}
.player-controls {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.3) 0%,
transparent 40%,
transparent 60%,
rgba(0, 0, 0, 0.3) 100%
);
opacity: 0;
transition: opacity 0.3s ease;
&.controls-visible {
opacity: 1;
}
.top-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 88rpx;
padding: 0 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
.title {
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
}
.online-count {
color: #ffffff;
font-size: 24rpx;
opacity: 0.8;
}
}
.bottom-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 88rpx;
padding: 0 32rpx;
display: flex;
align-items: center;
.control-btn {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
color: #ffffff;
font-size: 48rpx;
}
&:active {
opacity: 0.7;
}
}
.quality-switch {
flex: 1;
display: flex;
justify-content: center;
gap: 24rpx;
text {
color: #ffffff;
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 32rpx;
background-color: rgba(255, 255, 255, 0.2);
&.active {
background-color: #2979ff;
}
&:active {
opacity: 0.7;
}
}
}
}
}
.loading-layer {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
.loading-animation {
width: 64rpx;
height: 64rpx;
border: 4rpx solid rgba(255, 255, 255, 0.1);
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
color: #ffffff;
font-size: 28rpx;
margin-top: 16rpx;
}
}
.error-layer {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
.error-icon {
font-size: 80rpx;
color: #ffffff;
margin-bottom: 16rpx;
}
.error-text {
color: #ffffff;
font-size: 28rpx;
margin-bottom: 32rpx;
}
.retry-btn {
padding: 16rpx 48rpx;
background-color: #2979ff;
color: #ffffff;
font-size: 28rpx;
border-radius: 44rpx;
&:active {
opacity: 0.9;
}
}
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
2. 使用示例
下面展示如何在页面中使用直播播放器组件:
vue
<!-- pages/live/live.vue -->
<template>
<view class="live-page">
<harmony-live-player
:src="liveUrl"
:title="liveInfo.title"
:qualities="qualities"
:online-count="liveInfo.onlineCount"
@error="handlePlayError"
@quality-change="handleQualityChange"
@net-status="handleNetStatus"
></harmony-live-player>
<view class="live-info">
<view class="anchor-info">
<image class="avatar" :src="liveInfo.anchorAvatar"></image>
<view class="info-content">
<text class="anchor-name">{{ liveInfo.anchorName }}</text>
<text class="fans-count">{{ formatNumber(liveInfo.fansCount) }}粉丝</text>
</view>
<button class="follow-btn"
:class="{ 'followed': isFollowed }"
@click="toggleFollow">
{{ isFollowed ? '已关注' : '关注' }}
</button>
</view>
<view class="live-stats">
<view class="stat-item">
<text class="label">观看</text>
<text class="value">{{ formatNumber(liveInfo.viewCount) }}</text>
</view>
<view class="stat-item">
<text class="label">点赞</text>
<text class="value">{{ formatNumber(liveInfo.likeCount) }}</text>
</view>
<view class="stat-item">
<text class="label">分享</text>
<text class="value">{{ formatNumber(liveInfo.shareCount) }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import HarmonyLivePlayer from '@/components/harmony-live-player/harmony-live-player'
export default {
components: {
HarmonyLivePlayer
},
data() {
return {
liveUrl: '',
qualities: [
{ label: '标清', value: 'SD' },
{ label: '高清', value: 'HD' },
{ label: '超清', value: 'FHD' }
],
liveInfo: {
title: '直播标题',
anchorName: '主播昵称',
anchorAvatar: '/static/default-avatar.png',
onlineCount: 0,
viewCount: 0,
likeCount: 0,
shareCount: 0,
fansCount: 0
},
isFollowed: false
}
},
onLoad(options) {
// 获取直播信息
this.fetchLiveInfo(options.id)
},
methods: {
async fetchLiveInfo(id) {
try {
// 这里替换为实际的API调用
const response = await this.$api.getLiveInfo(id)
this.liveInfo = response.data
this.liveUrl = response.data.playUrl
} catch (error) {
uni.showToast({
title: '获取直播信息失败',
icon: 'none'
})
}
},
handlePlayError(error) {
console.error('播放错误:', error)
uni.showToast({
title: '播放出错,请重试',
icon: 'none'
})
},
handleQualityChange(quality) {
console.log('切换清晰度:', quality)
// 这里可以根据清晰度切换不同的播放地址
},
handleNetStatus(status) {
console.log('网络状态:', status)
// 这里可以根据网络状态做相应处理
},
toggleFollow() {
this.isFollowed = !this.isFollowed
// 调用关注/取消关注API
},
formatNumber(num) {
if (num >= 10000) {
return (num / 10000).toFixed(1) + 'w'
}
return num.toString()
}
}
}
</script>
<style lang="scss">
.live-page {
min-height: 100vh;
background-color: #f5f5f5;
.live-info {
padding: 32rpx;
background-color: #ffffff;
.anchor-info {
display: flex;
align-items: center;
margin-bottom: 32rpx;
.avatar {
width: 88rpx;
height: 88rpx;
border-radius: 44rpx;
margin-right: 24rpx;
}
.info-content {
flex: 1;
.anchor-name {
font-size: 32rpx;
color: #333333;
font-weight: 500;
margin-bottom: 8rpx;
}
.fans-count {
font-size: 24rpx;
color: #999999;
}
}
.follow-btn {
padding: 16rpx 48rpx;
background-color: #2979ff;
color: #ffffff;
font-size: 28rpx;
border-radius: 44rpx;
&.followed {
background-color: #f5f5f5;
color: #666666;
}
&:active {
opacity: 0.9;
}
}
}
.live-stats {
display: flex;
justify-content: space-around;
padding: 24rpx 0;
border-top: 2rpx solid #f5f5f5;
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
.label {
font-size: 24rpx;
color: #999999;
margin-bottom: 8rpx;
}
.value {
font-size: 32rpx;
color: #333333;
font-weight: 500;
}
}
}
}
}
</style>
性能优化
- 预加载优化
为了提升直播加载速度,我们可以在进入直播间前预加载资源:
javascript
// utils/preload.js
export const preloadLiveStream = (url) => {
return new Promise((resolve, reject) => {
const context = uni.createLivePlayerContext('preload-player')
context.play({
success: () => {
setTimeout(() => {
context.stop()
resolve()
}, 100)
},
fail: reject
})
})
}
- 状态管理优化
使用 Vuex 管理直播相关状态:
javascript
// store/modules/live.js
export default {
state: {
currentLive: null,
quality: 'HD',
netStatus: {}
},
mutations: {
SET_CURRENT_LIVE(state, live) {
state.currentLive = live
},
SET_QUALITY(state, quality) {
state.quality = quality
},
UPDATE_NET_STATUS(state, status) {
state.netStatus = status
}
},
actions: {
async enterLive({ commit }, liveId) {
try {
const live = await api.getLiveInfo(liveId)
commit('SET_CURRENT_LIVE', live)
return live
} catch (error) {
throw error
}
}
}
}
- 内存优化
在离开直播页面时,及时释放资源:
javascript
export default {
onUnload() {
// 停止播放
this.$refs.livePlayer.stopPlay()
// 清理定时器
this.clearAllTimers()
// 重置状态
this.$store.commit('SET_CURRENT_LIVE', null)
}
}
适配建议
- 网络适配
根据网络状况自动调整清晰度:
javascript
handleNetStatus(status) {
if (status.netSpeed < 1000 && this.currentQuality !== 'SD') {
// 网速较慢时切换到标清
this.switchQuality('SD')
} else if (status.netSpeed > 5000 && this.currentQuality === 'SD') {
// 网速较快时切换到高清
this.switchQuality('HD')
}
}
- 横竖屏适配
scss
.harmony-live-player {
&.fullscreen {
.player-controls {
.bottom-bar {
padding: 0 48rpx;
height: 120rpx;
}
.quality-switch {
text {
font-size: 28rpx;
padding: 12rpx 24rpx;
}
}
}
}
}
总结
通过本文的讲解,我们实现了一个功能完整的移动端直播播放器。该播放器不仅具有优雅的界面设计,还融入了鸿蒙系统的设计理念,同时考虑了性能优化和多端适配等实际开发中的重要因素。
在实际开发中,我们还可以根据具体需求扩展更多功能:
- 弹幕系统
- 礼物特效
- 互动游戏
- 直播回放
- 防盗链处理
希望这篇文章能够帮助你更好地理解如何在 UniApp 中开发高质量的直播功能,同时也能为你的实际项目开发提供有价值的参考。