小程序使用视频

微信小程序提供了强大的视频处理能力,包括视频选择、录制、播放、编辑等功能。主要涉及以下核心 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>

主要功能说明

  1. ​ 视频选择功能 ​

chooseVideoFromAlbum(): 从相册选择视频

takeVideo(): 拍摄视频

chooseVideoFile(): 选择视频文件(支持长视频)

支持多选和文件信息获取

  1. ​ 视频播放控制 ​

playVideo(): 播放指定视频

pauseVideo(): 暂停播放

stopVideo(): 停止播放

togglePlayPause(): 播放/暂停切换

seekTo(): 跳转到指定时间

  1. ​ 播放器功能 ​

changePlaybackRate(): 调整播放速率

toggleFullScreen(): 全屏切换

toggleControls(): 显示/隐藏控制器

自动连播、上一个/下一个视频

  1. ​ 视频处理功能 ​

compressVideo(): 视频压缩

getVideoInfo(): 获取视频详细信息

saveVideoToAlbum(): 保存视频到相册

  1. ​ 事件处理 ​

视频加载、播放、暂停、结束事件

时间更新、全屏变化、错误处理

进度条拖动、缓冲进度监控

  1. ​ 视频管理 ​

deleteVideo(): 删除单个视频

clearAllVideos(): 清空所有视频

视频列表管理、状态维护

  1. ​ 权限管理 ​

自动检查相册和相机权限

引导用户授权

友好的错误提示

  1. ​ 工具函数 ​

formatTime(): 时间格式化

formatFileSize(): 文件大小格式化

错误信息处理和状态管理

这个示例提供了完整的视频功能实现,包括视频选择、播放控制、文件管理等功能,可以直接在小程序项目中使用。

通关密语:视频