- 省钱有道之 云开发环境共享小结
#前言 最近为了节省一点小程序的运营成本,一些没啥流量的小程序如果每个月也要19块略微有些肉疼(主要还是穷),研究了一下云环境共享,在这里简单做一下总结。 [图片] 这里有官方的小程序环境共享文档需提前了解一下,具体共享步骤按官方文档操作即可。 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/resource-sharing/introduce.html #注意点 共享环境有几个注意点大致如下: 1、必须是相同主体 2、开通了云开发环境的小程序可以共享给同主体的小程序、公众号,被共享方无需开通云开发环境 3、一个云开发环境最多可以共享给10个小程序/公众号 4、共享后双发均可主动解除 5、按官方文档要求,资源方需有云函数cloudbase_auth,测试时发现没有这个云函数其实也能正常运行,可能我验证的场景还不够多 6、云能力初始化的方式不同,资源方按传统的云环境初始化方式即可,也就是 wx.cloud.init({ env: env.activeEnv, traceUser: true }); 而调用方的初始化方式有所不同 const cloud = new wx.cloud.Cloud({ //资源方AppID resourceAppid, //资源方环境ID resourceEnv, }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 const initRes = await cloud.init(); 后续调用资源方的云函数就用这个cloud就行了:cloud.callFunction({...}); 7、调用方有操作到云存储文件的api也需要用6步骤中的cloud 8、云存储fileId需要用cloud.getTempFileURL转换成临时/永久链接,否则在调用方无法展示 9、一些api的云调用方式也有变化,需指明具体的appid。比如A小程序授权给了B小程序,想给B小程序推送客服消息需要写成 await cloud.openapi({appid:B小程序appid}).customerServiceMessage.send({...}); 10、获取调用方的appid/openid/unionid也有所不同 // 跨账号调用时,由此拿到来源方小程序/公众号 AppID console.log(wxContext.FROM_APPID) // 跨账号调用时,由此拿到来源方小程序/公众号的用户 OpenID console.log(wxContext.FROM_OPENID) // 跨账号调用、且满足 unionid 获取条件时,由此拿到同主体下的用户 UnionID console.log(wxContext.FROM_UNIONID) #适配 基于以上注意点,开始进行适配,由于我是一套代码部署N个小程序,然后一个云环境共享给其他小程序,希望通过配置决定哪个小程序作为资源方,哪些作为调用方 首先是云开发环境的初始化: 1、env.js 环境配置: //云开发环境 const cloudBase = { //使用共享云环境资源,资源方=false,调用方=true useShareResource: false, //资源方AppID resourceAppid: "wx9d2xxxxxxxx0088", //资源方环境ID resourceEnv: "prod-9gxqvi3qb3c257ef", //云环境ID prod: "prod-9gxqvi3qb3c257ef" } 2、api.js 操作模块 const env = require('../env.js'); let cloud; /** * 初始化云能力 * @returns {Promise} */ const wxCloudInit = async function () { const {cloudBase} = env; if (!wx.cloud) { console.error('请使用 2.2.3 或以上的基础库以使用云能力') } else if (cloudBase.useShareResource) { const {resourceAppid, resourceEnv} = cloudBase; // 声明新的 cloud 实例 cloud = new wx.cloud.Cloud({ //资源方AppID resourceAppid, //资源方环境ID resourceEnv, }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 const initRes = await cloud.init(); console.log("初始化云能力完毕:", initRes, "资源方appid:", resourceAppid, "资源方环境ID:", resourceEnv); } else { wx.cloud.init({ env: env.activeEnv, traceUser: true }); console.log("初始化云能力完毕,当前环境:", env.activeEnv); cloud = wx.cloud; } this.cloud = cloud; } /** * 云函数调用 * @param name * @param data * @param success * @param fail * @param complete */ const callCloudFunction = function (name, data, success, fail, complete) { //执行云函数 cloud.callFunction({ // 云函数名称 name: name, // 传给云函数的参数 data: Object.assign({}, data, {env: env.activeEnv}) }).then(res => { typeof success == 'function' && success(res); }).catch(res => { typeof fail == 'function' && fail(res); }).then(res => { typeof complete == 'function' && complete(res); }); }; 3、在app.js中初始化云环境,后续有用到wx.cloud的都需要改成api.cloud const api = require('utils/api.js'); App({ onLaunch: async function (options) { await api.wxCloudInit(); } }); 其次是资源方的获取用户信息调整 每次都要判断wxContext.FROM_OPENID是否为空,不为空则是调用方的用户信息,为空则是资源方的用户信息,略微繁琐,干脆封装了一个npm包wx-server-inherit-sdk,改造了一下getWxContext函数,源码如下,引入这个包后也就可以不用引入官方的wx-server-sdk const cloud = require('wx-server-sdk'); // 保存原始getWXContext方法到另一个变量 const originalGetWXContext = cloud.getWXContext; cloud.getWXContext = function () { //调用原始getWXContext方法 const wxContext = originalGetWXContext.call(this); const {FROM_APPID, FROM_OPENID} = wxContext; //云开发环境共享时获取到的APPID会替换成源方APPID if (FROM_APPID) { Object.assign(wxContext, {APPID: FROM_APPID}); } //云开发环境共享时获取到的OPENID会替换成源方OPENID if (FROM_OPENID) { Object.assign(wxContext, {OPENID: FROM_OPENID}); } return wxContext; } module.exports = cloud; 到此也就大功告成。为了省钱也是够折腾的[哭笑]
2023-08-28 - 小程序帐号登录规范要求与修改指引
为更好地保护用户隐私信息,优化用户体验,平台对小程序内的帐号登录功能进行规范。“帐号登录功能”是指开发者在小程序内提供帐号登录功能,包括但不限于进行的手机号登录,getuserinfo形式登录、邮箱登录等形式。一、登录规范规则,你需要了解:[图片] 一、「体验范围开放」与「体验范围特定」区分与对应整改建议:1、“体验范围开放”定义:①直接打开即可体验 ②有账号限制,但有注册流程是对外开放 整改建议: ①授权个人信息功能后置,给与用户充分了解、体验功能服务后,再由用户主动点击进一步功能触发登录授权 ②授权同时,亦同时支持给与用户取消登录的权利 案例解析: ①范围开放-登录规范违规案例解析 违规点:服务范围开放,首页打开即要求授权登录,用户未体验功能服务强制要求授权登录,登录规范不合规。 [图片] ②范围开放-登录环节无取消/拒绝登录按钮案例解析 违规点:体验范围开放,用户体验功能服务后自主触发登录,提供取消/拒绝登录按钮,但点击取消/拒绝登录按钮无响应,仍强制要求登录,无法取消/拒绝登录。 [图片] 2、体验范围特定 定义: 体验范围提供给特定人员使用、对外无开放注册流程,例如为学校系统、员工系统、社保卡信息系统等提供特定服务的小程序 整改建议: ①首页有明显的使用范围(特定范围)说明 ②首页即要求授权来拉取身份鉴定类,需要为用户提供暂不登录/取消登录选项按钮 案例解析: ①特定范围-登录违规案例解析 违规点:打开首页即要求授权登录,无任何说明,不符合登录规范要求 [图片] 这是一份动态更新的文档,辅助开发者了解登录规范要求,避免开发者因登录规范问题审核失败导致无法按期发布上线,开发者如有其他疑问,可以通过目前开放的咨询渠道反馈: 1、微信开放社区-交流专区-小程序发帖咨询-提出问题-运营相关问题 2、驳回站内信通知-客服咨询入口(MP代码审核客服入口正处于灰度开放中,若未获得灰度测试入口,开发者可前往社区发帖咨询) 我们会根据新出现的问题、相关法律法规更新或产品运营的需要及时对其内容进行修改并更新,制定新的规则,保证微信用户的体验。建议开发者反复查看以便获得最新信息,定期了解更新情况。
2020-12-24 - 小程序开发进阶之前端开发
4 节课,教你快速入门小程序前端。本系列视频,由腾讯课堂NEXT学院、微信学堂联合出品。
2021-12-14 - 小程序切换后台后,不再使用 setData
[视频] 你好,我是李艺。 上节课我们主要学习了如何使用串发复合命令延迟调用代码,这节课学习一个小技巧,就是在小程序切换至后台以后关闭对setData的一个调用。 首先我们看一个问题,前面我们在了解小程序的启动流程的时候已经知道了启动有两种一种是冷启动,一种是热启动,当小程序进入后台状态的时候,这个时候会有5秒的一个暂停状态,在这个状态里面小程序的代码仍然是可以运行的,如果5秒内小程序没有被唤起,这个时候它就会进入一种挂起状态,在挂起状态中小程序的JS代码不会运行,在后台状态里面小程序已经不可见,此时所有的与视图有关的代码,像setData的一个调用已经没有必要进行执行了,下面我们看项目实践。 首先看实践一,监听App进入后台的一个事件。 在这个小程序里面使用wx.onAppHide可以监听小程序进入后台的事件,或者是我们在App的周期函数里面,在它的onHide周期函数里面也可以捕捉小程序进入后台的一个时机,这个时候就可以停止页面里面对所有的setData的一个操作了。为了方便控制这个页面对象里面的setData方法,我们需要劫持Page对象,怎么样去劫持,首先需要在library目录下面创建一个page.js文件,在这个文件里面劫持onLoad和setData方法,因为setData方法只有在运行的时候才可以拿到引用,所以在这个地方先劫持onLoad的方法,然后在onLoad的方法里面再获得setData方法的一个引用,接着再往下需要在app.js文件里面调用并应用page.js的代码,使其劫持发生作用,最后在首页的onReady这个方法里面可以写一个定时器,不断地去调用setData去测试我们新编写的功能,在测试区的里面AppData面板里面,我们可以看到data数据属性它一直都在变化,在模拟器里面模拟单击Home键可以使小程序处于一种后台的状态,此时setData就不再更新了,AppData面板里面的数据也不再变化了。 接下来我们看实践一的代码演示。 要完成对setData方法的一个劫持,我们需要对我们当前的页面对象也是page进行一个改写,怎么样去改写,先看一下我们最终源码,在library下面有一个page.js文件,这是我们接下来要编写的一个文件,我们将这个文件先拷贝一下放到我们目前的项目下面,也放在同样的目录下 library下面,我们看一下这个代码主要是干了什么事情。 第一步首先是我们有一个常量的赋值,需要将我们原本的小程序本身,它原生的Page对象要做一个引用的一个保存,稍后因为这个地方我们可能还要用到,然后接下来我们会导出,有一个方法的导出,在这个里边,首先要从我们的options,options其实是我们这个页面里面我们自己设置的一些信息,包括data对象还有这些方法都在这个对象里面,然后我们要取到onLoad这个方法以后,接下来我们就开始重写这个方法了。 重写方法以后我们可以看到在这个地方,我们拿到了这个setData的一个引用,它是从this对象里面进行一个解构赋值,然后拿到这样一个引用的,在这个地方this其实就是我们当前的页面对象的一个实例,因为在这个地方它这个代码已经运行了,指的就是它的一个实例,再往下我们重新定义了setData的方法了,然后value是它的一个值,在这个地方我们看一下,我们用到了一个全局的状态就是global.state,现在是我们自定义的,现在它还没有值或者说它默认的它是undefined,在这个地方我们要检查,如果是这个状态等于hide,也就是它切到后台状态,非后台状态 这个地方有一个非的判断对吧,我们就调用setData方法对不对,如果是切到这个后台状态,那就是把我们这个调用给它忽略掉,这就是它的一个方法,这个地方有对于我们原来的方法就是一个再调用,我们把现在的方法放在前面执行,再执行具体的页面里边的自定义的那些代码逻辑,这就是我们整个的文件它的主要的一个代码,大概是这样的一个实现。 接下来我们看怎么样去应用这样的一个文件,首先看我们app.js里面,在这个地方有一个引入,我们将这个代码拷贝一下放在我们的app.js文件里面,放在这个页面的顶部 放在这个位置,这个地方我们看一下,当前其实它相当于是对我们当前的上下文执行环境里面的Page类型 类对象进行了一个重写,这个写法跟我们原来的普遍的一个写法有点不太一样,加载完成要取到default,然后将导出来的新的类对象然后赋值给它,这就完成了一个劫持,这个劫持完成以后为了测试效果,我们还需要写一个测试代码。 看一下我们这个里边是不是有一个叫做定时器,在这个地方有一个定时器,这个代码我们写到了onReady周期函数里面,找到首页,再找到它的onReady周期函数然后切到这个地方,这个代码稍后我们会删除,所以暂时就放在这个地方,然后这个里面我们做了一个什么事情,每隔500毫秒去设置当前这个页面上的一个名称,为xxx的这样的一个数据属性,当然这个数据属性在我们目前这个页面上它不存在也没有关系,不存在的话我们设置之后它会自动地去新增的,这地方设置这个方法调用就是setData(AppData),因为我们现在对setData有劫持,稍后我们再切换到后台的时候可以看到它会发挥作用,这个代码搞完了。 接下来我们开始在微信开发者工具里面进行测试,单击编译看它的一个表现,已经启动了,调试区没有错误,现在我们看一下AppData面板。看这个 这有一个三个x,有个数据属性,然后它后面这个值一直都在变化,它为啥会变化,因为我们里面有一个定时器500毫秒一个定时器,然后不断对它进行修改,所以它这个地方一直会变。现在我们在模拟器里面,我们选的这个地方有一个菜单,然后选择模拟操作里面有一个我们选择Home,当我们按下Home的时候,其实相当于在手机上按下Home键,这个时候小程序它会切到后台的状态,我们选这个不太明显对吧,重启了一下,我们刚才点了重启的按钮现在已经重新启动了,那么单击完以后这个不要去选了,因为它选了以后它又回到了那个状态。 项目在重启以后,我们看到我们小程序在切到后台状态以后,从我们AppData面板里面我们一直都可以看到三个x的数据属性一直都在变化,可以现在再打开模拟操作 选择Home,当这个面板浮现的时候其实已经代表无论我们接下来选哪一个,其实已经代表我们小程序已经进入一个后台状态了,但是此时我们看到AppData面板里面它这个数据属性仍然在发生改变,为什么仍然在发生改变,为啥发生改变,因为我们先前页面劫持里边用到了一个全局的数据属global.state对吧,现在global.state我们还没有设置,肯定它们不能发挥作用,所以接下来我们要在我们的AppData里面要添加程序的周期函数,然后去设置全局属性。 看一下我们最终的源码,找到app.js,在这个里边有两个方法在这里,一个是onShow,另外一个是onHide,下面这个是onUnhandledRejection,这个是什么意思呢,我们在用了Promise编程方式以后,它有一些没有捕捉到的一些reject的异常,我们可以捕捉以后在这个地方进行打印,这是它的一个作用,所以这三个方法我们都可以给它拷贝过来放在我们目前的项目里面,找到app.js放在下面 最下面就可以了,所有的周期函数我们都往下放 这样就可以了,这个代码修改完了 我们再次刷新项目看一下它的运行效果,现在我们看到这个数据属性一直在发生变化,这个时候我们单击模拟操作,然后选择Home,我们看到这个数据属性现在不在变化了,同时在这个地方,我们可以看到这个地方 setData没有作用,这个打印是在哪里打印的,就在我们劫持代码里面对不对,当我们选择一个入口方式以后,我们这个代码又开始恢复调用了,这个时候我们可以看到它定时器又起作用了,这样我们就实现了,对我们的所有页面里面的一个setData方法的自动的一个劫持,只要我们这个程序进入后台状态,无论是哪个页面里边对setData的一个调用都已经受到了我们的管制了,最后我们将我们的测试代码给它注掉。因为本身我们这个程序不需要这个测试代码,这个代码演示就到这里。 最后我们总结一下,我们想实现对setData方法的一个劫持,除了劫持原生的Page类对象以外,我们还可以定义一个工具函数 例如叫mySetData,在原来所有调用setData的地方改为调用mySetData,但是这种方式它比较繁杂,一般我们不这样采用对于原生的类对象的劫持,我们在使用的时候一定要克制,如果必须要发生劫持的话一定要把劫持代码统一放在程序的入口处,这样方便我们项目的其他维护者看到,如果是默默劫持了原生的类对象 而不加说明,这可能会给其他人 这个代码带来一些问题,导致他们的代码出错,这不是一个优雅的程序员所为。这节课我们就讲到这里,上面我们现在看到的这些网址是我们本节课涉及到的一些文档地址。 点击查看开放文档: 小程序运行机制框架接口 /小程序 App /App 这节课我们主要学习了如何劫持Page类对象以及如何在小程序进入后台状态的时候,不再真正的调用setData方法,下节课学习如何实现数据预拉取与周期性更新。 最后我们说一下思考题。这里有一个问题请你思考一下,在第一课我们讲启动流程的时候我们曾经介绍过微信特意为开发者提供了数据预拉取和周期性数据拉取的这样一种优化机制,对于一些大块量的这些数据,我们不需要开发者自己拉取了,微信可以代为拉取,拉取以后再将得到的数据再提供小程序直接使用,这种贴心的机制你知道它怎么使用吗?这个问题先留给你思考一下,下节课我们一起深入探讨一下这个问题。
2022-07-14 - 这些 Canvas 小技巧,保证你新年用得上
来自「微信开发者」公众号,作者为微信小程序技术研发工程师binnie。 本文主要介绍了3个隐藏的 Canvas 小技巧: - 绘制并生成图片 - Video 绘制 Canvas / webgl - 视频解码并绘制到 webgl - 录制并导出 webgl 视频 一键加滤镜 快速合成音视频 轻松挑选视频封面 …… Canvas 能够做这些? 作为资深的开发者,相信大家对 Canvas 都不陌生。这项能力在绘制图形方面发挥着极大的作用,高效支持图片编辑、数据可视化等应用场景。但是只局限于一般能力应用,那格局就小了。 Canvas 的应用场景非常丰富!赶紧往下看看这些隐藏的 Canvas 小技巧,保证你新年用得上!还有手把手教程以及文末彩蛋哟。 -- • 绘制并生成图片 • -- [图片] 示例:新年模板长按保存祝福 适用场景:图片分享海报 相关 API:RenderingContext/Canvas/wx.canvasToTempFilePath Step 1: 创建实例获取对象 创建 Canvas 实例,获取 CanvasRenderingContext2D 对象(Canvas 绘图上下文)来绘制形状、文本、图像等。 const query = wx.createSelectorQuery() let canvas = null query.select('#myCanvas') .fields({ node: true, size: true }) .exec((res) => { // 通过 wx.createSelectorQuery 获取到 canvas 实例 canvas = res[0].node // 通过 canvas.getContext('2d') 获取 CanvasRenderingContext2D 对象 const ctx = canvas.getContext('2d') }) Step 2: 设置宽高调整图片 获取 Canvas 绘图上下文后,将 Canvas 的宽高设置为节点宽高 * 设备像素比,绘制出来的图片更清晰 // 获取设备像素比 const dpr = wx.getSystemInfoSync().pixelRatio // 将 canvas 宽高设置为 canvas.width = res[0].width * dpr canvas.height = res[0].height * dpr Step 3: 绘制内容 使用 CanvasRenderingContext2D 绘制,根据业务需要在画布中绘制头像、文字、背景等 // 矩形 ctx.fillStyle = '#FFFFFF' ctx.fillRect(0, 0, canvas.width , canvas.height ) // 图片 var image = canvas.createImage() himage.src = 'https://example.com/example.jpg' headImage.onload = (res) => { ctx.drawImage(himage 0, 0, 32, 32; } // 文本 ctx.font = "18px SimHei"; ctx.textAlgin = "left" ctx.fillStyle = "#07c160"; ctx.fillText("这是我的名字", 0, 0); Step 4: 生成并保存本地 使用 wx.canvasToTempFilePath 将画布生成图片,wx.saveImageToPhotosAlbum 将图片保存到本地。 wx.canvasToTempFilePath({ canvas: canvas, // canvas 实例 success(res) { // canvas 生成图片成功 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success(res) { // 保存成功 } }) } }) -- • Video 绘制 Canvas / webgl • -- [图片] 示例:视频文件绘制 Canvas 适用场景:制作 Video 滤镜、挑选 Video 封面等 相关 API:RenderingContext/Canvas Step 1: 获取实例 通过 wx.createSelectorQuery 获取 VideoContext 实例 let video = null wx.createSelectorQuery().select('#video').context(res => { // 通过 wx.createSelectorQuery 获取 VideoContext 实例 video = res.context; }) Step 2: 绘制内容 获取 VideoContext 实例后,将 VideoContext 传递给 Canvas 进行绘制。开发者根据业务需求选择绘制类型: Canvas 2d 写法:canvas.drawImage(video, ...)webgl 写法:gl.texImage2D(..., video) wx.createSelectorQuery().selectAll('#myCanvas,#webglCanvas').node(res => { const ctx = res[0].node.getContext('2d') const gl = res[1].node.getContext('webgl') setInterval(() => { // canvas 2d // 将 video 纹理对象传入 drawImage 进行绘制 ctx1.drawImage(video, 0, 0, w * dpr, h * dpr); // 添加一个蒙层 ctx1.fillStyle = 'rgba(0, 0, 0, 0.3)' ctx1.fillRect(0, 0, w * dpr, h * dpr); // webgl const render = createRenderer(res[1].node, w, h) render(new Uint8Array(ctx1.getImageData(0, 0, w * dpr, h * dpr).data), w * dpr, h * dpr) }, 1000 / 24) }).exec() function createRenderer(canvas, width, height) { const gl = canvas.getContext("webgl") ... return (arrayBuffer, width, height) => { ... // 指定二维纹理图像 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, arrayBuffer) gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0) } } -- • 视频解码并绘制到 webgl • -- [图片] 示例:视频一键解码并绘制到 webgl 适用场景:添加特效、贴图等视频编辑场景 相关 API:wx.createVideoDecoder/VideoDecoder/RenderingContext/Canvas.requestAnimationFrame/wx.createMediaAudioPlayer/MediaAudioPlayer Step 1: 创建视频解码器进行解码 1. 调用 createVideoDecoder 对视频进行解码 2. 使用 videodecoder.start 启动解码,视频源文件不限制本地或远程路径 3. 通过 videodecoder.on('start', res => {}) 监听解码,通过 videodecoder.getFrameData() 获取到解码数据 // 获取视频解码器 getVideoDecoder(source, abortAudio) { return new Promise((resolve, reject) => { // 创建视频解码器 videodecoder = wx.createVideoDecoder() // 开始解码 videodecoder.start({ abortAudio: abortAudio, source: source, // 视频源文件,支持本地路径&远程路径 mode: 0 // 按pts解码,保证音画同步 }) // 监听解码 开始 videodecoder.on('start', res => { console.log('videodecoder start', res) // 状态初始化 isStop = false resolve(videodecoder) }) // 监听解码 结束 videodecoder.on('ended', res => { // 状态设置为结束,停止画面录制器 isStop = true }) }) }, Step 2: 解码数据绘制到 webgl 1. 通过 gl.texImage2D(..., image) 将解码数据绘制到 webgl 2. 使用 webgl.requestAnimationFrame 继续绘制,效果更加流畅 // 将解码数据绘制到 webgl 中 const query = wx.createSelectorQuery() query.select('#webglCanvas').node().exec((res) => { const webgl = res[0].node const requestAnimationFrame = webgl.requestAnimationFrame; // 初始化webgl let render = null if (!render) { render = createRenderer(webgl, 600, 400) } /** * 绘制视频帧到 canvas */ let i = 1 let loop = () => { // 解码结束,停止循环 if (isStop) { return } // 获取解码数据,绘制到 webgl 中 const imageData = videodecoder.getFrameData() if (imageData) { // render 的高宽需要设置为图片的宽高才可以绘制出来 render(new Uint8Array(imageData.data), imageData.width, imageData.height) } // 继续绘制 console.log('绘制帧数:', i++) requestAnimationFrame(loop) } // 启动录制循环 requestAnimationFrame(loop) }) Step 3: 添加音频播放器同步播放音频 完成 Step2 后,webgl 只有视频播放,缺少音频。因此使用 wx.createMediaAudioPlayer(),支持 addAudioSource 传入 videodecoder,保证视频帧渲染音画同步 /** * 创建媒体音频播放器 */ let mediaAudioPlayer = null let addAudio = () => { if (mediaAudioPlayer) return mediaAudioPlayer = wx.createMediaAudioPlayer() mediaAudioPlayer.start().then(() => { // 添加播放器音频来源 mediaAudioPlayer.addAudioSource(videodecoder).then(res => { console.log('add mediaAudioPlayer: ',) }) }) } // render 绘制视频同时添加音频 render(new Uint8Array(imageData.data), imageData.width, imageData.height) addAudio() -- • 录制并导出 webgl 视频 • -- [图片] 示例:录制并一键导出 webgl 视频 适用场景:将动画、编辑过的视频导出视频文件保存 相关 API:wx.createMediaRecorder/MediaRecorder/wx.createMediaContainer/MediaContainer/MediaTrack Step 1: 创建 webgl 画面录制器进行录制 通过 createMediaRecorder 创建页面录制器,并且绑定 webgl(建议离屏状态,效果更好)进行录制 /** * 获取画面录制器 */ getRecorder() { let canvas = this.getMainCanvasNode() let recorder = wx.createMediaRecorder(canvas, { fps: choosedVideoInfo.fps, // 实际视频的 fps videoBitsPerSecond: choosedVideoInfo.bitrate, // 实际视频的 bitrate gop: 12 }) // 监听录制事件 recorder.on("timeupdate", (res) => { console.log('recorder 录制中,当前时间:', res.currentTime) }) recorder.on("stop", (res) => { console.log('recorder停止') this.saveMedia(res.tempFilePath) }) // 开始录制 recorder.start() this.recorder = recorder return recorder }, // 初始化 画面录制器 并进行录制 await this.initRenderer() this.getDecoder().then((decoder) => { let recorder = this.getRecorder() var self = this function loop() { if (self.stopped) { return } let frameData = decoder.getFrameData() if (!frameData) { console.log('没取到帧') setTimeout(() => { loop() }, 1000/60) } else { self.renderFrame(frameData) recorder.requestFrame(() => { console.log('录制帧数:', i++) loop() }) } } loop() }) Step 2: 添加音频合成音视频 1. 通过 createMediaContainer 创建音视频处理容器来合成音视频 2. 通过 MediaContainer.extractDataSource 将视频源分离出视频轨道和音频轨道,将需要的轨道通过 MediaContainer.addTrack 添加到容器中 3. 通过 MediaContainer.export 导出即可获得合成后的视频文件 /** * 将视频和音频合到一起并保存到本地 * @param {*} videoTempFilePath */ saveMedia(videoTempFilePath) { const self = this let choosedFile = this.choosedFile const MediaContainer = wx.createMediaContainer() // webgl的取视频 MediaContainer.extractDataSource({ source: videoTempFilePath, success(res) { MediaContainer.addTrack(res.tracks[0]) // 源视频取音频 MediaContainer.extractDataSource({ source: choosedFile, success(res) { // 拿到音频轨道并加入到容器 res.tracks[0].kind == 'audio' && MediaContainer.addTrack(res.tracks[0]) res.tracks[1].kind == 'audio' && MediaContainer.addTrack(res.tracks[1]) // 合成视频并导出视频文件 MediaContainer.export({ success(res) { // 保存视频到本地 wx.saveVideoToPhotosAlbum({ filePath: res.tempFilePath, success() { wx.showToast({ title: '导出成功', icon: 'success', duration: 2000 }) self.destroy() } }) } }) } }) } }) }, -- •高效图像处理彩蛋 • -- 学会以上这些 Canvas 小技巧,还担心新年的美图美照美视频处理不过来?赶紧码下这个 Canvas 代码包,保证你就是家里最闪耀的靓女靓仔。 预祝大家新的一年 Canvas 在手,红包一直有!
2022-03-24