小程序使用视频
微信小程序提供了强大的视频处理能力,包括视频选择、录制、播放、编辑等功能。主要涉及以下核心 API:
主要视频 API:
wx.chooseMedia - 选择视频文件(支持拍摄和相册选择)
wx.createVideoContext - 创建视频上下文,控制视频播放
Video 组件 - 内置视频播放器组件
wx.compressVideo - 视频压缩
wx.saveVideoToPhotosAlbum - 保存视频到相册
视频组件特性:
支持多种视频格式(mp4, mov, avi 等)
支持播放控制(播放/暂停、进度、全屏等)
支持弹幕、字幕功能
支持自定义控制器
支持视频缓存和预加载
这是一个 JS 文件示例:
// pages/video/video.js
Page({
data: {
// 视频列表
videoList: [],
// 当前播放的视频索引
currentVideoIndex: -1,
// 视频播放器上下文
videoContext: null,
// 播放状态
isPlaying: false,
// 播放进度
currentTime: 0,
// 视频总时长
duration: 0,
// 播放速率
playbackRate: 1.0,
// 是否全屏
isFullScreen: false,
// 是否显示控制器
showControls: true,
// 加载状态
isLoading: false,
// 错误信息
errorMsg: '',
// 视频信息
videoInfo: {
width: 0,
height: 0,
size: 0,
duration: 0
}
},
onLoad() {
console.log('视频页面加载');
this.initVideoContext();
this.checkVideoPermission();
},
onUnload() {
// 页面卸载时停止播放
if (this.data.videoContext) {
this.data.videoContext.stop();
}
},
onHide() {
// 页面隐藏时暂停播放
if (this.data.isPlaying) {
this.pauseVideo();
}
},
/**
* 初始化视频上下文
*/
initVideoContext() {
this.setData({
videoContext: wx.createVideoContext('myVideo')
});
},
/**
* 检查视频权限
*/
checkVideoPermission() {
wx.getSetting({
success: (res) => {
if (!res.authSetting['scope.writePhotosAlbum']) {
console.log('未授权相册权限');
}
}
});
},
/**
* 选择视频 - 从相册选择
*/
chooseVideoFromAlbum() {
wx.chooseMedia({
count: 9, // 最多选择9个
mediaType: ['video'], // 只选择视频
sourceType: ['album'], // 从相册选择
maxDuration: 60, // 最大时长60秒
camera: 'back',
success: (res) => {
console.log('选择视频成功', res);
const tempFiles = res.tempFiles;
const newVideoList = tempFiles.map((file, index) => ({
tempFilePath: file.tempFilePath,
thumbTempFilePath: file.thumbTempFilePath,
size: file.size,
duration: file.duration,
width: file.width,
height: file.height,
id: Date.now() + index,
name: `视频_${index + 1}`,
createTime: new Date().toLocaleString()
}));
this.setData({
videoList: [...this.data.videoList, ...newVideoList],
errorMsg: ''
});
wx.showToast({
title: `成功选择${newVideoList.length}个视频`,
icon: 'success',
duration: 2000
});
// 获取视频详细信息
this.getVideoInfo(newVideoList[0].tempFilePath);
},
fail: (err) => {
console.error('选择视频失败', err);
this.handleVideoError(err, '选择视频');
}
});
},
/**
* 拍摄视频
*/
takeVideo() {
wx.chooseMedia({
count: 1,
mediaType: ['video'],
sourceType: ['camera'], // 从相机拍摄
maxDuration: 60, // 最大时长60秒
camera: 'back',
success: (res) => {
console.log('拍摄视频成功', res);
const file = res.tempFiles[0];
const newVideo = {
tempFilePath: file.tempFilePath,
thumbTempFilePath: file.thumbTempFilePath,
size: file.size,
duration: file.duration,
width: file.width,
height: file.height,
id: Date.now(),
name: '拍摄视频',
createTime: new Date().toLocaleString(),
isFromCamera: true
};
this.setData({
videoList: [newVideo, ...this.data.videoList],
errorMsg: ''
});
wx.showToast({
title: '拍摄成功',
icon: 'success',
duration: 2000
});
// 自动播放新拍摄的视频
this.playVideo(0);
},
fail: (err) => {
console.error('拍摄视频失败', err);
this.handleVideoError(err, '拍摄视频');
}
});
},
/**
* 选择视频文件(支持长视频)
*/
chooseVideoFile() {
wx.chooseMessageFile({
count: 5,
type: 'video',
success: (res) => {
console.log('选择视频文件成功', res);
const tempFiles = res.tempFiles;
const newVideoList = tempFiles.map((file, index) => ({
tempFilePath: file.path,
size: file.size,
name: file.name,
id: Date.now() + index,
createTime: new Date().toLocaleString(),
isFromFile: true
}));
this.setData({
videoList: [...this.data.videoList, ...newVideoList]
});
wx.showToast({
title: `成功选择${newVideoList.length}个视频文件`,
icon: 'success'
});
},
fail: (err) => {
this.handleVideoError(err, '选择视频文件');
}
});
},
/**
* 播放视频
*/
playVideo(index) {
if (index < 0 || index >= this.data.videoList.length) {
wx.showToast({
title: '视频不存在',
icon: 'none'
});
return;
}
this.setData({
currentVideoIndex: index,
isPlaying: true,
showControls: true
});
// 延时播放确保视频组件准备好
setTimeout(() => {
if (this.data.videoContext) {
this.data.videoContext.play();
}
}, 100);
// 更新视频信息
const currentVideo = this.data.videoList[index];
this.setData({
videoInfo: {
width: currentVideo.width || 0,
height: currentVideo.height || 0,
size: currentVideo.size || 0,
duration: currentVideo.duration || 0
}
});
},
/**
* 暂停播放
*/
pauseVideo() {
if (this.data.videoContext) {
this.data.videoContext.pause();
this.setData({
isPlaying: false
});
}
},
/**
* 停止播放
*/
stopVideo() {
if (this.data.videoContext) {
this.data.videoContext.stop();
this.setData({
isPlaying: false,
currentTime: 0
});
}
},
/**
* 切换播放/暂停
*/
togglePlayPause() {
if (this.data.currentVideoIndex === -1) {
wx.showToast({
title: '请先选择视频',
icon: 'none'
});
return;
}
if (this.data.isPlaying) {
this.pauseVideo();
} else {
this.playVideo(this.data.currentVideoIndex);
}
},
/**
* 跳转到指定时间
*/
seekTo(e) {
const time = e.detail.value;
if (this.data.videoContext) {
this.data.videoContext.seek(time);
this.setData({
currentTime: time
});
}
},
/**
* 调整播放速率
*/
changePlaybackRate(rate) {
if (this.data.videoContext) {
this.data.videoContext.playbackRate(rate);
this.setData({
playbackRate: rate
});
wx.showToast({
title: `播放速率: ${rate}x`,
icon: 'none',
duration: 1000
});
}
},
/**
* 进入全屏
*/
enterFullScreen() {
if (this.data.videoContext) {
this.data.videoContext.requestFullScreen({
direction: 0 // 0-正常竖向,90-横向
});
}
},
/**
* 退出全屏
*/
exitFullScreen() {
if (this.data.videoContext) {
this.data.videoContext.exitFullScreen();
}
},
/**
* 切换全屏
*/
toggleFullScreen() {
if (this.data.isFullScreen) {
this.exitFullScreen();
} else {
this.enterFullScreen();
}
},
/**
* 显示/隐藏控制器
*/
toggleControls() {
this.setData({
showControls: !this.data.showControls
});
},
/**
* 视频播放事件
*/
onVideoPlay() {
console.log('视频开始播放');
this.setData({
isPlaying: true,
errorMsg: ''
});
},
/**
* 视频暂停事件
*/
onVideoPause() {
console.log('视频暂停');
this.setData({
isPlaying: false
});
},
/**
* 视频结束事件
*/
onVideoEnded() {
console.log('视频播放结束');
this.setData({
isPlaying: false,
currentTime: 0
});
// 自动播放下一个视频
this.playNextVideo();
},
/**
* 播放下一个视频
*/
playNextVideo() {
const nextIndex = this.data.currentVideoIndex + 1;
if (nextIndex < this.data.videoList.length) {
this.playVideo(nextIndex);
wx.showToast({
title: '自动播放下一个视频',
icon: 'none',
duration: 1000
});
}
},
/**
* 播放上一个视频
*/
playPrevVideo() {
const prevIndex = this.data.currentVideoIndex - 1;
if (prevIndex >= 0) {
this.playVideo(prevIndex);
}
},
/**
* 视频时间更新事件
*/
onTimeUpdate(e) {
this.setData({
currentTime: e.detail.currentTime,
duration: e.detail.duration
});
},
/**
* 全屏变化事件
*/
onFullScreenChange(e) {
this.setData({
isFullScreen: e.detail.fullScreen
});
},
/**
* 视频加载事件
*/
onVideoLoad(e) {
console.log('视频加载完成', e);
this.setData({
isLoading: false,
duration: e.detail.duration
});
// 更新视频信息
this.setData({
videoInfo: {
...this.data.videoInfo,
duration: e.detail.duration
}
});
},
/**
* 视频加载开始
*/
onVideoLoadStart() {
console.log('视频开始加载');
this.setData({
isLoading: true
});
},
/**
* 视频错误事件
*/
onVideoError(e) {
console.error('视频播放错误', e);
this.setData({
errorMsg: this.getVideoErrorMsg(e.detail.errMsg),
isLoading: false,
isPlaying: false
});
wx.showToast({
title: '视频播放失败',
icon: 'none',
duration: 2000
});
},
/**
* 获取视频错误信息
*/
getVideoErrorMsg(errMsg) {
const errorMap = {
'1': '视频资源已失效',
'2': '网络错误',
'3': '视频解码错误',
'4': '视频格式不支持',
'5': '视频播放超时'
};
const code = errMsg.split(':')[1];
return errorMap[code] || `播放错误: ${errMsg}`;
},
/**
* 处理视频错误
*/
handleVideoError(err, operation) {
let errorMsg = `${operation}失败`;
switch (err.errMsg) {
case 'chooseMedia:fail auth deny':
errorMsg = '没有相册或相机权限';
break;
case 'chooseMedia:fail cancel':
errorMsg = '用户取消了操作';
return; // 用户取消不提示错误
case 'chooseMedia:fail no video':
errorMsg = '没有找到视频文件';
break;
default:
errorMsg = err.errMsg || errorMsg;
}
wx.showToast({
title: errorMsg,
icon: 'none',
duration: 2000
});
},
/**
* 获取视频信息
*/
getVideoInfo(videoPath) {
wx.getVideoInfo({
src: videoPath,
success: (res) => {
console.log('视频信息获取成功', res);
this.setData({
videoInfo: {
width: res.width,
height: res.height,
size: res.size,
duration: res.duration,
fps: res.fps,
bitrate: res.bitrate
}
});
},
fail: (err) => {
console.error('获取视频信息失败', err);
}
});
},
/**
* 压缩视频
*/
compressVideo(videoPath) {
return new Promise((resolve, reject) => {
wx.compressVideo({
src: videoPath,
quality: 'high', // low, medium, high
bitrate: 1000000,
fps: 30,
resolution: 0.5,
success: (res) => {
console.log('视频压缩成功', res);
resolve(res.tempFilePath);
},
fail: (err) => {
console.error('视频压缩失败', err);
reject(err);
}
});
});
},
/**
* 压缩并替换当前视频
*/
async compressCurrentVideo() {
if (this.data.currentVideoIndex === -1) {
wx.showToast({
title: '请先选择视频',
icon: 'none'
});
return;
}
wx.showLoading({
title: '压缩中...',
mask: true
});
try {
const currentVideo = this.data.videoList[this.data.currentVideoIndex];
const compressedPath = await this.compressVideo(currentVideo.tempFilePath);
// 更新视频列表
const newVideoList = [...this.data.videoList];
newVideoList[this.data.currentVideoIndex] = {
...currentVideo,
tempFilePath: compressedPath,
isCompressed: true,
compressedTime: new Date().toLocaleString()
};
this.setData({
videoList: newVideoList
});
wx.hideLoading();
wx.showToast({
title: '压缩成功',
icon: 'success'
});
// 重新播放压缩后的视频
this.playVideo(this.data.currentVideoIndex);
} catch (error) {
wx.hideLoading();
wx.showToast({
title: '压缩失败',
icon: 'none'
});
}
},
/**
* 保存视频到相册
*/
saveVideoToAlbum() {
if (this.data.currentVideoIndex === -1) {
wx.showToast({
title: '请先选择视频',
icon: 'none'
});
return;
}
const currentVideo = this.data.videoList[this.data.currentVideoIndex];
wx.saveVideoToPhotosAlbum({
filePath: currentVideo.tempFilePath,
success: () => {
wx.showToast({
title: '保存成功',
icon: 'success'
});
// 更新视频状态
const newVideoList = [...this.data.videoList];
newVideoList[this.data.currentVideoIndex].saved = true;
newVideoList[this.data.currentVideoIndex].savedTime = new Date().toLocaleString();
this.setData({
videoList: newVideoList
});
},
fail: (err) => {
console.error('保存视频失败', err);
this.handleSaveError(err);
}
});
},
/**
* 处理保存错误
*/
handleSaveError(err) {
if (err.errMsg.includes('auth deny')) {
this.requestSavePermission();
} else {
wx.showToast({
title: '保存失败',
icon: 'none'
});
}
},
/**
* 请求保存权限
*/
requestSavePermission() {
wx.authorize({
scope: 'scope.writePhotosAlbum',
success: () => {
this.saveVideoToAlbum();
},
fail: () => {
wx.showModal({
title: '权限提示',
content: '需要相册权限才能保存视频',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
wx.openSetting();
}
}
});
}
});
},
/**
* 删除视频
*/
deleteVideo(index) {
if (index === undefined) {
index = this.data.currentVideoIndex;
}
if (index === -1) return;
wx.showModal({
title: '确认删除',
content: '确定要删除这个视频吗?',
success: (res) => {
if (res.confirm) {
const newVideoList = this.data.videoList.filter((_, i) => i !== index);
let newCurrentIndex = this.data.currentVideoIndex;
// 调整当前播放索引
if (index === this.data.currentVideoIndex) {
newCurrentIndex = -1;
this.stopVideo();
} else if (index < this.data.currentVideoIndex) {
newCurrentIndex = this.data.currentVideoIndex - 1;
}
this.setData({
videoList: newVideoList,
currentVideoIndex: newCurrentIndex
});
wx.showToast({
title: '删除成功',
icon: 'success'
});
}
}
});
},
/**
* 清空所有视频
*/
clearAllVideos() {
if (this.data.videoList.length === 0) return;
wx.showModal({
title: '确认清空',
content: '确定要清空所有视频吗?',
success: (res) => {
if (res.confirm) {
this.setData({
videoList: [],
currentVideoIndex: -1
});
this.stopVideo();
wx.showToast({
title: '清空成功',
icon: 'success'
});
}
}
});
},
/**
* 分享视频
*/
onShareAppMessage() {
if (this.data.currentVideoIndex !== -1) {
const currentVideo = this.data.videoList[this.data.currentVideoIndex];
return {
title: `分享视频: ${currentVideo.name}`,
path: '/pages/video/video',
imageUrl: currentVideo.thumbTempFilePath
};
}
return {
title: '分享我的视频',
path: '/pages/video/video'
};
},
/**
* 视频进度条拖动事件
*/
onSliderChange(e) {
const time = e.detail.value;
if (this.data.videoContext) {
this.data.videoContext.seek(time);
}
},
/**
* 视频缓冲事件
*/
onVideoProgress(e) {
console.log('视频缓冲进度', e.detail.buffered);
},
/**
* 视频分辨率切换(模拟)
*/
changeVideoQuality(quality) {
const qualities = {
'auto': '自动',
'1080p': '超清',
'720p': '高清',
'480p': '标清'
};
wx.showToast({
title: `切换至${qualities[quality] || quality}`,
icon: 'none'
});
// 实际项目中可能需要重新加载不同分辨率的视频
},
/**
* 下载网络视频
*/
downloadNetworkVideo(url) {
wx.showLoading({
title: '下载中...',
mask: true
});
wx.downloadFile({
url: url,
success: (res) => {
wx.hideLoading();
if (res.statusCode === 200) {
const newVideo = {
tempFilePath: res.tempFilePath,
id: Date.now(),
name: '网络视频',
createTime: new Date().toLocaleString(),
isFromNetwork: true
};
this.setData({
videoList: [newVideo, ...this.data.videoList]
});
wx.showToast({
title: '下载成功',
icon: 'success'
});
// 播放下载的视频
this.playVideo(0);
}
},
fail: (err) => {
wx.hideLoading();
wx.showToast({
title: '下载失败',
icon: 'none'
});
}
});
},
/**
* 获取当前播放状态
*/
getPlaybackStatus() {
return {
isPlaying: this.data.isPlaying,
currentTime: this.data.currentTime,
duration: this.data.duration,
progress: this.data.duration > 0 ? (this.data.currentTime / this.data.duration * 100) : 0,
currentVideo: this.data.currentVideoIndex !== -1 ?
this.data.videoList[this.data.currentVideoIndex] : null
};
},
/**
* 格式化时间(秒 -> 分:秒)
*/
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
},
/**
* 格式化文件大小
*/
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
});
这是一个 WXML 文件示例:
<!-- pages/video/video.wxml -->
<view class="container">
<!-- 操作按钮区域 -->
<view class="action-buttons">
<button bindtap="chooseVideoFromAlbum">选择视频</button>
<button bindtap="takeVideo">拍摄视频</button>
<button bindtap="chooseVideoFile">选择文件</button>
<button bindtap="togglePlayPause">{{isPlaying ? '暂停' : '播放'}}</button>
<button bindtap="stopVideo">停止</button>
<button bindtap="saveVideoToAlbum">保存</button>
<button bindtap="compressCurrentVideo">压缩</button>
</view>
<!-- 视频播放器 -->
<view class="video-player" wx:if="{{currentVideoIndex !== -1}}">
<video
id="myVideo"
src="{{videoList[currentVideoIndex].tempFilePath}}"
controls="{{showControls}}"
autoplay="{{false}}"
loop="{{false}}"
muted="{{false}}"
initial-time="0"
bindplay="onVideoPlay"
bindpause="onVideoPause"
bindended="onVideoEnded"
bindtimeupdate="onTimeUpdate"
bindfullscreenchange="onFullScreenChange"
bindloadedmetadata="onVideoLoad"
bindloadstart="onVideoLoadStart"
binderror="onVideoError"
bindprogress="onVideoProgress"
danmu-list="{{danmuList}}"
>
<cover-view class="custom-controls" wx:if="{{showControls}}">
<!-- 自定义控制器 -->
</cover-view>
</video>
</view>
<!-- 视频信息 -->
<view class="video-info" wx:if="{{currentVideoIndex !== -1}}">
<text>当前播放: {{videoList[currentVideoIndex].name}}</text>
<text>时长: {{formatTime(duration)}}</text>
<text>大小: {{formatFileSize(videoInfo.size)}}</text>
<text>分辨率: {{videoInfo.width}}x{{videoInfo.height}}</text>
</view>
<!-- 播放控制 -->
<view class="playback-controls" wx:if="{{currentVideoIndex !== -1}}">
<slider
value="{{currentTime}}"
max="{{duration}}"
step="1"
bindchange="seekTo"
show-value
/>
<text>{{formatTime(currentTime)}} / {{formatTime(duration)}}</text>
<view class="control-buttons">
<button bindtap="playPrevVideo">上一个</button>
<button bindtap="togglePlayPause">{{isPlaying ? '暂停' : '播放'}}</button>
<button bindtap="playNextVideo">下一个</button>
<button bindtap="toggleFullScreen">{{isFullScreen ? '退出全屏' : '全屏'}}</button>
</view>
</view>
<!-- 视频列表 -->
<view class="video-list">
<view class="video-item"
wx:for="{{videoList}}"
wx:key="id"
bindtap="playVideo"
data-index="{{index}}"
class="{{index === currentVideoIndex ? 'active' : ''}}"
>
<image src="{{item.thumbTempFilePath}}" mode="aspectFill" />
<view class="video-info">
<text>{{item.name}}</text>
<text>{{formatTime(item.duration)}} - {{formatFileSize(item.size)}}</text>
</view>
<button bindtap="deleteVideo" data-index="{{index}}">删除</button>
</view>
</view>
</view>
主要功能说明
- 视频选择功能
chooseVideoFromAlbum(): 从相册选择视频
takeVideo(): 拍摄视频
chooseVideoFile(): 选择视频文件(支持长视频)
支持多选和文件信息获取
- 视频播放控制
playVideo(): 播放指定视频
pauseVideo(): 暂停播放
stopVideo(): 停止播放
togglePlayPause(): 播放/暂停切换
seekTo(): 跳转到指定时间
- 播放器功能
changePlaybackRate(): 调整播放速率
toggleFullScreen(): 全屏切换
toggleControls(): 显示/隐藏控制器
自动连播、上一个/下一个视频
- 视频处理功能
compressVideo(): 视频压缩
getVideoInfo(): 获取视频详细信息
saveVideoToAlbum(): 保存视频到相册
- 事件处理
视频加载、播放、暂停、结束事件
时间更新、全屏变化、错误处理
进度条拖动、缓冲进度监控
- 视频管理
deleteVideo(): 删除单个视频
clearAllVideos(): 清空所有视频
视频列表管理、状态维护
- 权限管理
自动检查相册和相机权限
引导用户授权
友好的错误提示
- 工具函数
formatTime(): 时间格式化
formatFileSize(): 文件大小格式化
错误信息处理和状态管理
这个示例提供了完整的视频功能实现,包括视频选择、播放控制、文件管理等功能,可以直接在小程序项目中使用。
通关密语:视频