uniapp - 腾讯云点播小程序插件

2,410 阅读7分钟

欢迎关注微信公众号:FSA全栈行动 👋

一、简介

微信小程序播放教育类视频要求具备有相关资质,但这些资质一般公司很难短时间申请下来(甚至有的公司压根就申请不了),而【短视频播放器小程序插件】含有《信息网络传播视听节目许可证》的资质证书备案,可以利用该插件来解决资质问题,相关截图如下:

图片来源:cloud.tencent.com/document/pr…

采购流程于技术无关,以下内容着重讲解如何集成该微信小程序插件。

注:【短视频播放器小程序插件】授权费 3 万/年(有 14 天试用 Licence),如果有购买腾讯云其他服务的话,满足一定条件,会赠送 1 年 免费使用 Licence,详情找腾讯云客服咨询(2022 年 04 月如此,赠送情况可能随时会变)。

二、使用

  • 激活:在腾讯云控制台激活插件 Licence 之后,才能正常使用该播放器插件。
  • appid:后面插件用到的 appid 需要在【腾讯云控制台】>账号信息中查看获取。
  • 云点播短视频播放器-开发文档:mp.weixin.qq.com/wxopen/plug…

1、绑定插件

因为微信小程序插件没有实质代码或 SDK,所以无法在本地添加集成,需要在微信小程序平台,将 小程序AppID插件AppID 进行绑定(即给小程序添加插件),开发者工具在编译时会自动引入,绑定有 2 种方式:

  • 方式 1:登录微信小程序平台,在 设置-第三方设置 中找到 添加插件,输入插件 AppID(wx116d0dd5e6a39ac7 )搜索并添加:

云点播短视频播放器文档:mp.weixin.qq.com/wxopen/plug…

2、集成插件

微信小程序原生工程需要 在 app.json 里声明使用的插件及版本,对应到 uniapp 工程,则是在 manifest.json 文件中微信小程序特有配置(即 mp-weixin 节点) 处,进行 plugins 配置声明:

// manifest.json 源码视图
{
  /* 小程序特有相关 */
  "mp-weixin": {
    "appid": "wxxxxxxxxxxx",
    "plugins": {
      // 云点播短视频播放器
      // 文档:https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx116d0dd5e6a39ac7&token=1835838344&lang=zh_CN
      "cloudPlayer": {
        "version": "0.1.2",
        "provider": "wx116d0dd5e6a39ac7"
      }
    }
  }
}

manifest.json 配置项说明:uniapp.dcloud.io/collocation…

3、页面内使用播放器

微信小程序原生工程需要 在 页面的 xxxx.json 里声明,对应到 uniapp 工程,则是在 pages.json 文件中,在需要使用插件的 页面的 style 的微信小程序特有配置(即 mp-weixin 节点)处,进行 usingComponents 配置声明:

// pages.json
{
  "pages": [
    ...,
    {
      "path": "pages/course/course",
      "style": {
        "navigationBarTitleText": "课程",
        "mp-weixin": {
          // 云点播短视频播放器
          "usingComponents": {
            "cloud-player": "plugin://cloudPlayer/player"
          }
        }
      }
    }
  ],
  "globalStyle": {
    ...
  }
}

pages.json 配置项说明:uniapp.dcloud.io/collocation…

声明完哪个页面需要使用插件播放器之后,就可以在那个页面的布局文件中使用插件播放器了:

// 在wxml里插入
<cloud-player
  appid="xxxxx"
  fileid="xxxxxxxx"
  playerid="myPlayerId"
></cloud-player>

注:微信小程序原生工程中,页面是 wxml 文件,uniapp 工程中是 vue 文件。另外,目前这种声明方式只对单个配置过的页面有效,也就是说,如果其他页面也需要使用插件播放器,还需要在其他页面的style中单独进行配置,这就很麻烦了,不过现在不用烦恼,后面会解决这个问题。

4、组件内使用播放器

为了功能复用,以及方便代码维护,实际开发中,往往会自定义组件,对常用的布局、功能进行封装。微信小程序原生工程可以在自定义组件的 json 文件中进行配置,跟页面相同的 usingComponents 配置即可:

官方的 云点播短视频播放器-开发文档 只说明在了如何在网页中使用插件,但没有对组件中如何使用插件进行说明,很无语,希望后续官方能完善一下。另外,这是我发起工单询问之后,腾讯技术售后给我的 demo 工程中的代码,是否有效暂不确定 -_-!

uniapp 遵循 vue 规范,想要在自定义组件要使用其他自定义组件,需要在 vue 文件中的 <script> 标签中,配置 components ,例如:

<script>
import leadHeader from "./lead-header.vue";
export default {
  components: {
    leadHeader,
  },
};
</script>

那么依葫芦画瓢,是否也可以这样配置插件播放器呢?例如:

<script>
import cloudPlayer from "plugin://cloudPlayer/player";
export default {
  components: {
    cloudPlayer,
  },
};
</script>

可惜不行,编译时会报错 Error: Can't resolve 'plugin://cloudPlayer/player',而且 uniapp 也没有提供对应的配置项。不过呢,uniapp 是可以直接使用微信小程序自定义组件的,这是否意味着,可以将用到插件的自定义组件改用 wxml+wcss 的方式进行编写,然后再引入到 uniapp 工程中呢?

uniapp 使用小程序原生组件:uniapp.dcloud.io/tutorial/mi…

仔细想想,这个方案是有问题的。首先,对不熟悉微信小程序原生开发的人很不友好,其次,wxcomponents 目录下的小程序组件,要使用的话,还需要在 pages.json 中进行配置,这意味着 uniapp 自定义组件中是不能直接使用小程序组件的,无法解决 组件中引入组件 的情况,所以,这个方案不行。难道 uniapp 对此就无解了吗?非也,仔细阅读上面的 uniapp 官方文档,可以找到这么一句:当需要在 vue 组件中使用小程序组件时,注意在 pages.json 的 globalStyle 中配置 usingComponents,而不是页面级配置

于是,在 pages.json 文件中做如下修改:

// pages.json
{
  "pages": [
    ...,
    {
      "path": "pages/course/course",
      "style": {
        "navigationBarTitleText": "课程",
        // "mp-weixin": {
        //  "usingComponents": {
        //    "cloud-player": "plugin://cloudPlayer/player"
        //  }
        // }
      }
    }
  ],
  "globalStyle": {
    // #ifdef MP-WEIXIN
    "usingComponents": {
      "cloud-player": "plugin://cloudPlayer/player"
    },
    // #endif
    ...
  }
}

可以发现,我把页面 style 下的 mp-weixin 配置给注释掉了,原因是在 globalStyle 下配置了 usingComponents 之后,就可以全局使用插件播放器,不管是页面或是组件中,都不需要再单独去配置 usingComponents,这样就可以在项目中随心所欲地使用播放器插件了,nice~

5、获取播放器 Context

当需要在业务逻辑中控制视频播放或暂停时,会用到 videoContext,如果使用默认的 <video> 标签,那么可以通过 uni.createVideoContext(videoId, this) 来获取视频播放器上下文,再通过上下文执行 play()pause() 等方法,即可控制视频播放,详细说明见 uniapp 官方文档:

createVideoContext:uniapp.dcloud.io/api/media/v…

但是,uni.createVideoContext(videoId, this) 对腾讯云点播插件无效,需改用 requirePlugin(pluginName).getContext(videoId) 来获取,例如:

const plugin = requirePlugin("cloudPlayer");
let player = plugin.getContext("myVideo");

该解决方案源自一篇社区帖子 《腾讯云点播 wx.createVideoContext("myVideo").pause()无法暂停》:developers.weixin.qq.com/community/d…

三、封装

腾讯云点播插件 <cloud-player> 与默认的 <video> 标签在使用上差异不多,就以下几点:

  • <cloud-player> 使用时需要配置 appid 属性。
  • <cloud-player> 使用时需要配置 widthheight 属性。
  • <cloud-player> 视频源属性是 fileid<video> 视频源属性是 src
  • <cloud-player> id 属性是 playerid<video> id 属性是 id
  • <cloud-player> 上下文通过 requirePlugin(pluginName).getContext(videoId) 获取,<video> 上下文通过 uni.createVideoContext(videoId, this) 获取。

所以,为了代码可维护性,统一模板代码,我们可以自定义组件(名为 video-mix)对两者进行封装,用法上跟 <video> 标签差不多:

<video-mix
  videoId="videoPlayer"
  width="710rpx"
  height="400rpx"
  :fileid="curPlayEpisode.code"
  src="http://xxxx/video1.mp4"
  :poster="curPlayEpisode.cover_img"
  :controls="true"
  :autoplay="true"
  :show-progress="showProgress"
  @error="onVideoError"
  @controlstoggle="onVideoControlsToggle"
></video-mix>

注:我个人设想在微信小程序上使用腾讯云点播插件播放视频,在其他平台上还是继续使用 <video> 标签,于是设计为 fileidsrc 共存。

以下是 video-mix.vue 的完整代码:

// video-mix.vue
<template>
  <view class="video-mix-container">
    <!-- #ifdef MP-WEIXIN -->
    <cloud-player
      appid="GitLqr亲自打码"
      :id="videoId"
      :playerid="videoId"
      :width="width"
      :height="height"
      :fileid="fileid"
      :autoplay="autoplay"
      :loop="loop"
      :muted="muted"
      :controls="controls"
      :danmu-list="danmuList"
      :danmu-btn="danmuBtn"
      :enable-danmu="enableDanmu"
      :page-gesture="pageGesture"
      :show-progress="showProgress"
      :show-fullscreen-btn="showFullscreenBtn"
      :show-play-btn="showPlayBtn"
      :show-center-play-btn="showCenterPlayBtn"
      :enable-progress-gesture="enableProgressGesture"
      :object-fit="objectFit"
      :poster="poster"
      :show-mute-btn="showMuteBtn"
      :title="title"
      :play-btn-position="playBtnPosition"
      :enable-play-gesture="enablePlayGesture"
      :auto-pause-if-navigate="autoPauseIfNavigate"
      :auto-pause-if-open-native="autoPauseIfOpenNative"
      :vslide-gesture="vslideGesture"
      :vslide-gesture-in-fullscreen="vslideGestureInFullscreen"
      :ad-unit-id="adUnitId"
      :poster-for-crawler="posterForCrawler"
      @play="onPlay"
      @pause="onPause"
      @ended="onEnded"
      @timeupdate="onTimeUpdate"
      @fullscreenchange="onFullScreenChange"
      @waiting="onWaiting"
      @error="onError"
      @progress="onProgress"
      @loadedmetadata="onLoadedMetaData"
      @controlstoggle="onControlsToggle"
    >
    </cloud-player>
    <!-- #endif -->
    <!-- #ifndef MP-WEIXIN -->
    <video
      :id="videoId"
      :style="{ width: width, height: height }"
      :src="src"
      :autoplay="autoplay"
      :loop="loop"
      :muted="muted"
      :controls="controls"
      :danmu-list="danmuList"
      :danmu-btn="danmuBtn"
      :enable-danmu="enableDanmu"
      :page-gesture="pageGesture"
      :show-progress="showProgress"
      :show-fullscreen-btn="showFullscreenBtn"
      :show-play-btn="showPlayBtn"
      :show-center-play-btn="showCenterPlayBtn"
      :enable-progress-gesture="enableProgressGesture"
      :object-fit="objectFit"
      :poster="poster"
      :show-mute-btn="showMuteBtn"
      :title="title"
      :play-btn-position="playBtnPosition"
      :enable-play-gesture="enablePlayGesture"
      :auto-pause-if-navigate="autoPauseIfNavigate"
      :auto-pause-if-open-native="autoPauseIfOpenNative"
      :vslide-gesture="vslideGesture"
      :vslide-gesture-in-fullscreen="vslideGestureInFullscreen"
      :ad-unit-id="adUnitId"
      :poster-for-crawler="posterForCrawler"
      @play="onPlay"
      @pause="onPause"
      @ended="onEnded"
      @timeupdate="onTimeUpdate"
      @fullscreenchange="onFullScreenChange"
      @waiting="onWaiting"
      @error="onError"
      @progress="onProgress"
      @loadedmetadata="onLoadedMetaData"
      @controlstoggle="onControlsToggle"
    ></video>
    <!-- #endif -->
  </view>
</template>

<script>
export default {
  name: "video-mix",
  props: {
    videoId: {
      type: String,
      default: "",
    },
    width: {
      type: String,
      default: "750rpx",
    },
    height: {
      type: String,
      default: "420rpx",
    },
    fileid: {
      type: String,
      default: "",
    },
    src: {
      type: String,
      default: "",
    },
    autoplay: {
      type: Boolean,
      default: false,
    },
    loop: {
      type: Boolean,
      default: false,
    },
    muted: {
      type: Boolean,
      default: false,
    },
    initialTime: {
      type: Number,
      default: 0,
    },
    controls: {
      type: Boolean,
      default: true,
    },
    danmuList: {
      type: Array,
      default() {
        return [];
      },
    },
    danmuBtn: {
      type: Boolean,
      default: false,
    },
    enableDanmu: {
      type: Boolean,
      default: false,
    },
    pageGesture: {
      type: Boolean,
      default: false,
    },
    // direction: {
    // 	type: Number,
    // 	default: undefined,
    // },
    showProgress: {
      type: Boolean,
      default: true,
    },
    showFullscreenBtn: {
      type: Boolean,
      default: true,
    },
    showPlayBtn: {
      type: Boolean,
      default: true,
    },
    showCenterPlayBtn: {
      type: Boolean,
      default: true,
    },
    enableProgressGesture: {
      type: Boolean,
      default: true,
    },
    objectFit: {
      type: String,
      default: "contain",
    },
    poster: {
      type: String,
      default: "",
    },
    showMuteBtn: {
      type: Boolean,
      default: false,
    },
    title: {
      type: String,
      default: "",
    },
    playBtnPosition: {
      type: String,
      default: "bottom",
    },
    enablePlayGesture: {
      type: Boolean,
      default: false,
    },
    autoPauseIfNavigate: {
      type: Boolean,
      default: true,
    },
    autoPauseIfOpenNative: {
      type: Boolean,
      default: true,
    },
    vslideGesture: {
      type: Boolean,
      default: false,
    },
    vslideGestureInFullscreen: {
      type: Boolean,
      default: true,
    },
    adUnitId: {
      type: String,
      default: "",
    },
    posterForCrawler: {
      type: String,
      default: "",
    },
  },
  emits: [
    "play",
    "pause",
    "ended",
    "timeupdate",
    "fullscreenchange",
    "waiting",
    "error",
    "progress",
    "loadedmetadata",
    "controlstoggle",
  ],
  data() {
    return {
      isVideoPlaying: false,
      videoContext: null,
    };
  },
  methods: {
    onPlay(e) {
      this.isVideoPlaying = true;
      this.$emit("play", e);
    },
    onPause(e) {
      this.isVideoPlaying = false;
      this.$emit("pause", e);
    },
    onEnded(e) {
      this.isVideoPlaying = false;
      this.$emit("ended", e);
    },
    onTimeUpdate(e) {
      this.$emit("timeupdate", e);
    },
    onFullScreenChange(e) {
      this.$emit("fullscreenchange", e);
    },
    onWaiting(e) {
      this.$emit("waiting", e);
    },
    onError(e) {
      this.isVideoPlaying = false;
      this.$emit("error", e);
    },
    onProgress(e) {
      this.$emit("progress", e);
    },
    onLoadedMetaData(e) {
      this.$emit("loadedmetadata", e);
    },
    onControlsToggle(e) {
      this.$emit("controlstoggle", e);
    },
    isPlaying() {
      return this.isVideoPlaying;
    },
    play() {
      this._log("play");
      this._fetchVideoContext().then(() => {
        this.videoContext.play();
      });
    },
    pause() {
      this._log("pause");
      this._fetchVideoContext().then(() => {
        this.videoContext.pause();
      });
    },
    stop() {
      this._log("stop");
      this._fetchVideoContext().then(() => {
        this.videoContext.stop();
      });
    },
    _fetchVideoContext() {
      const operation = () =>
        new Promise((resolve, reject) => {
          if (!this.videoContext) {
            // #ifdef MP-WEIXIN
            // 这里的cloudPlayer是在json配置上引入的插件子组件名
            const plugin = requirePlugin("cloudPlayer");
            console.log('requirePlugin("cloudPlayer"): ', plugin);
            this.videoContext = plugin.getContext(this.videoId);
            console.log(
              `plugin.getContext(${this.videoId}): `,
              this.videoContext
            );
            // #endif
            // #ifndef MP-WEIXIN
            // this是在自定义组件下,当前组件实例的this,以操作组件内 video 组件(在自定义组件中药加上this,如果是普通页面即不需要加)
            this.videoContext = uni.createVideoContext(this.videoId, this);
            console.log(
              "uni.createVideoContext(this.videoId, this): ",
              this.videoContext
            );
            // #endif
          }

          if (this.videoContext) {
            resolve(this.videoContext);
          } else {
            reject("videoContext is empty");
          }
        });
      return new Promise((resolve, reject) => {
        this.$utils
          .promiseRetry(operation, 500, 3)
          .then(resolve)
          .catch(reject);
      });
    },
    _log(msg) {
      console.log(`video-mix : ${msg}`);
    },
  },
};
</script>

<style lang="scss" scoped>
.video-mix-container {
}
</style>

上述代码中用到的工具方法:

// utils.js
/* Promise 包装好的 setTimeout */
export const promiseWait = (ms) => new Promise((r) => setTimeout(r, ms));
/**
 * Promise 重试
 * @param {Function} operation 操作函数
 * @param {Number} delay 时间间隔
 * @param {Number} retries 重试次数
 */
export const promiseRetry = (operation, delay, retries) =>
  new Promise((resolve, reject) => {
    return operation()
      .then(resolve)
      .catch((reason) => {
        if (retries > 0) {
          return promiseWait(delay)
            .then(promiseRetry.bind(null, operation, delay, retries - 1))
            .then(resolve)
            .catch(reject);
        }
        return reject(reason);
      });
  });

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有Android技术, 还有iOS, Python等文章, 可能有你想要了解的技能知识点哦~