- 小程序音视频合成初探
小程序音视频 最近使用了一下微信音视频相关 api, 这绝对是一件振奋人心的事, 让视频的合成在小程序端就能完成, 以下是我使用这些 api 的一些记录。 以下代码片段可以直接在开发者工具中预览 视频合成 音视频合成主要用到 [代码]wx.createMediaContainer()[代码] 方法, 该方法会返回一个 [代码]MediaContainer[代码] 对象, 可以将视频源传入到容器中, 对视频轨和音频轨进行一些操作。 下面以合成视频为例: [代码]const mediaContainer = wx.createMediaContainer(); const { tempFilePath } = this.data; // 本地视频文件地址 // 将视频源传入轨道当中 mediaContainer.extractDataSource({ source: tempFilePath, success: (res) => { // 返回的结果中的 tracks 是一个类数组对象, 第一项是音频轨道, 第二项是视频轨道 const [audioTrack, mediaTrack] = res.tracks; mediaContainer.addTrack(mediaTrack); // 将视频轨道加入到待合成容器中 mediaTrack.slice(1000, 5000); // 截取视频轨道中第 1-5 秒的视频 mediaContainer.addTrack(audioTrack); // 将音频轨道加入到待合成容器中 audioTrack.slice(1000, 5000); // 截取音频轨道中第 1-5 秒的视频 // 导出合成容器中的音频和视频 mediaContainer.export({ success: (res) => { // 拿到导出之后的视频 console .log(res.tempFilePath); }, }); }, }); [代码] 视频解码和录制 如果想要给视频加滤镜和贴图可以采用, [代码]VideoDecoder[代码] + [代码]MediaRecorder[代码] + [代码]WebGL[代码] 的方式, 通过 [代码]VideoDecoder[代码] 将视频解码, 获取视频的每一帧画面, 再绘制到 [代码]canvas[代码] 上, 再通过 [代码]glsl[代码] 着色器给画面加滤镜。同时用 [代码]MediaRecorder[代码] 去录制 [代码]canvas[代码] 上的画面, 最后可以导出一段视频。 以下是将一个视频的前十秒加上黑白滤镜合成出来的主要代码: [代码]let w = 300 let h = 200 const vs = ` attribute vec3 aPos; attribute vec2 aVertexTextureCoord; varying highp vec2 vTextureCoord; void main(void){ gl_Position = vec4(aPos, 1); vTextureCoord = aVertexTextureCoord; } ` const fs = ` varying highp vec2 vTextureCoord; uniform sampler2D uSampler; #ifdef GL_ES precision lowp float; #endif void main(void) { vec4 color = texture2D(uSampler, vTextureCoord); float gray = 0.2989*color.r + 0.5870*color.g + 0.1140*color.b; gl_FragColor = vec4(gray, gray, gray, color.a); } ` const vertex = [ -1, -1, 0.0, 1, -1, 0.0, 1, 1, 0.0, -1, 1, 0.0 ] const vertexIndice = [ 0, 1, 2, 0, 2, 3 ] const texCoords = [ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 ] function createShader(gl, src, type) { const shader = gl.createShader(type) gl.shaderSource(shader, src) gl.compileShader(shader) if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error('Error compiling shader: ' + gl.getShaderInfoLog(shader)) } return shader } function createRenderer(canvas, width, height) { const gl = canvas.getContext("webgl") if (!gl) { console.error('Unable to get webgl context.') return } const info = wx.getSystemInfoSync() gl.canvas.width = width //info.pixelRatio * width gl.canvas.height = height // info.pixelRatio * height gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight) const vertexShader = createShader(gl, vs, gl.VERTEX_SHADER) const fragmentShader = createShader(gl, fs, gl.FRAGMENT_SHADER) const program = gl.createProgram() gl.attachShader(program, vertexShader) gl.attachShader(program, fragmentShader) gl.linkProgram(program) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error('Unable to initialize the shader program.') return } gl.useProgram(program) const texture = gl.createTexture() gl.activeTexture(gl.TEXTURE0) gl.bindTexture(gl.TEXTURE_2D, texture) gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) gl.bindTexture(gl.TEXTURE_2D, null) buffers.vertexBuffer = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, buffers.vertexBuffer) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertex), gl.STATIC_DRAW) buffers.vertexIndiceBuffer = gl.createBuffer() gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.vertexIndiceBuffer) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vertexIndice), gl.STATIC_DRAW) const aVertexPosition = gl.getAttribLocation(program, 'aPos') gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0) gl.enableVertexAttribArray(aVertexPosition) buffers.trianglesTexCoordBuffer = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, buffers.trianglesTexCoordBuffer) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW) const vertexTexCoordAttribute = gl.getAttribLocation(program, "aVertexTextureCoord") gl.enableVertexAttribArray(vertexTexCoordAttribute) gl.vertexAttribPointer(vertexTexCoordAttribute, 2, gl.FLOAT, false, 0, 0) const samplerUniform = gl.getUniformLocation(program, 'uSampler') gl.uniform1i(samplerUniform, 0) return (arrayBuffer, width, height) => { gl.bindTexture(gl.TEXTURE_2D, texture) 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) } } wx.createSelectorQuery().select('#video').context(res => { const video = this.video = res.context // 获取 VideoContext video.pause() const decoder = wx.createVideoDecoder() // 创建解码器 const dpr = 1 const webgl = wx.createOffscreenCanvas() const render = createRenderer(webgl, w, h) console.log('webgl', webgl.id, w, h, dpr, webgl.width, webgl.height) const fps = 25 const recorder = wx.createMediaRecorder(webgl, { fps, videoBitsPerSecond: 3000, timeUpdateInterval: 30 }) const startTime = Date.now() let counter = 0 let loopStopped = false let timeCnt = 0 setTimeout(() => { recorder.stop(); decoder.stop(); }, 3000); function loop() { const renderTime = (counter + 1) * (1000 / fps) if (loopStopped || renderTime > 10100) { console.log('recorder stop.', timeCnt, Date.now() - startTime) recorder.stop() return } const ts = Date.now() const imageData = decoder.getFrameData() if (imageData) { render(new Uint8Array(imageData.data), w * dpr, h * dpr) timeCnt += Date.now() - ts counter++ } console.log('render end', counter, Date.now() - ts); recorder.requestFrame(loop) } recorder.on('start', () => { console.log('start render') loopStopped = false loop() }) recorder.on('timeupdate', ({ currentTime }) => { console.log('timeupdate', currentTime) }) recorder.on('stop', (res) => { console.log('recorder finished.', timeCnt, Date.now() - startTime, res) this.setData({ distSrc: res.tempFilePath }) recorder.destroy() }) decoder.on('start', () => { console.log('decoder start 2', decoder.seek) decoder.on('seek', () => { console.log('decoder seeked') recorder.start() }) decoder.seek(0) }) decoder.start({ source: this.data.src, // 这里是一个视频的本地路径, 可通过 wx.chooseVideo 获取 }) }).exec() [代码] 不过此时录制出来的视频是没有声音, 需要通过上面讲到的 [代码]MediaContainer[代码] 截取前 10s 的音频, 将录制出的视频和音频合成得到一段完整的视频。[代码]CanvasRenderingContext2D[代码] 的 [代码]drawImage[代码] 方法 2.10.0 起支持传入通过 [代码]SelectorQuery[代码] 获取的 video 对象, 所以对视频的操作可以先使用 canvas 预览, 再使用 [代码]MediaRecorder[代码] 进行录制。 小程序音视频相关 api 就介绍到这儿, 更多具体的文档, 可以参考微信官方文档。这些 api 大大方便了开发者对音视频的处理, 也期待更多小程序音视频 api 的开放。
2020-07-01 - 小程序下WebGL截图
最近做了关于小程序下WebGL截图的项目,遇到一些坑,记录下。 安卓切换页面后返回,再一次获取像素为空 IOS偶尔获取到的为空白图像 获取到的数据绘制出来后上下颠倒 翻THREE.JS的代码,发现正确的截图方法 https://github.com/mrdoob/three.js/blob/master/src/renderers/WebGLRenderer.js#L1903 [代码]if ( _gl.checkFramebufferStatus( _gl.FRAMEBUFFER ) === _gl.FRAMEBUFFER_COMPLETE ) { // the following if statement ensures valid read requests (no out-of-bounds pixels, see #8604) if ( ( x >= 0 && x <= ( renderTarget.width - width ) ) && ( y >= 0 && y <= ( renderTarget.height - height ) ) ) { _gl.readPixels( x, y, width, height, utils.convert( textureFormat ), utils.convert( textureType ), buffer ); } } else { console.error( 'THREE.WebGLRenderer.readRenderTargetPixels: readPixels from renderTarget failed. Framebuffer not complete.' ); } [代码] 所以解决方法 [代码]// 在渲染循环里检测checkFramebufferStatus状态,完成后再读取像素 const render = () => { if (this.disposing) return requestAnimationFrame(render); demo.update() renderer.render(scene, camera); if (this.screenshotResolve) { // @ts-ignore 参考 Threejs WebGLRenderer.readRenderTargetPixels if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) { // @ts-ignore gl.readPixels(0, 0, frameBuffer.x, frameBuffer.y, gl.RGBA, gl.UNSIGNED_BYTE, pixelData); // 翻转Y轴 flip(pixelData, frameBuffer.x, frameBuffer.y, 4); // 确保有像素,微信小程序安卓在进入子页面返回本页面后,再一次readPixels稳定无像素 if (pixelData.some(i => i !== 0)) { this.screenshotResolve([pixelData, frameBuffer.x, frameBuffer.y]) this.screenshotResolve = null as unknown as Function; } } } } [代码] 具体可阅读代码片段 https://developers.weixin.qq.com/s/syS4xzmw7AnO
2021-01-23 - iOSWASM报错手动修复指引(MiniProgramError undefined is not an object)
手动修复微信8.0.9iOS客户端中WASM能力异常指引 概述 在微信客户端(iOS 8.0.9)中小游戏使用WASM时有概率对部分的WASM库调用发生报错,例如目前Cocos Creator3中使用WASM版本的Ammo物理引擎是将会发生异常。 该问题在微信开发者社区中其他人的提问地址为:https://developers.weixin.qq.com/community/minigame/doc/0002a0271c4d982fa07cdb3795b400 根据官方的说明是指JS胶水层代码在对同一个函数的导出做了次数限制,超过3次函数将没有实际的指针,官方将在后续的客户端版本修复这一问题,但修复前需要进行手动适配,经过本人的实际操作,已成功修复,因此本文将总结出这一问题的修复指引。 本文仓库:https://github.com/liuxinyumocn/WXWASMFIX 修复说明 下图是官方给出的解决思路 [图片] 简单地说,我们解决过程是: 在模拟器中打印出当前的WASM胶水层代码的句柄列表 list1; 再试图导出iOS微信客户端中的WASM胶水层代码的句柄列表 list2; 此时统计list2中缺失的(不在list1)中的函数名列表 list3; 回到list1中可以看到不同函数名其对应的实际指针ID(函数),将list3的Key健替换成list2中存在且指针相同的导出名,形成替换 map1; 根据map1完成对胶水层的更替,至此问题解决。 通常来说list的数量很大,我们需要写一个简单的脚本对其进行批量处理,为了方便大家的使用,我写了一个在线的替换工具,需要大家根据指引填入相应的信息,在线工具源代码在本仓库目录 /tool/ 内。由于修复是根据WASM在iOS真机上的实际表现有关,因此工具并不能一键完成转换,请参考下方实践完成这一修复工作。 实践修复 本文给出Ammo.wasm的手动修复实践案例。 修复前的 Demo 源代码在本仓库目录 /demo/prev/ 内 修复后的 Demo 源代码在本仓库目录 /demo/fixed/ 内 使用微信开发者工具打开 prev 代码包,模拟器与Android真机运行正常,iOS8.0.9客户端运行时控制台报如下错误: 报错内容为:未定义的对象 打开wasm的胶水层代码,本案例中为 wx.ammo.wasm.js ,在 17行 位置进行对 b.asm 的结构进行打印。 [代码]var b; setTimeout(()=>{ //使用setTimeout 是确保对象b被实例并完全初始化 console.log(b.asm); //打印 b.asm 本案例是 var b 其他案例自行观察代码 var description = ""; for (var i in b.asm) { //将对象的函数已经对应的实际指针ID转换成字符串 var property = b.asm[i]; description += i + " = " + property + "\n"; } wx.setClipboardData({ //利用剪贴板完整导出 data: description, }) },1000); b||(b=typeof Ammo !== 'undefined' ? Ammo : {}); // others... [代码] 打印结果如下: [图片] 其中 $、$a…就是导出函数名,后面的 f 27() 、f 49() 是导出的函数指向的实际指针,报错的原因就是同一个实际指针被导出多个函数名(意味着右边存在超过3个以上相同的实际指针),多出的导出名在iOS中被忽略掉,因此导致了报错。 分别在模拟器与iOS真机中运行该脚本,获得粘贴板内的文本内容。可以看到模拟器中有855个导出函数,而iOS真机只有600个导出函数。接下来我们需要借助脚本进行批量扫描,得到需要替换的序列。使用浏览器打开本仓库 /tool/index.html 替换工具,将模拟器给出的粘贴板内容复制到第一个文本框内,将真机给出的粘贴板内容复制到第二个文本框内,点击识别。如下图所示。 [图片] 将胶水层代码全部复制粘贴至替换工具中的第三个文本框内,本文中也就是 wx.ammo.wasm.js 文件的源代码。 观察一下替换的部分的代码规则,以本文为例,需要将 [代码]=function(){return b.asm.XXXX.apply(null,arguments)} [代码] 更替为 [代码]=function(){return b.asm.YYYY.apply(null,arguments)} [代码] 因此在工具的第四个文本框定义替换规则: 使用 ${Fun} 来声明更替的函数部位。 以上述更替案例为例,则第四个文本框应该输入: =function(){return b.asm.${fun}.apply(null,arguments)} 最后点击替换,最后一个文本框即是替换的源代码,覆盖原胶水层代码,删除调试时的注释,问题到此解决。
2021-08-13 - wasm的胶水js该如何修改以适配WXWebAssembly?
比如一个最简单的hello world cpp通过Emscripten编译成hello.wasm和胶水js:hello.js. //hello.cc #include <stdio.h> #ifndef EM_PORT_API # if defined(__EMSCRIPTEN__) # include <emscripten.h> # if defined(__cplusplus) # define EM_PORT_API(rettype) extern "C" rettype EMSCRIPTEN_KEEPALIVE # else # define EM_PORT_API(rettype) rettype EMSCRIPTEN_KEEPALIVE # endif # else # if defined(__cplusplus) # define EM_PORT_API(rettype) extern "C" rettype # else # define EM_PORT_API(rettype) rettype # endif # endif #endif EM_PORT_API(int) show_me_the_answer() { return 42; } EM_PORT_API(float) add(float a, float b) { return a + b; } int main() { printf("Hello World-\n"); return 0; } 在普通的html中可以顺利调用: <script> Module = {}; Module.onRuntimeInitialized = function() { //do sth. Module._main(); console.log(Module._add(12, 1.0)); } </script> <script src="hello.js"></script> 在小程序中似乎只WXWebAssembly.instantiate(path, imports) 请问该如何修改胶水js以适配呢?WebAssembly.RuntimeError等需要修改成WXWebAssembly.xxx吗? 能否帮忙提供一个小程序调用c++ 转化的wasm的最小示例?谢谢! (或者说小程序还提供其他更方便地使用c++进行密集运算的方式吗?)
2021-11-19 - 微信小游戏绘制排行榜时,IOS机型在滑动canvas时会闪屏
- 当前 Bug 的表现(可附上截图) 查看排行榜、滑动排行榜时会闪屏,而且很严重,非常影响体验。闪屏是黑屏和正常显示交替出现,黑屏时就是整个屏幕是全黑的,然后又显示正常 - 预期表现 实现在开放数据域里,绘制可以滑动的排行榜 - 复现路径 IOS机型(ip7,ip6s,ipad2017都出现)查看排行榜、滑动排行榜时会闪屏,而且很严重,非常影响体验 - 提供一个最简复现 Demo 好像是在心跳定时器里运行的loop函数在重绘canvas时就会闪屏 /** * 循环函数 * 每帧判断一下是否需要渲染 * 如果被标脏,则重新渲染 */ function loop() { if (renderDirty) { // console.log(`stageWidth :${stageWidth} stageHeight:${stageHeight}`) context.setTransform(1, 0, 0, 1, 0, 0); context.clearRect(0, 0, sharedCanvas.width, sharedCanvas.height); drawRankPanel(); renderDirty = false; } requestAnimationFrameID = requestAnimationFrame(loop); } wx.onTouchMove((event) => { var l = event.changedTouches.length; for (var i = 0; i < l; i++) { onTouchMove(event.changedTouches[i]); } }) function onTouchMove(event) { // console.log("onTouchMove evt=", event); if (totalGroup.length<=2||isOpen==false){ return; } touchY = event.clientY; renderDirty = true; console.log("2"); } /**绘制自己的排行榜信息 */ function drawMyRankByData(data,i){ let myY = 1095/1334 *stageHeight; // 绘制List背景 context.drawImage(assets.myListBG, startX, myY, barWidth, barHeight); //绘制玩家排名 switch (i) { case 0: context.drawImage(assets.NO1Icon, NoOffsetX, myY + NoOffsetY, NoWidth, NoHeight); break; case 1: context.drawImage(assets.NO2Icon, NoOffsetX, myY + NoOffsetY, NoWidth, NoHeight); break; case 2: context.drawImage(assets.NO3Icon, NoOffsetX, myY + NoOffsetY , NoWidth, NoHeight); break; default: //设置字体和描边 context.font = 40 + "px Arial"; context.lineWidth = 8; //描边大小 context.strokeStyle = '#04af75';//描边颜色 context.strokeText((i + 1) + "", NoTextX, myY + textOffsetY, textMaxSize);//描边文字 context.fillStyle = "#ffffff";//字体颜色 context.fillText((i + 1) + "", NoTextX, myY + textOffsetY, textMaxSize);//绘制序号 } //绘制头像 var image2 = wx.createImage(); image2.src = data.url; image2.onload = function () { renderDirty = true; console.log("myonload。。。"); } context.font = fontSize + "px Arial"; context.fillStyle = "#ffffff";//字体颜色 context.drawImage(image2, iconStartX, myY + iconOffsetY , avatarSize, avatarSize); //绘制名称 context.fillText(data.name + "", nameStartX,myY + textOffsetY , textMaxSize); //绘制分数 context.fillText("关卡 " + data.scroes, scoreStartX, myY + textOffsetY, textMaxSize); // renderDirty = true; }
2018-09-26 - 小程序 font-family 设置自定义字体
小程序 font-family 设置自定义字体 现在 模拟器里有 设置字体成功[图片] 但在真机上无效 [图片] 请问是什么原因?是小程序不支持还是代码问题? wx.loadFontFace({ family: 'webfont', source: 'url("../../DINCond-Regular.otf")', success(e){ console.log(e) }, fail(e){ console.log(e) } })
2018-12-14 - 社区每周 | 上周社区问题反馈以及功能优化更新(07.23-07.27)
各位微信开发者: 大家下午好。 以下是上周我们新增能力(beta版)的更细说明,以及在社区收到的问题反馈、需求处理进度。希望同大家一同打造小程序生态。 Beta 版能力更新(07.30-08.03)开发者工具beta版本新增 Git 版本管理功能 查看能力: 项目窗口新增版本管理面板,开发者可以方便地对项目进行简单的 Git 版本管理操作。 npm 支持能力更新 查看能力: 支持自定义配置构建文件生成目录(默认为 miniprogram_dist )。支持在单个 npm 包中放置多个模块/自定义组件,通过包名和包内相对路径的方式引入。 下载地址 内测版工具 下载链接 Beta 内测能力汇总: Git 版本管理功能 npm 支持 小程序体验评分 查看能力 上周问题反馈和处理进度(07.23-07.27)修复中的问题微信6.7.2安卓测试版客户端 sceneID 错误的问题 查看反馈 开发者工具模拟器调节栏消失的问题 查看反馈 iPad代码包中的图片真机上无法显示的问题 查看反馈 印刷出来的小程序码无法识别 查看反馈 开发工具的 ScreenX 和 ScreenY 显示错误的问题 查看反馈 cover-view 嵌入 button 按钮无法 open-type的问题 查看反馈 素材管理素材较多的时候显示错误的问题 查看反馈 小程序 数据分析-访问分析 带参二维码翻页后数据不完整显示的问题 查看反馈 运维中心错误查询中出现小程序中未有页面的问题 查看反馈 Wxml 代码补全使用单引号的问题 查看反馈 wx.loadFontFace source 为 base64 遇到长度限制的问题 查看反馈 小程序所有需要用到授权的 API 和组件均无法在无网环境使用的问题 查看反馈 需求反馈跟进迭代中管理后台小程序版本显示修改未开发者自定义的格式的需求 查看反馈 希望 wx.chooseImage 返回的 tempFiles 中增加时间属性的需求 查看反馈 希望视频弹幕增加多字体支持的需求 查看反馈 小程序多语言切换的需求 查看反馈 希望 cover-view 在真机上支持 touchmove 事件 查看反馈 微信团队 2018.08.01
2018-08-01