第8章 8.2 音视频:uni.createVideoContext

上一章我们搞定了地图和定位,学会了在地图上标记位置、获取用户坐标。这一章我们要给 App 装上「眼睛和耳朵」——让它们能看见画面、听见声音。学完这章,你就能做出视频播放器、录音机、甚至简易的拍照录像工具。

你有没有遇到过这种情况:刷短视频时,点一下暂停、再点一下继续,流畅得像变魔术?或者用过录音 App,录完之后立刻就能回放?这些功能的背后,都是音视频 API 在默默工作。

今天我们就来搞定 uniapp 里的音视频开发。学完你能:

  • 做一个自己的视频播放器
  • 写一个录音并回放的小工具
  • 组合做出拍照 + 录像的综合工具

🎯 开场 3 分钟:为什么要学这个?

先问大家一个问题:你手机里装了多少个音视频相关的 App?

抖音、快手、微信语音、网易云音乐……几乎每个 App 都在用音视频。音视频能力已经是现代 App 的标配,不会做音视频,就像开餐厅不会做菜——你其他方面做得再好,客户也觉得少了点啥。

举一个真实的痛点:你想给家里\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n老人做个「家庭相册 App」,可以看照片、听老歌、甚至录一段祝福视频发给大家。要是不知道怎么用音视频 API,这功能就做不了。

学完这一章,这个 App 你能做出来。


🧱 基础 25 分钟:核心概念

8.2.1 视频播放:就像去电影院

想象你去电影院看电影:

  1. 首先你得有个屏幕——这就是 <video> 组件,uniapp 里放视频就靠它
  2. 然后你需要一把钥匙——uni.createVideoContext 就是这把钥匙,它能控制视频的播放、暂停、跳转
  3. 钥匙插进去才能开灯——创建完 context,你才能真正操控视频

用生活类比:video 组件是电影院的IMAX巨幕,createVideoContext 是放映厅的遥控器。没有遥控器,你只能眼巴巴看着屏幕;有了遥控器,你才能想播就播、想停就停。

8.2.2 创建视频上下文

// wxml 里先放一个 video 组件,记得给它起个 id
// <video id="myVideo" src="{{videoUrl}}" bindended="onVideoEnded"></video>

// js 里创建控制上下文
const videoContext = uni.createVideoContext('myVideo')
  • uni.createVideoContext('myVideo') 中的 'myVideo' 要和 video 组件的 id 一致
  • 这行代码执行后,videoContext 就是你控制视频的「遥控器」

8.2.3 用遥控器控制视频

创建好 context,你就可以调用各种方法:

// 播放视频
videoContext.play()

// 暂停视频
videoContext.pause()

// 跳到指定位置(单位:秒)
videoContext.seek(30)

// 停止播放
videoContext.stop()

这四个方法就是遥控器的四个按钮,基本涵盖了视频控制的所有场景。

8.2.4 视频组件的属性

光有遥控器不够,视频组件本身也要配置好:

<video
id="myVideo"
src="{{currentVideo}}"
controls="{{true}}"
autoplay="{{false}}"
loop="{{false}}"
muted="{{false}}"
bindplay="onPlay"
bindpause="onPause"
bindended="onEnded"
binderror="onError"
poster="{{posterImage}}"
objectFit="contain"
/>
属性 作用 可选值
src 视频地址 字符串
controls 是否显示控制条 true/false
autoplay 是否自动播放 true/false
loop 是否循环播放 true/false
poster 视频封面图 图片地址
objectFit 视频填充方式 contain/fill/cover

举个例子objectFit="contain" 像是把照片装进相框,完整显示但可能有黑边;objectFit="fill" 像是拉伸铺满,可能变形。

8.2.5 音频播放:更轻量的声音控制

视频太重了?如果你只需要播放声音(比如背景音乐、语音播报),用 InnerAudioContext 更轻便:

// 创建音频上下文(相当于一个随身听)
const audioContext = uni.createInnerAudioContext()

// 设置音频源
audioContext.src = 'https://example.com/music.mp3'

// 播放
audioContext.play()

// 暂停
audioContext.pause()

// 监听播放完成
audioContext.onPlay(() => {
console.log('开始播放了')
})

audioContext.onEnded(() => {
console.log('播完了')
})

类比:视频是「电影院」,音频是「随身听」——功能更少,但更轻便省电。

8.2.6 录音功能:给App装上耳朵

录音需要用到 RecorderManager,步骤很简单:

// 获取录音管理器
const recorderManager = uni.getRecorderManager()

// 监听录音开始
recorderManager.onStart(() => {
console.log('开始录音')
})

// 监听录音结束
recorderManager.onStop((res) => {
console.log('录音文件路径:', res.tempFilePath)
// res.tempFilePath 就是录好的音频文件
})

// 开始录音(最长时间 60 秒)
recorderManager.start({
duration: 60000,
format: 'mp3'
})

// 停止录音
recorderManager.stop()

注意:录音功能需要用户授权,在 pages.json 里要配置权限描述。

8.2.7 相机拍照:捕捉瞬间

相机功能用 CameraContext

// 创建相机上下文
const cameraContext = uni.createCameraContext()

// 拍照
cameraContext.takePhoto({
quality: 'high',
success: (res) => {
console.log('照片路径:', res.tempImagePath)
}
})

🔥 实战 35 分钟:3 个递进的小项目

项目 1:5分钟 - 简易视频播放器

目标:点击按钮播放/暂停视频

<!-- video-player.wxml -->
<view class="container">
<video
id="myPlayer"
src="{{videoUrl}}"
controls
bindended="onEnded"
></video>

<view class="btn-group">
<button bindtap="playVideo">▶ 播放</button>
<button bindtap="pauseVideo">⏸ 暂停</button>
<button bindtap="restartVideo">🔄 重播</button>
</view>

<view class="tip">当前状态:{{status}}</view>
</view>
// video-player.js
Page({
data: {
videoUrl: 'https://example.com/demo.mp4',
status: '等待播放'
},

onReady() {
// 页面加载完成后创建视频上下文
this.videoContext = uni.createVideoContext('myPlayer')
},

playVideo() {
this.videoContext.play()
this.setData({ status: '正在播放' })
},

pauseVideo() {
this.videoContext.pause()
this.setData({ status: '已暂停' })
},

restartVideo() {
this.videoContext.seek(0)
this.videoContext.play()
this.setData({ status: '正在播放' })
},

onEnded() {
this.setData({ status: '播放结束' })
}
})

预期输出:点击「播放」按钮,视频开始播放,状态显示"正在播放";点击「暂停」显示"已暂停";点击「重播」从头开始。

一句话解释:创建 VideoContext 后,调用 play()/pause()/seek() 就能控制视频,就像拿到了遥控器。


项目 2:15分钟 - 视频列表切换器

目标:从列表中选择视频播放,模拟一个简易的短视频切换功能

<!-- video-list.wxml -->
<view class="container">
<!-- 当前播放的视频 -->
<video
id="mainPlayer"
src="{{currentVideo.url}}"
controls
autoplay
></video>

<!-- 视频信息 -->
<view class="video-info">
<text class="title">{{currentVideo.title}}</text>
<text class="desc">{{currentVideo.desc}}</text>
</view>

<!-- 视频列表 -->
<view class="video-list">
<text class="list-title">推荐视频:</text>
<view
  wx:for="{{videoList}}"
  wx:key="id"
  class="list-item {{item.id === currentVideo.id ? 'active' : ''}}"
  bindtap="switchVideo"
  data-id="{{item.id}}"
>
  <image src="{{item.thumb}}" mode="aspectFill" />
  <text>{{item.title}}</text>
</view>
</view>
</view>
// video-list.js
Page({
data: {
currentVideo: null,
videoList: [
  {
    id: 1,
    title: 'Python 入门第一课',
    desc: '5分钟学会变量和循环',
    url: 'https://example.com/python1.mp4',
    thumb: '/static/thumb/python1.jpg'
  },
  {
    id: 2,
    title: 'JavaScript 速通',
    desc: '前端工程师必修课',
    url: 'https://example.com/js1.mp4',
    thumb: '/static/thumb/js1.jpg'
  },
  {
    id: 3,
    title: 'uniapp 实战',
    desc: '从小程序到App开发',
    url: 'https://example.com/uni1.mp4',
    thumb: '/static/thumb/uni1.jpg'
  }
]
},

onLoad() {
this.videoContext = uni.createVideoContext('mainPlayer')
this.setData({ currentVideo: this.data.videoList[0] })
},

switchVideo(e) {
const id = e.currentTarget.dataset.id
const video = this.data.videoList.find(v => v.id === id)
if (video) {
  this.setData({ currentVideo: video })
}
}
})

预期输出:页面加载自动播放第一个视频,点击列表中的其他视频,当前视频切换到新视频并自动播放。

一句话解释:维护一个视频列表,点击时更新 currentVideo 数据,video 组件的 src 绑定会自动触发切换。


项目 3:15分钟 - 录音 + 回放综合工具

目标:录音 -> 显示录音列表 -> 点击回放,完整实现「录-存-播」流程

<!-- voice-recorder.wxml -->
<view class="container">
<!-- 录音按钮区 -->
<view class="record-section">
<button
  type="{{isRecording ? 'warn' : 'primary'}}"
  bindtap="toggleRecord"
>
  {{isRecording ? '⏹ 停止录音' : '🎤 开始录音'}}
</button>
<text class="timer">{{recordTime}}秒</text>
</view>

<!-- 录音列表 -->
<view class="record-list">
<text class="list-title">录音记录:</text>
<view
  wx:for="{{recordings}}"
  wx:key="id"
  class="record-item"
>
  <view class="record-info">
    <text class="record-name">{{item.name}}</text>
    <text class="record-duration">{{item.duration}}秒</text>
  </view>
  <view class="record-actions">
    <button size="mini" bindtap="playRecording" data-id="{{item.id}}">▶ 播放</button>
    <button size="mini" type="warn" bindtap="deleteRecording" data-id="{{item.id}}">删除</button>
  </view>
</view>
<view wx:if="{{recordings.length === 0}}" class="empty-tip">
  还没有录音,试试点击上方按钮
</view>
</view>
</view>
// voice-recorder.js
Page({
data: {
isRecording: false,
recordTime: 0,
recordings: [],
timer: null
},

onLoad() {
this.recorderManager = uni.getRecorderManager()

this.recorderManager.onStart(() => {
  this.setData({ isRecording: true })
  this.startTimer()
})

this.recorderManager.onStop((res) => {
  this.setData({ isRecording: false })
  this.stopTimer()
  this.addRecording(res.tempFilePath)
})

// 音频实例(用于回放)
this.audioContext = uni.createInnerAudioContext()
},

toggleRecord() {
if (this.data.isRecording) {
  this.recorderManager.stop()
} else {
  this.recorderManager.start({
    duration: 60000,
    format: 'mp3'
  })
}
},

startTimer() {
this.setData({ recordTime: 0 })
this.timer = setInterval(() => {
  this.setData({ recordTime: this.data.recordTime + 1 })
}, 1000)
},

stopTimer() {
if (this.timer) {
  clearInterval(this.timer)
  this.timer = null
}
},

addRecording(filePath) {
const newRecording = {
  id: Date.now(),
  name: `录音${this.data.recordings.length + 1}`,
  path: filePath,
  duration: this.data.recordTime
}
this.setData({
  recordings: [...this.data.recordings, newRecording]
})
},

playRecording(e) {
const id = e.currentTarget.dataset.id
const recording = this.data.recordings.find(r => r.id === id)
if (recording) {
  this.audioContext.src = recording.path
  this.audioContext.play()
}
},

deleteRecording(e) {
const id = e.currentTarget.dataset.id
const recordings = this.data.recordings.filter(r => r.id !== id)
this.setData({ recordings })
},

onUnload() {
this.stopTimer()
this.audioContext.destroy()
}
})

预期输出:点击「开始录音」,计时器开始计时;点击「停止录音」,新录音出现在列表中;点击列表项的「播放」可以回放录音;点击「删除」移除该录音。

一句话解释RecorderManager 负责录音,InnerAudioContext 负责回放,两者配合实现完整的录音工具。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:video 组件的 id 和 createVideoContext 对不上

// ❌ 错误:id 写错了
// <video id="myVideo" ...></video>
const ctx = uni.createVideoContext('myVideo1') // 拼写错误!

// ✅ 正确:id 要完全一致
const ctx = uni.createVideoContext('myVideo')

提示:养成习惯,id 统一用驼峰命名,写完检查一遍。

坑 2:视频地址是空的

// ❌ 错误:src 为空时播放会报错
this.setData({ videoUrl: '' })
this.videoContext.play()

// ✅ 正确:先检查 src 是否有值
if (this.data.videoUrl) {
this.videoContext.play()
}

坑 3:录音没配权限

// ❌ 错误:在微信小程序里直接录音,没配置权限
// manifest.json 里没配,pages.json 里也没配

// ✅ 正确:需要在 manifest.json 配置
{
"mp-weixin": {
"permission": {
  "scope.record": {
    "desc": "用于录音功能"
  }
}
}
}

坑 4:autoplay 在有些平台不生效

// ❌ 错误:iOS 微信小程序禁止自动播放
// <video autoplay="{{true}}" ...></video>

// ✅ 正确:iOS 最好让用户主动触发播放
// autoplay 设为 false,点击按钮后再 play()

坑 5:音频播放完成没监听

// ❌ 错误:没写 onEnded 监听,播放完不知道
this.audioContext.play()

// ✅ 正确:监听播放完成事件
this.audioContext.onEnded(() => {
console.log('播放结束了')
// 可以在这里做下一首自动播放等逻辑
})

性能小优化:视频封面用懒加载

// ❌ 一次性加载所有视频封面
const videos = [
{ url: '...', poster: 'https://example.com/p1.jpg' },
{ url: '...', poster: 'https://example.com/p2.jpg' },
// ... 100个
]

// ✅ 滚动到可见区域再加载封面
// 使用 IntersectionObserver 监听元素可见性

调试技巧:善用 bind 事件

<!-- 视频组件绑定所有事件 -->
<video
bindplay="onPlay"
bindpause="onPause"
bindended="onEnded"
binderror="onError"
bindtimeupdate="onTimeUpdate"
/>
onPlay(e) {
console.log('开始播放', e.detail)
},

onTimeUpdate(e) {
// 可以用来更新进度条
const { currentTime, duration } = e.detail
this.setData({ progress: (currentTime / duration) * 100 })
},

onError(e) {
console.error('视频出错了', e.detail)
uni.showToast({ title: '视频加载失败' })
}

✏️ 练习题 + 作业题

练习题(10 分钟)

练习 1(2 分钟):播放控制
- 输入:视频 URL https://example.com/test.mp4
- 预期输出:点击「播放」按钮视频开始播放,点击「暂停」按钮视频暂停
- 提示:直接复用项目 1 的代码,只改一下 videoUrl

练习 2(2 分钟):添加静音功能
- 输入:在项目 1 基础上加一个静音按钮
- 预期输出:点击「静音」按钮,视频切换静音状态
- 提示:videoContext.muted = true/false

练习 3(2 分钟):视频进度条
- 输入:在项目 1 的视频下方显示当前播放进度(如 "00:30 / 03:45")
- 预期输出:播放时实时更新进度文字
- 提示:用 bindtimeupdate 事件获取 currentTime

练习 4(2 分钟):切换到新数据
- 输入:用项目 2 的结构,处理这个视频列表:

const videos = [
{ id: 1, title: '教程1', url: '...', thumb: '...' },
{ id: 2, title: '教程2', url: '...', thumb: '...' }
]
  • 预期输出:页面显示两个视频,点击可以切换
  • 提示:把 videos 赋值给 videoList 即可

练习 5(2 分钟):录音超时处理
- 输入:用户录音超过 30 秒时提示"录音超时"
- 预期输出:录音 30 秒时自动停止并弹出提示
- 提示:在 onStart 回调里设置 setTimeout,30 秒后调用 stop()


作业题(30 分钟 - 2 小时)

作业:做一个「简易视频播放器」

  • 需求描述:实现一个带播放列表的视频播放器,可以切换视频、显示进度
  • 功能点:
    1. 顶部显示当前播放视频的标题
    2. 中间是视频播放器,带播放/暂停按钮
    3. 下方是视频列表,点击切换播放
    4. 显示当前播放进度(如 "01:23 / 05:00")
  • 加分项:
    1. 支持播放/暂停图标切换(▶ ↔ ⏸)
    2. 点击进度条可以跳转
    3. 视频切换时平滑过渡
  • 验收标准:
  • 能跑起来,不报错
  • 视频列表至少 3 个视频
  • 点击列表项能切换视频
  • 代码有适当注释
  • 提交方式:评论区贴核心代码或 GitHub 链接

📚 总结 + 资源

本文学了 3 个核心点:

  1. VideoContext 是遥控器uni.createVideoContext 创建控制对象,play/pause/seek/stop 是四个基本按钮
  2. InnerAudioContext 是随身听 — 只需要播放声音时用它,更轻量
  3. RecorderManager 是录音笔start/stop 控制录音,onStop 拿到录音文件路径

推荐延伸资源:

互动钩子:

你做过音视频相关的项目吗?遇到过什么坑?是视频加载慢、还是录音权限被拒?评论区聊聊,老粉优先回复!


下章预告:
学会了「眼睛」和「耳朵」,下一章我们要解锁一个新技能——蓝牙和 NFC。想象一下,用手机靠近一下就能开门、刷卡、连接设备,这些功能怎么用代码实现?下一章见!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。