一、项目架构设计
本教程将基于UniApp构建一个适配微信小程序、H5和App的短视频应用,实现从视频采集到播放的全流程解决方案。
技术架构:
- 核心框架:UniApp 3.0 + Vue3
- 视频处理:腾讯云点播SDK
- 状态管理:Pinia 2.0
- UI组件:uView UI 3.0
- 性能监控:自定义性能统计SDK
核心功能模块:
- 视频流瀑布布局
- 手势滑动切换
- 视频预加载机制
- 多端录制上传
- 播放器性能优化
二、项目初始化与配置
1. 项目创建与扩展安装
# 通过Vue CLI创建项目
vue create -p dcloudio/uni-preset-vue short-video-app
# 安装必要依赖
cd short-video-app
npm install uview-ui @dcloudio/uni-ui pinia
# 配置vue.config.js
module.exports = {
transpileDependencies: ['uview-ui'],
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
maxSize: 500 * 1024
}
}
}
}
2. 多端兼容目录结构
src/
├── common/
│ ├── libs/ # 多端通用库
│ ├── utils/ # 工具函数
│ └── styles/ # 全局样式
├── components/ # 通用组件
├── pages/
│ ├── index/ # 首页
│ ├── record/ # 录制页
│ └── profile/ # 个人页
├── static/
│ ├── videos/ # 本地测试视频
│ └── icons/ # 图标资源
├── store/ # Pinia状态
├── App.vue # 应用入口
└── main.js # 项目入口
三、核心功能实现
1. 视频流组件开发
<template>
<scroll-view
class="video-feed"
scroll-y
@scrolltolower="loadMore"
:scroll-with-animation="true">
<video
v-for="(item, index) in videoList"
:key="item.id"
:id="'video-'+index"
:src="item.url"
:autoplay="currentIndex === index"
:controls="false"
:show-center-play-btn="false"
:enable-progress-gesture="false"
@play="handlePlay(index)"
@error="handleError(index)"
class="video-item">
</video>
<uni-load-more :status="loadingStatus" />
</scroll-view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { fetchVideoList } from '@/api/video'
const videoList = ref([])
const loadingStatus = ref('more')
const currentIndex = ref(0)
const loadData = async () => {
if (loadingStatus.value === 'loading') return
loadingStatus.value = 'loading'
try {
const res = await fetchVideoList({
page: Math.ceil(videoList.value.length / 10) + 1,
size: 10
})
videoList.value = [...videoList.value, ...res.list]
loadingStatus.value = res.hasMore ? 'more' : 'noMore'
// 预加载下个视频
preloadNextVideo()
} catch (e) {
loadingStatus.value = 'error'
}
}
const preloadNextVideo = () => {
const nextIndex = currentIndex.value + 1
if (nextIndex videoContext.pause())
}
}
const handlePlay = (index) => {
// 暂停其他视频
videoList.value.forEach((_, i) => {
if (i !== index) {
const videoContext = uni.createVideoContext(`video-${i}`)
videoContext.pause()
}
})
currentIndex.value = index
preloadNextVideo()
}
onMounted(() => {
loadData()
})
</script>
2. 多端录制实现
// pages/record/record.vue
<template>
<view class="record-page">
<camera
v-if="showCamera"
device-position="front"
flash="off"
@error="cameraError"
class="camera">
</camera>
<view class="controls">
<button @click="toggleCamera">切换摄像头</button>
<button @click="startRecord" :disabled="isRecording">开始录制</button>
<button @click="stopRecord" :disabled="!isRecording">停止录制</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { uploadVideo } from '@/api/video'
const showCamera = ref(true)
const isRecording = ref(false)
const cameraContext = ref(null)
// 初始化相机上下文
onMounted(() => {
cameraContext.value = uni.createCameraContext()
})
const toggleCamera = () => {
showCamera.value = false
nextTick(() => {
showCamera.value = true
})
}
const startRecord = () => {
isRecording.value = true
cameraContext.value.startRecord({
success: () => console.log('开始录制'),
fail: (err) => console.error('录制失败:', err)
})
}
const stopRecord = async () => {
isRecording.value = false
cameraContext.value.stopRecord({
success: async (res) => {
const { tempVideoPath } = res
try {
await uploadVideo(tempVideoPath)
uni.showToast({ title: '上传成功', icon: 'success' })
} catch (e) {
uni.showToast({ title: '上传失败', icon: 'error' })
}
}
})
}
</script>
四、性能优化策略
1. 视频预加载方案
// utils/videoPreload.js
export const preloadVideos = (videos) => {
// 小程序端使用后台播放器预加载
// #ifdef MP-WEIXIN
const backgroundAudioManager = uni.getBackgroundAudioManager()
videos.forEach(video => {
backgroundAudioManager.src = video.url
backgroundAudioManager.pause()
})
// #endif
// H5端使用video元素预加载
// #ifdef H5
videos.forEach(video => {
const videoEl = document.createElement('video')
videoEl.src = video.url
videoEl.preload = 'auto'
document.body.appendChild(videoEl)
})
// #endif
// App端使用原生预加载
// #ifdef APP-PLUS
const videoPlayer = plus.video.createVideoPlayer('preloader', {
top: '-1000px',
height: '1px',
width: '1px'
})
videos.forEach(video => {
videoPlayer.load(video.url)
videoPlayer.pause()
})
// #endif
}
// 在页面中使用
watch(videoList, (newVal) => {
const nextVideos = newVal.slice(currentIndex.value, currentIndex.value + 3)
preloadVideos(nextVideos)
})
2. 播放器性能优化
// 使用uniapp的video组件优化属性
<video
:src="currentVideo.url"
:autoplay="true"
:controls="false"
:show-center-play-btn="false"
:enable-progress-gesture="false"
:enable-play-gesture="true"
:vslide-gesture="true"
:vslide-gesture-in-fullscreen="true"
:picture-in-picture-mode="['push', 'pop']"
:danmu-list="danmuList"
@play="onPlay"
@pause="onPause"
@ended="onEnded"
@error="onError">
</video>
// 播放器事件处理
const onPlay = () => {
// 暂停所有其他视频
videoList.value.forEach((_, index) => {
if (index !== currentIndex.value) {
const videoContext = uni.createVideoContext(`video-${index}`)
videoContext.pause()
}
})
// 上报播放数据
reportPlayStart(currentVideo.value.id)
// 预加载下一个视频
preloadNextVideo()
}
// 使用worker处理弹幕数据
const initDanmuWorker = () => {
const worker = uni.createWorker('workers/danmu.js')
worker.onMessage((res) => {
danmuList.value = res.data
})
worker.postMessage({
action: 'init',
videoId: currentVideo.value.id
})
}
五、多端兼容方案
1. 平台差异化代码处理
// 录制功能兼容多端
const startRecord = () => {
// #ifdef MP-WEIXIN
wx.startRecord({
success: (res) => {
handleRecordSuccess(res.tempFilePath)
}
})
// #endif
// #ifdef APP-PLUS
plus.camera.getCamera().captureVideo(
(res) => handleRecordSuccess(res),
(err) => console.error(err)
)
// #endif
// #ifdef H5
if (navigator.mediaDevices) {
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => {
mediaRecorder.value = new MediaRecorder(stream)
mediaRecorder.value.start()
})
} else {
uni.showToast({ title: '浏览器不支持录制', icon: 'none' })
}
// #endif
}
// 上传功能兼容处理
const uploadVideo = async (filePath) => {
// #ifdef MP-WEIXIN || APP-PLUS
const [uploadRes] = await uni.uploadFile({
url: '/api/upload',
filePath,
name: 'video'
})
return JSON.parse(uploadRes.data)
// #endif
// #ifdef H5
const formData = new FormData()
formData.append('video', filePath)
const res = await fetch('/api/upload', {
method: 'POST',
body: formData
})
return await res.json()
// #endif
}
2. 统一API封装
// api/video.js
import { request } from '@/utils/request'
export const fetchVideoList = (params) => {
return request({
url: '/video/list',
method: 'GET',
params
})
}
export const uploadVideo = (filePath) => {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN || APP-PLUS
uni.uploadFile({
url: '/api/upload',
filePath,
name: 'video',
success: (res) => {
resolve(JSON.parse(res.data))
},
fail: reject
})
// #endif
// #ifdef H5
const input = document.createElement('input')
input.type = 'file'
input.accept = 'video/*'
input.onchange = (e) => {
const file = e.target.files[0]
const formData = new FormData()
formData.append('video', file)
fetch('/api/upload', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(resolve)
.catch(reject)
}
input.click()
// #endif
})
}
// utils/request.js
export const request = (options) => {
return new Promise((resolve, reject) => {
// 统一处理请求
uni.request({
...options,
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else {
reject(res)
}
},
fail: (err) => {
reject(err)
}
})
})
}
六、部署与发布
1. 多端发布配置
// manifest.json 配置示例
{
"name": "短视频应用",
"appid": "__UNI__XXXXXX",
"description": "跨平台短视频应用",
/* 小程序特有配置 */
"mp-weixin": {
"appid": "wxXXXXXXXXXXXXXX",
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "需要获取您的位置信息用于附近视频展示"
}
}
},
/* App特有配置 */
"app-plus": {
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name="android.permission.CAMERA"/>",
"<uses-permission android:name="android.permission.RECORD_AUDIO"/>"
]
},
"ios": {
"UIRequiresFullScreen": true
}
}
},
/* H5配置 */
"h5": {
"router": {
"mode": "history"
},
"template": "template.h5.html"
}
}
七、总结与扩展
本教程构建了一个完整的短视频应用:
- 实现了视频流核心功能
- 优化了多端录制体验
- 完善了性能优化方案
- 解决了多端兼容问题
扩展方向:
- 视频编辑功能集成
- 直播功能扩展
- AI内容审核
- WebAssembly视频处理