uniapp - 腾讯云点播小程序插件
# 一、简介
微信小程序播放教育类视频要求具备有相关资质,但这些资质一般公司很难短时间申请下来(甚至有的公司压根就申请不了),而【短视频播放器小程序插件】含有《信息网络传播视听节目许可证》的资质证书备案,可以利用该插件来解决资质问题,相关截图如下:
图片来源:https://cloud.tencent.com/document/product/266/36849 (opens new window)
采购流程于技术无关,以下内容着重讲解如何集成该微信小程序插件。
注:【短视频播放器小程序插件】授权费 3 万/年(有 14 天试用 Licence),如果有购买腾讯云其他服务的话,满足一定条件,会赠送 1 年 免费使用 Licence,详情找腾讯云客服咨询(2022 年 04 月如此,赠送情况可能随时会变)。
# 二、使用
- 激活:在腾讯云控制台激活插件 Licence 之后,才能正常使用该播放器插件。
- appid:后面插件用到的 appid 需要在【腾讯云控制台】>账号信息中查看获取。
- 云点播短视频播放器-开发文档:https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx116d0dd5e6a39ac7&lang=zh_CN (opens new window)
# 1、绑定插件
因为微信小程序插件没有实质代码或 SDK,所以无法在本地添加集成,需要在微信小程序平台,将 小程序AppID
与 插件AppID
进行绑定(即给小程序添加插件),开发者工具在编译时会自动引入,绑定有 2 种方式:
- 方式 1:登录微信小程序平台,在
设置-第三方设置
中找到添加插件
,输入插件 AppID(wx116d0dd5e6a39ac7
)搜索并添加:
- 方式 2:在 云点播短视频播放器文档 (opens new window) 页面直接点击
添加插件
:
# 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 配置项说明:https://uniapp.dcloud.io/collocation/manifest.html (opens new window)
# 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 配置项说明:https://uniapp.dcloud.io/collocation/pages.html#style (opens new window)
声明完哪个页面需要使用插件播放器之后,就可以在那个页面的布局文件中使用插件播放器了:
// 在wxml里插入
<cloud-player
appid="xxxxx"
fileid="xxxxxxxx"
playerid="myPlayerId"
></cloud-player>
注:微信小程序原生工程中,页面是
wxml
文件,uniapp 工程中是vue
文件。另外,目前这种声明方式只对单个配置过的页面有效,也就是说,如果其他页面也需要使用插件播放器,还需要在其他页面的style
中单独进行配置,这就很麻烦了,不过现在不用烦恼,后面会解决这个问题。
# 4、组件内使用播放器
为了功能复用,以及方便代码维护,实际开发中,往往会自定义组件,对常用的布局、功能进行封装。微信小程序原生工程可以在自定义组件的 json 文件中进行配置,跟页面相同的 usingComponents
配置即可:
官方的 云点播短视频播放器-开发文档 (opens new window) 只说明在了如何在网页中使用插件,但没有对组件中如何使用插件进行说明,很无语,希望后续官方能完善一下。另外,这是我发起工单询问之后,腾讯技术售后给我的 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 使用小程序原生组件:https://uniapp.dcloud.io/tutorial/miniprogram-subject.html#小程序自定义组件支持 (opens new window)
仔细想想,这个方案是有问题的。首先,对不熟悉微信小程序原生开发的人很不友好,其次,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:https://uniapp.dcloud.io/api/media/video-context.html#createvideocontext (opens new window)
但是,uni.createVideoContext(videoId, this)
对腾讯云点播插件无效,需改用 requirePlugin(pluginName).getContext(videoId)
来获取,例如:
const plugin = requirePlugin("cloudPlayer");
let player = plugin.getContext("myVideo");
该解决方案源自一篇社区帖子 《腾讯云点播 wx.createVideoContext("myVideo").pause()无法暂停》:https://developers.weixin.qq.com/community/develop/doc/0004eae9e6cd08e31d6d827e657800 (opens new window)
# 三、封装
腾讯云点播插件 <cloud-player>
与默认的 <video>
标签在使用上差异不多,就以下几点:
<cloud-player>
使用时需要配置appid
属性。<cloud-player>
使用时需要配置width
和height
属性。<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>
标签,于是设计为fileid
和src
共存。
以下是 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);
});
});
- 01
- Flutter - 危!3.24版本苹果审核被拒!11-13
- 02
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21
- 03
- Flutter - 轻松实现PageView卡片偏移效果09-08